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 { IconUser, IconTerminal, IconSetting } from '@douyinfe/semi-icons';
import './Menu.less';
function parsePathName(name) {
const split = name.split('/').filter((s) => s.length !== 0);
@@ -14,7 +15,12 @@ const TopMenu = function TopMenu({ isAdmin }) {
const history = useHistory();
const location = useLocation();
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
itemKey="/jobs"
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 { IconDelete, IconEdit, IconHistogram } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import './JobTable.less';
const empty = (
<Empty
image={<IllustrationNoResult />}
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',
},
{
title: 'Number of findings',
title: 'Findings',
dataIndex: 'numberOfFoundListings',
render: (value) => {
return value || 0;
},
},
{
title: 'Active provider',
title: 'Providers',
dataIndex: 'provider',
render: (value) => {
return value.length || 0;
},
},
{
title: 'Active notification adapter',
title: 'Notification adapters',
dataIndex: 'notificationAdapter',
render: (value) => {
return value.length || 0;
@@ -54,19 +57,9 @@ export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged,
dataIndex: 'tools',
render: (_, job) => {
return (
<div style={{ float: 'right' }}>
<Button
type="primary"
icon={<IconHistogram />}
onClick={() => onJobInsight(job.id)}
style={{ marginRight: '1rem' }}
/>
<Button
type="secondary"
icon={<IconEdit />}
onClick={() => onJobEdit(job.id)}
style={{ marginRight: '1rem' }}
/>
<div className="interactions">
<Button type="primary" icon={<IconHistogram />} onClick={() => onJobInsight(job.id)} />
<Button type="secondary" icon={<IconEdit />} onClick={() => onJobEdit(job.id)} />
<Button type="danger" icon={<IconDelete />} onClick={() => onJobRemoval(job.id)} />
</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 (
<Table
pagination={false}
empty={<Empty description="No Data" />}
empty={<Empty description="No notification adapters found." />}
columns={[
{
title: 'Notification Adapter Name',
title: 'Name',
dataIndex: 'name',
},

View File

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

View File

@@ -9,7 +9,7 @@ const empty = (
<Empty
image={<IllustrationNoResult />}
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 throwMessage = (message, type) => {
if (type === 'error') {
Toast.error(message);
} else {
Toast.success(message);
}
};
const onStore = async () => {
if (nullOrEmpty(interval)) {
throwMessage('Interval may not be empty.', 'error');
Toast.error('Interval may not be empty.');
return;
}
if (nullOrEmpty(port)) {
throwMessage('Port may not be empty.', 'error');
Toast.error('Port may not be empty.');
return;
}
if (
(!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;
}
try {
@@ -109,13 +101,13 @@ const GeneralSettings = function GeneralSettings() {
} catch (exception) {
console.error(exception);
if (exception?.json?.message != null) {
throwMessage(exception.json.message, 'error');
Toast.error(exception.json.message);
} else {
throwMessage('Error while trying to store settings.', 'error');
Toast.error('Error while trying to store settings.');
}
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(() => {
location.reload();
}, 3000);

View File

@@ -38,7 +38,7 @@ export default function JobMutator() {
const dispatch = useDispatch();
const isSavingEnabled = () => {
return notificationAdapterData.length > 0 && providerData.length > 0 && name != null && name.length > 0;
return Boolean(notificationAdapterData.length && providerData.length && name);
};
const mutateJob = async () => {
@@ -105,13 +105,13 @@ export default function JobMutator() {
</SegmentPart>
<Divider margin="1rem" />
<SegmentPart
name="Provider"
name="Providers"
icon="briefcase"
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 ' +
'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 ' +
'to search for new listings.'
}
helpText={`
A provider is essentially the service (e.g. ImmoScout24, Kleinanzeigen) that Fredy searches for new listings.
Fredy will open a new tab pointing 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 in here.
`}
>
<Button
type="primary"
@@ -132,7 +132,7 @@ export default function JobMutator() {
<Divider margin="1rem" />
<SegmentPart
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."
>
<Button
@@ -172,7 +172,7 @@ export default function JobMutator() {
<SegmentPart
icon="play circle outline"
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} />
</SegmentPart>

View File

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

View File

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