Mobile view and wording (#151)

* feat(ui): simplified titles and adjusted some wording

* style(ui): simplified some views for mobile

* style(ui): make job table responsive for mobile

* style(ui): login button gap

* style(ui): dont hide mobile columns

* fix: method return type
This commit is contained in:
Alexander Roidl
2025-08-01 09:51:42 +02:00
committed by GitHub
parent 2b36f868e7
commit ae4b6d1f40
11 changed files with 67 additions and 59 deletions

View File

@@ -4,6 +4,7 @@ import { Tabs, TabPane } from '@douyinfe/semi-ui';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import { IconUser, IconTerminal, IconSetting } from '@douyinfe/semi-icons'; import { IconUser, IconTerminal, IconSetting } from '@douyinfe/semi-icons';
import './Menu.less';
function parsePathName(name) { function parsePathName(name) {
const split = name.split('/').filter((s) => s.length !== 0); const split = name.split('/').filter((s) => s.length !== 0);
@@ -14,7 +15,12 @@ const TopMenu = function TopMenu({ isAdmin }) {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
return ( return (
<Tabs type="line" activeKey={parsePathName(location.pathname)} onTabClick={(key) => history.push(key)}> <Tabs
className="menu"
type="line"
activeKey={parsePathName(location.pathname)}
onTabClick={(key) => history.push(key)}
>
<TabPane <TabPane
itemKey="/jobs" itemKey="/jobs"
tab={ tab={

View File

@@ -0,0 +1,3 @@
.menu {
margin-top: 3rem;
}

View File

@@ -3,11 +3,14 @@ import React from 'react';
import { Button, Empty, Table, Switch } from '@douyinfe/semi-ui'; import { Button, Empty, Table, Switch } from '@douyinfe/semi-ui';
import { IconDelete, IconEdit, IconHistogram } from '@douyinfe/semi-icons'; import { IconDelete, IconEdit, IconHistogram } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import './JobTable.less';
const empty = ( const empty = (
<Empty <Empty
image={<IllustrationNoResult />} image={<IllustrationNoResult />}
darkModeImage={<IllustrationNoResultDark />} darkModeImage={<IllustrationNoResultDark />}
description={'No jobs available'} description={'No jobs available.'}
/> />
); );
@@ -25,25 +28,25 @@ export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged,
}, },
}, },
{ {
title: 'Job Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
}, },
{ {
title: 'Number of findings', title: 'Findings',
dataIndex: 'numberOfFoundListings', dataIndex: 'numberOfFoundListings',
render: (value) => { render: (value) => {
return value || 0; return value || 0;
}, },
}, },
{ {
title: 'Active provider', title: 'Providers',
dataIndex: 'provider', dataIndex: 'provider',
render: (value) => { render: (value) => {
return value.length || 0; return value.length || 0;
}, },
}, },
{ {
title: 'Active notification adapter', title: 'Notification adapters',
dataIndex: 'notificationAdapter', dataIndex: 'notificationAdapter',
render: (value) => { render: (value) => {
return value.length || 0; return value.length || 0;
@@ -54,19 +57,9 @@ export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged,
dataIndex: 'tools', dataIndex: 'tools',
render: (_, job) => { render: (_, job) => {
return ( return (
<div style={{ float: 'right' }}> <div className="interactions">
<Button <Button type="primary" icon={<IconHistogram />} onClick={() => onJobInsight(job.id)} />
type="primary" <Button type="secondary" icon={<IconEdit />} onClick={() => onJobEdit(job.id)} />
icon={<IconHistogram />}
onClick={() => onJobInsight(job.id)}
style={{ marginRight: '1rem' }}
/>
<Button
type="secondary"
icon={<IconEdit />}
onClick={() => onJobEdit(job.id)}
style={{ marginRight: '1rem' }}
/>
<Button type="danger" icon={<IconDelete />} onClick={() => onJobRemoval(job.id)} /> <Button type="danger" icon={<IconDelete />} onClick={() => onJobRemoval(job.id)} />
</div> </div>
); );

View File

@@ -0,0 +1,12 @@
.interactions {
float: right;
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.interactions {
flex-direction: initial;
}
}

View File

@@ -7,10 +7,10 @@ export default function NotificationAdapterTable({ notificationAdapter = [], onR
return ( return (
<Table <Table
pagination={false} pagination={false}
empty={<Empty description="No Data" />} empty={<Empty description="No notification adapters found." />}
columns={[ columns={[
{ {
title: 'Notification Adapter Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
}, },

View File

@@ -7,14 +7,14 @@ export default function ProviderTable({ providerData = [], onRemove } = {}) {
return ( return (
<Table <Table
pagination={false} pagination={false}
empty={<Empty description="No Provider available" />} empty={<Empty description="No providers found." />}
columns={[ columns={[
{ {
title: 'Provider Name', title: 'Name',
dataIndex: 'name', dataIndex: 'name',
}, },
{ {
title: 'Provider Url', title: 'URL',
dataIndex: 'url', dataIndex: 'url',
render: (_, data) => { render: (_, data) => {
return ( return (

View File

@@ -9,7 +9,7 @@ const empty = (
<Empty <Empty
image={<IllustrationNoResult />} image={<IllustrationNoResult />}
darkModeImage={<IllustrationNoResultDark />} darkModeImage={<IllustrationNoResultDark />}
description={'No user available'} description={'No users found.'}
/> />
); );

View File

@@ -71,28 +71,20 @@ const GeneralSettings = function GeneralSettings() {
const nullOrEmpty = (val) => val == null || val.length === 0; const nullOrEmpty = (val) => val == null || val.length === 0;
const throwMessage = (message, type) => {
if (type === 'error') {
Toast.error(message);
} else {
Toast.success(message);
}
};
const onStore = async () => { const onStore = async () => {
if (nullOrEmpty(interval)) { if (nullOrEmpty(interval)) {
throwMessage('Interval may not be empty.', 'error'); Toast.error('Interval may not be empty.');
return; return;
} }
if (nullOrEmpty(port)) { if (nullOrEmpty(port)) {
throwMessage('Port may not be empty.', 'error'); Toast.error('Port may not be empty.');
return; return;
} }
if ( if (
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) || (!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo)) (nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
) { ) {
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error'); Toast.error('Working hours to and from must be set if either to or from has been set before.');
return; return;
} }
try { try {
@@ -109,13 +101,13 @@ const GeneralSettings = function GeneralSettings() {
} catch (exception) { } catch (exception) {
console.error(exception); console.error(exception);
if (exception?.json?.message != null) { if (exception?.json?.message != null) {
throwMessage(exception.json.message, 'error'); Toast.error(exception.json.message);
} else { } else {
throwMessage('Error while trying to store settings.', 'error'); Toast.error('Error while trying to store settings.');
} }
return; return;
} }
throwMessage('Settings stored successfully. We will reload your browser in 3 seconds.', 'success'); Toast.success('Settings stored successfully. We will reload your browser in 3 seconds.');
setTimeout(() => { setTimeout(() => {
location.reload(); location.reload();
}, 3000); }, 3000);

View File

@@ -38,7 +38,7 @@ export default function JobMutator() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isSavingEnabled = () => { const isSavingEnabled = () => {
return notificationAdapterData.length > 0 && providerData.length > 0 && name != null && name.length > 0; return Boolean(notificationAdapterData.length && providerData.length && name);
}; };
const mutateJob = async () => { const mutateJob = async () => {
@@ -105,13 +105,13 @@ export default function JobMutator() {
</SegmentPart> </SegmentPart>
<Divider margin="1rem" /> <Divider margin="1rem" />
<SegmentPart <SegmentPart
name="Provider" name="Providers"
icon="briefcase" icon="briefcase"
helpText={ helpText={`
'A provider is essentially the service (Immowelt etc.) that Fredy is using to search for new listings. When adding a new provider, Fredy will open a new tab pointing ' + A provider is essentially the service (e.g. ImmoScout24, Kleinanzeigen) that Fredy searches for new listings.
'to the website of this provider. You have to adjust your search parameter and click on "Search". If the results are being shown, copy the browser url. This is the url, Fredy will use ' + Fredy will open a new tab pointing to the website of this provider. You have to adjust your search parameter
'to search for new listings.' and click on "Search". If the results are being shown, copy the browser URL in here.
} `}
> >
<Button <Button
type="primary" type="primary"
@@ -132,7 +132,7 @@ export default function JobMutator() {
<Divider margin="1rem" /> <Divider margin="1rem" />
<SegmentPart <SegmentPart
icon="bell" icon="bell"
name="Notification Adapter" name="Notification Adapters"
helpText="Fredy supports multiple ways to notify you about new findings. These are called notification adapter. You can chose between email, Telegram etc." helpText="Fredy supports multiple ways to notify you about new findings. These are called notification adapter. You can chose between email, Telegram etc."
> >
<Button <Button
@@ -172,7 +172,7 @@ export default function JobMutator() {
<SegmentPart <SegmentPart
icon="play circle outline" icon="play circle outline"
name="Job activation" name="Job activation"
helpText="Whether or not the job is activated. If it is not activated, it will be ignored when Fredy checks for new listings." helpText="Whether or not the job is activated. Inactive jobs will be ignored when Fredy checks for new listings."
> >
<Switch className="jobMutation__spaceTop" onChange={(checked) => setEnabled(checked)} checked={enabled} /> <Switch className="jobMutation__spaceTop" onChange={(checked) => setEnabled(checked)} checked={enabled} />
</SegmentPart> </SegmentPart>

View File

@@ -5,7 +5,7 @@ import Logo from '../../components/logo/Logo';
import { xhrPost } from '../../services/xhr'; import { xhrPost } from '../../services/xhr';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Input, Button, Banner } from '@douyinfe/semi-ui'; import { Input, Button, Banner, Toast } from '@douyinfe/semi-ui';
import './login.less'; import './login.less';
import { IconUser, IconLock } from '@douyinfe/semi-icons'; import { IconUser, IconLock } from '@douyinfe/semi-icons';
@@ -27,20 +27,24 @@ export default function Login() {
}, []); }, []);
const tryLogin = async () => { const tryLogin = async () => {
if (username.length === 0 || password.length === 0) { if (!username?.trim() || !password) {
setError('Username and password are mandatory.'); setError('Username and password are mandatory.');
return; return;
} }
setError(null);
try { try {
await xhrPost('/api/login', { await xhrPost('/api/login', {
username, username: username.trim(),
password, password,
}); });
setError(null);
} catch (Exception) { } catch (Exception) {
setError('Login not successful...'); Toast.error('Login unsuccessful');
return; return;
} }
Toast.success('Login successful!');
await dispatch.user.getCurrentUser(); await dispatch.user.getCurrentUser();
history.push('/jobs'); history.push('/jobs');
}; };
@@ -58,7 +62,6 @@ export default function Login() {
placeholder="Username" placeholder="Username"
value={username} value={username}
showClear showClear
style={{ marginTop: error ? '1rem' : '4rem' }}
autoFocus autoFocus
onChange={(value) => setUserName(value)} onChange={(value) => setUserName(value)}
onKeyPress={async (e) => { onKeyPress={async (e) => {
@@ -74,7 +77,6 @@ export default function Login() {
prefix={<IconLock />} prefix={<IconLock />}
value={password} value={password}
placeholder="Password" placeholder="Password"
style={{ marginTop: '2rem' }}
onChange={(value) => setPassword(value)} onChange={(value) => setPassword(value)}
onKeyPress={async (e) => { onKeyPress={async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@@ -83,10 +85,10 @@ export default function Login() {
}} }}
/> />
<Button type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '3rem' }}> <Button type="primary" onClick={tryLogin} theme="solid" style={{ marginTop: '1rem' }}>
Login Login
</Button> </Button>
<br />
{demoMode && ( {demoMode && (
<Banner <Banner
fullMode={true} fullMode={true}

View File

@@ -20,13 +20,13 @@
&__loginWrapper { &__loginWrapper {
border: 1px solid #555050; border: 1px solid #555050;
border-radius: 30px; border-radius: 30px;
height: 23rem;
width: 30rem;
z-index: 1; z-index: 1;
background-color: #151313ab; background-color: #151313ab;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2rem; padding: 2rem;
gap: 1rem;
} }
form { form {