mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Calculating the distance (#255)
* migra for distance * adding distance calculator * adding ability to store home address * improve distance calculation * calculating distance * show distance in grid view * upgrading dependencies * moving to react 19 * ability to clone a job * fixing tests * polishing
This commit is contained in:
committed by
GitHub
parent
51b4e51f3f
commit
4dd0370ec1
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Col, Row, Toast } from '@douyinfe/semi-ui';
|
||||
import { Button, Col, Row, Toast } from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
IconTerminal,
|
||||
IconStar,
|
||||
|
||||
@@ -7,11 +7,11 @@ import React from 'react';
|
||||
|
||||
import { useActions, useSelector } from '../../services/state/store';
|
||||
|
||||
import { Divider, TimePicker, Button, Checkbox, Input, Modal } from '@douyinfe/semi-ui';
|
||||
import { InputNumber } from '@douyinfe/semi-ui';
|
||||
import { Divider, TimePicker, Button, Checkbox, Input, Modal } 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';
|
||||
import { Banner, Toast } from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
downloadBackup as downloadBackupZip,
|
||||
precheckRestore as clientPrecheckRestore,
|
||||
|
||||
@@ -12,8 +12,8 @@ import ProviderMutator from './components/provider/ProviderMutator';
|
||||
import Headline from '../../../components/headline/Headline';
|
||||
import { useActions, useSelector } from '../../../services/state/store';
|
||||
import { xhrPost } from '../../../services/xhr';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Divider, Input, Switch, Button, TagInput, Toast, Select } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { Divider, Input, Switch, Button, TagInput, Toast, Select } from '@douyinfe/semi-ui-19';
|
||||
import './JobMutation.less';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart';
|
||||
import {
|
||||
@@ -30,14 +30,20 @@ export default function JobMutator() {
|
||||
const jobs = useSelector((state) => state.jobsData.jobs);
|
||||
const shareableUserList = useSelector((state) => state.jobsData.shareableUserList);
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
const cloneFromId = location.state?.cloneFrom;
|
||||
const jobToClone = cloneFromId ? jobs.find((job) => job.id === cloneFromId) : null;
|
||||
const jobToBeEdit = params.jobId == null ? null : jobs.find((job) => job.id === params.jobId);
|
||||
|
||||
const defaultBlacklist = jobToBeEdit?.blacklist || [];
|
||||
const defaultName = jobToBeEdit?.name || null;
|
||||
const defaultProviderData = jobToBeEdit?.provider || [];
|
||||
const defaultNotificationAdapter = jobToBeEdit?.notificationAdapter || [];
|
||||
const defaultEnabled = jobToBeEdit?.enabled ?? true;
|
||||
const sourceJob = jobToBeEdit || jobToClone;
|
||||
|
||||
const defaultBlacklist = sourceJob?.blacklist || [];
|
||||
const defaultName = jobToClone ? `Copy of - ${sourceJob?.name}` : sourceJob?.name || null;
|
||||
const defaultProviderData = sourceJob?.provider || [];
|
||||
const defaultNotificationAdapter = sourceJob?.notificationAdapter || [];
|
||||
const defaultEnabled = sourceJob?.enabled ?? true;
|
||||
const defaultShareWithUsers = sourceJob?.shared_with_user ?? [];
|
||||
|
||||
const [providerToEdit, setProviderToEdit] = useState(null);
|
||||
const [providerCreationVisible, setProviderCreationVisibility] = useState(false);
|
||||
@@ -47,7 +53,7 @@ export default function JobMutator() {
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [blacklist, setBlacklist] = useState(defaultBlacklist);
|
||||
const [notificationAdapterData, setNotificationAdapterData] = useState(defaultNotificationAdapter);
|
||||
const [shareWithUsers, setShareWithUsers] = useState(jobToBeEdit?.shared_with_user ?? []);
|
||||
const [shareWithUsers, setShareWithUsers] = useState(defaultShareWithUsers);
|
||||
const [enabled, setEnabled] = useState(defaultEnabled);
|
||||
const navigate = useNavigate();
|
||||
const actions = useActions();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { transform } from '../../../../../services/transformer/notificationAdapt
|
||||
import { xhrPost } from '../../../../../services/xhr';
|
||||
import Help from './NotificationHelpDisplay';
|
||||
import { useSelector } from '../../../../../services/state/store';
|
||||
import { Banner, Button, Form, Modal, Select, Switch } from '@douyinfe/semi-ui';
|
||||
import { Banner, Button, Form, Modal, Select, Switch } from '@douyinfe/semi-ui-19';
|
||||
|
||||
import './NotificationAdapterMutator.less';
|
||||
import { useScreenWidth } from '../../../../../hooks/screenWidth.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Banner, MarkdownRender } from '@douyinfe/semi-ui';
|
||||
import { Banner, MarkdownRender } from '@douyinfe/semi-ui-19';
|
||||
|
||||
export default function Help({ readme }) {
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { Banner, Modal, Select, Input } from '@douyinfe/semi-ui';
|
||||
import { Banner, Modal, Select, Input } from '@douyinfe/semi-ui-19';
|
||||
import { transform } from '../../../../../services/transformer/providerTransformer';
|
||||
import { useSelector } from '../../../../../services/state/store';
|
||||
import { IconLikeHeart } from '@douyinfe/semi-icons';
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import { useSelector, useActions } from '../../services/state/store.js';
|
||||
import { Select, Space, Typography, Button, Popover, Divider, Switch, Banner } from '@douyinfe/semi-ui';
|
||||
import { Select, Space, Typography, Button, Popover, Divider, Switch, Banner } from '@douyinfe/semi-ui-19';
|
||||
import { IconFilter } from '@douyinfe/semi-icons';
|
||||
import no_image from '../../assets/no_image.jpg';
|
||||
import RangeSlider from 'react-range-slider-input';
|
||||
@@ -281,12 +281,14 @@ export default function MapView() {
|
||||
</div>
|
||||
</div>
|
||||
<Popover content="Filter Results" style={{ color: 'white', padding: '.5rem' }}>
|
||||
<Button
|
||||
icon={<IconFilter />}
|
||||
onClick={() => {
|
||||
setShowFilterBar(!showFilterBar);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
icon={<IconFilter />}
|
||||
onClick={() => {
|
||||
setShowFilterBar(!showFilterBar);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconHorn } from '@douyinfe/semi-icons';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart.jsx';
|
||||
import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui';
|
||||
import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui-19';
|
||||
import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx';
|
||||
import Headline from '../../../components/headline/Headline.jsx';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Logo from '../../components/logo/Logo';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useActions, useSelector } from '../../services/state/store';
|
||||
import { Input, Button, Banner, Toast } from '@douyinfe/semi-ui';
|
||||
import { Input, Button, Banner, Toast } from '@douyinfe/semi-ui-19';
|
||||
|
||||
import './login.less';
|
||||
import { IconUser, IconLock } from '@douyinfe/semi-icons';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
import { Modal } from '@douyinfe/semi-ui-19';
|
||||
const UserRemovalModal = function UserRemovalModal({ onOk, onCancel }) {
|
||||
return (
|
||||
<Modal title="Removing user" visible={true} closable={false} onOk={onOk} onCancel={onCancel}>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Toast } from '@douyinfe/semi-ui';
|
||||
import { Toast } from '@douyinfe/semi-ui-19';
|
||||
import UserTable from '../../components/table/UserTable';
|
||||
import { useActions, useSelector } from '../../services/state/store';
|
||||
import { IconPlus } from '@douyinfe/semi-icons';
|
||||
import { Button } from '@douyinfe/semi-ui';
|
||||
import { Button } from '@douyinfe/semi-ui-19';
|
||||
import UserRemovalModal from './UserRemovalModal';
|
||||
import { xhrDelete } from '../../services/xhr';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -8,7 +8,7 @@ import React from 'react';
|
||||
import { xhrGet, xhrPost } from '../../../services/xhr';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useActions } from '../../../services/state/store';
|
||||
import { Divider, Input, Switch, Button, Toast } from '@douyinfe/semi-ui';
|
||||
import { Divider, Input, Switch, Button, Toast } from '@douyinfe/semi-ui-19';
|
||||
import './UserMutator.less';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart';
|
||||
import { IconPlusCircle } from '@douyinfe/semi-icons';
|
||||
|
||||
119
ui/src/views/userSettings/UserSettings.jsx
Normal file
119
ui/src/views/userSettings/UserSettings.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2026 by Christian Kellner.
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { Divider, Button, AutoComplete, Toast, Typography, Banner } from '@douyinfe/semi-ui-19';
|
||||
import { IconSave, IconHome } from '@douyinfe/semi-icons';
|
||||
import { xhrGet, xhrPost } from '../../services/xhr';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const UserSettings = () => {
|
||||
const [address, setAddress] = useState('');
|
||||
const [coords, setCoords] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [dataSource, setDataSource] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserSettings();
|
||||
}, []);
|
||||
|
||||
const fetchUserSettings = async () => {
|
||||
try {
|
||||
const response = await xhrGet('/api/user/settings');
|
||||
if (response.status === 200) {
|
||||
const homeAddress = response.json.home_address;
|
||||
setAddress(homeAddress?.address || '');
|
||||
setCoords(homeAddress?.coords || null);
|
||||
}
|
||||
} catch {
|
||||
Toast.error('Failed to fetch user settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await xhrPost('/api/user/settings', { home_address: address });
|
||||
if (response.status === 200) {
|
||||
setCoords(response.json.coords);
|
||||
Toast.success('Settings saved successfully');
|
||||
} else {
|
||||
Toast.error(response.json.error || 'Failed to save settings');
|
||||
}
|
||||
} catch (error) {
|
||||
Toast.error(error.json?.error || 'Error while saving settings');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((value) => {
|
||||
xhrGet(`/api/user/settings/autocomplete?q=${encodeURIComponent(value)}`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setDataSource(response.json);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently fail for autocomplete
|
||||
});
|
||||
}, 300),
|
||||
[],
|
||||
);
|
||||
|
||||
const searchAddress = (value) => {
|
||||
if (!value) {
|
||||
setDataSource([]);
|
||||
return;
|
||||
}
|
||||
debouncedSearch(value);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-settings">
|
||||
<Title heading={2}>User Specific Settings</Title>
|
||||
<Divider />
|
||||
<SegmentPart
|
||||
name="Distance claculation"
|
||||
Icon={IconHome}
|
||||
helpText="The address you enter is used to calculate the distance between your chosen location and each listing. The distance is computed using an approximate mathematical method and is intended to give you a rough indication of commute time. If you update your address, we will recalculate the distance for all active listings."
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '600px' }}>
|
||||
<AutoComplete
|
||||
data={dataSource}
|
||||
value={address}
|
||||
onChange={(v) => setAddress(v)}
|
||||
onSearch={searchAddress}
|
||||
placeholder="Enter your home address"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{coords && coords.lat === -1 && (
|
||||
<Banner type="danger" description="Address found but could not be geocoded accurately." closeIcon={null} />
|
||||
)}
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider />
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<Button icon={<IconSave />} theme="solid" type="primary" onClick={handleSave} loading={saving}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
Reference in New Issue
Block a user