/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import { useState, useEffect, useMemo } from 'react'; import { Card, Col, Row, Image, Button, Typography, Pagination, Toast, Divider, Input, Select, Empty, Radio, RadioGroup, } from '@douyinfe/semi-ui-19'; import { IconBriefcase, IconCart, IconClock, IconDelete, IconLink, IconMapPin, IconStar, IconStarStroked, IconSearch, IconActivity, IconEyeOpened, IconArrowUp, IconArrowDown, } from '@douyinfe/semi-icons'; import { useNavigate } from 'react-router-dom'; import ListingDeletionModal from '../../ListingDeletionModal.jsx'; 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 [deleteModalVisible, setDeleteModalVisible] = useState(false); const [listingToDelete, setListingToDelete] = useState(null); 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 confirmDeletion = async (hardDelete) => { try { await xhrDelete('/api/listings/', { ids: [listingToDelete], hardDelete }); Toast.success('Listing successfully removed'); loadData(); } catch (error) { Toast.error(error.message || 'Error deleting listing'); } finally { setDeleteModalVisible(false); setListingToDelete(null); } }; const cap = (val) => { return String(val).charAt(0).toUpperCase() + String(val).slice(1); }; return (
} showClear placeholder="Search" onChange={handleFilterChange} /> { const v = e.target.value; setActivityFilter(v === 'all' ? null : v === 'true'); }} > All Active Inactive { const v = e.target.value; setWatchListFilter(v === 'all' ? null : v === 'true'); }} > All Watched Unwatched
{(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)}
{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 )}
e.stopPropagation()}>
))}
{(listingsData?.result || []).length > 0 && (
)} { setDeleteModalVisible(false); setListingToDelete(null); }} />
); }; export default ListingsGrid;