Listing improvements (#222)

* upgrading dependencies, fixing image placeholder

* improving processing times label and hide when screen width is too low

* aligning run now button

* renaming settings -> general settings

* smaller security and memory improvements

* improving footer
This commit is contained in:
Christian Kellner
2025-11-01 10:46:55 +01:00
committed by GitHub
parent db3702ed33
commit 32c7518454
6 changed files with 409 additions and 256 deletions

View File

@@ -5,6 +5,8 @@
justify-content: space-between;
align-items: center;
height: 1.7rem;
border-radius: .3rem;
border-top: 1px solid #45464b;
&__version {
padding-left: .5rem;

View File

@@ -22,7 +22,7 @@ export default function Navigation({ isAdmin }) {
if (isAdmin) {
items.push({ itemKey: '/users', text: 'User Management', icon: <IconUser /> });
items.push({ itemKey: '/generalSettings', text: 'Settings', icon: <IconSetting /> });
items.push({ itemKey: '/generalSettings', text: 'General Settings', icon: <IconSetting /> });
}
function parsePathName(name) {

View File

@@ -14,8 +14,8 @@ import ListingsFilter from './ListingsFilter.jsx';
const columns = [
{
title: '#',
width: 100,
title: 'Watchlist',
width: 110,
dataIndex: 'isWatched',
sorter: true,
render: (id, row) => {
@@ -180,6 +180,7 @@ export default function ListingsTable() {
const [activityFilter, setActivityFilter] = useState(null);
const [providerFilter, setProviderFilter] = useState(null);
const [imageWidth, setImageWidth] = useState('100%');
const handlePageChange = (_page) => {
setPage(_page);
};
@@ -208,14 +209,29 @@ export default function ListingsTable() {
const handleFilterChange = useMemo(() => debounce((value) => setFreeTextFilter(value), 500), []);
useEffect(() => {
return () => {
// cleanup debounced handler to avoid memory leaks
handleFilterChange.cancel && handleFilterChange.cancel();
};
}, [handleFilterChange]);
const expandRowRender = (record) => {
return (
<div className="listingsTable__expanded">
<div>
{record.image_url == null ? (
<Image height={200} src={no_image} />
<Image height={200} width={180} src={no_image} />
) : (
<Image height={200} src={record.image_url} />
<Image
height={200}
width={imageWidth}
src={record.image_url}
onError={() => {
setImageWidth('180px');
}}
fallback={<Image height={200} src={no_image} />}
/>
)}
</div>
<div>
@@ -226,7 +242,7 @@ export default function ListingsTable() {
</Tag>
</Descriptions.Item>
<Descriptions.Item itemKey="Link">
<a href={record.link} target="_blank" rel="noreferrer">
<a href={record.link} target="_blank" rel="noopener noreferrer">
Link to Listing
</a>
</Descriptions.Item>

View File

@@ -1,16 +1,32 @@
import React from 'react';
import { format } from '../../services/time/timeService';
import { Button, Card, Col, Row, Toast } from '@douyinfe/semi-ui';
import { IconPlayCircle } from '@douyinfe/semi-icons';
import {
IconClock,
IconDoubleChevronLeft,
IconDoubleChevronRight,
IconPlayCircle,
IconSearch,
} from '@douyinfe/semi-icons';
import { xhrPost } from '../../services/xhr.js';
import './ProsessingTimes.less';
import { useScreenWidth } from '../../hooks/screenWidth.js';
function InfoCard({ title, value }) {
function InfoCard({ title, value, icon }) {
const { Meta } = Card;
return (
<Card style={{ maxWidth: '13rem', margin: '1rem', background: 'rgb(53, 54, 60)' }} title={title}>
{value}
</Card>
<div
style={{
margin: '1rem',
background: 'rgb(53, 54, 60)',
borderRadius: '.3rem',
padding: '1rem',
minHeight: '3rem',
}}
>
<Meta title={title} description={value} avatar={icon} />
</div>
);
}
@@ -18,32 +34,57 @@ export default function ProcessingTimes({ processingTimes = {} }) {
if (Object.keys(processingTimes).length === 0) {
return null;
}
const width = useScreenWidth();
const invisible = width <= 1180;
if (invisible) {
return null;
}
return (
<Row>
<Col span={6}>
<InfoCard title="Processing Interval" value={`${processingTimes.interval} min`} />
<InfoCard
title="Search Interval"
value={`${processingTimes.interval} min`}
icon={<IconClock style={{ color: 'rgba(var(--semi-grey-4), 1)' }} />}
/>
</Col>
{processingTimes.lastRun && (
<>
<Col span={6}>
<InfoCard title="Last run" value={format(processingTimes.lastRun)} />
<InfoCard
title="Last search"
icon={<IconDoubleChevronLeft style={{ color: 'rgba(var(--semi-grey-4), 1)' }} />}
value={format(processingTimes.lastRun)}
/>
</Col>
<Col span={6}>
<InfoCard title="Next run" value={format(processingTimes.lastRun + processingTimes.interval * 60000)} />
<InfoCard
title="Next search"
icon={<IconDoubleChevronRight style={{ color: 'rgba(var(--semi-grey-4), 1)' }} />}
value={format(processingTimes.lastRun + processingTimes.interval * 60000)}
/>
</Col>
</>
)}
<Col span={6}>
<InfoCard
title="Find Listings Now"
title="Search Now"
icon={<IconSearch style={{ color: 'rgba(var(--semi-grey-4), 1)' }} />}
value={
<Button
size="small"
style={{ marginTop: '.2rem' }}
icon={<IconPlayCircle />}
aria-label="Start now"
onClick={async () => {
await xhrPost('/api/jobs/startAll', null);
Toast.success('Successfully triggered Fredy search.');
try {
await xhrPost('/api/jobs/startAll', null);
Toast.success('Successfully triggered Fredy search.');
} catch {
Toast.error('Failed to trigger search');
}
}}
>
Search now