/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import React, { useState, useEffect, useMemo } from 'react'; import { Card, Col, Row, Image, Button, Space, Typography, Pagination, Toast, Divider, Input, Select, Popover, Empty, } from '@douyinfe/semi-ui-19'; import { IconBriefcase, IconCart, IconClock, IconDelete, IconLink, IconMapPin, IconStar, IconStarStroked, IconSearch, IconFilter, IconActivity, IconEyeOpened, } from '@douyinfe/semi-icons'; import { useNavigate } from 'react-router-dom'; import no_image from '../../../assets/no_image.jpg'; import * as timeService from '../../../services/time/timeService.js'; import { xhrDelete, xhrPost } from '../../../services/xhr.js'; import { useActions, useSelector } from '../../../services/state/store.js'; import debounce from 'lodash/debounce'; import './ListingsGrid.less'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; const { Text } = Typography; const ListingsGrid = () => { const listingsData = useSelector((state) => state.listingsData); const providers = useSelector((state) => state.provider); const jobs = useSelector((state) => state.jobsData.jobs); const actions = useActions(); const navigate = useNavigate(); const [page, setPage] = useState(1); const pageSize = 40; const [sortField, setSortField] = useState('created_at'); const [sortDir, setSortDir] = useState('desc'); const [freeTextFilter, setFreeTextFilter] = useState(null); const [watchListFilter, setWatchListFilter] = useState(null); const [jobNameFilter, setJobNameFilter] = useState(null); const [activityFilter, setActivityFilter] = useState(null); const [providerFilter, setProviderFilter] = useState(null); const [showFilterBar, setShowFilterBar] = useState(false); const loadData = () => { actions.listingsData.getListingsData({ page, pageSize, sortfield: sortField, sortdir: sortDir, freeTextFilter, filter: { watchListFilter, jobNameFilter, activityFilter, providerFilter }, }); }; useEffect(() => { loadData(); }, [page, sortField, sortDir, freeTextFilter, providerFilter, activityFilter, jobNameFilter, watchListFilter]); const handleFilterChange = useMemo(() => debounce((value) => setFreeTextFilter(value), 500), []); useEffect(() => { return () => { // cleanup debounced handler to avoid memory leaks handleFilterChange.cancel && handleFilterChange.cancel(); }; }, [handleFilterChange]); const handleWatch = async (e, item) => { e.preventDefault(); e.stopPropagation(); try { await xhrPost('/api/listings/watch', { listingId: item.id }); Toast.success(item.isWatched === 1 ? 'Listing removed from Watchlist' : 'Listing added to Watchlist'); loadData(); } catch (e) { console.error(e); Toast.error('Failed to operate Watchlist'); } }; const handlePageChange = (_page) => { setPage(_page); }; const cap = (val) => { return String(val).charAt(0).toUpperCase() + String(val).slice(1); }; return (
} showClear placeholder="Search" onChange={handleFilterChange} />
{showFilterBar && (
Filter by:
Sort by:
)} {(listingsData?.result || []).length === 0 && ( } darkModeImage={} description="No listings available yet..." /> )} {(listingsData?.result || []).map((item) => ( navigate(`/listings/listing/${item.id}`)} cover={
{!item.is_active &&
Inactive
}
} bodyStyle={{ padding: '12px' }} >
{cap(item.title)} } size="small"> {item.price} € } size="small" ellipsis={{ showTooltip: true }} style={{ width: '100%' }} > {item.address || 'No address provided'} }> {timeService.format(item.created_at, false)} }> {item.provider.charAt(0).toUpperCase() + item.provider.slice(1)} {item.distance_to_destination ? ( }> {item.distance_to_destination} m to chosen address ) : ( }> Distance cannot be calculated, provide an address )}
e.stopPropagation()}>
))}
{(listingsData?.result || []).length > 0 && (
)}
); }; export default ListingsGrid;