mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
UI improvements (#283)
* ui-improvements * improving dashboard and settings * improve job overview * improving job card * improving grid view of listings+ * restructuring settings * next release version
This commit is contained in:
committed by
GitHub
parent
a1289acf15
commit
8c039f0026
@@ -9,7 +9,6 @@ import {
|
||||
Col,
|
||||
Row,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Divider,
|
||||
Switch,
|
||||
@@ -20,6 +19,8 @@ import {
|
||||
Pagination,
|
||||
Toast,
|
||||
Empty,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
@@ -31,8 +32,10 @@ import {
|
||||
IconBriefcase,
|
||||
IconBell,
|
||||
IconSearch,
|
||||
IconFilter,
|
||||
IconPlusCircle,
|
||||
IconArrowUp,
|
||||
IconArrowDown,
|
||||
IconHome,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||
@@ -59,8 +62,6 @@ const JobGrid = () => {
|
||||
const [sortDir, setSortDir] = useState('asc');
|
||||
const [freeTextFilter, setFreeTextFilter] = useState(null);
|
||||
const [activityFilter, setActivityFilter] = useState(null);
|
||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [pendingDeletion, setPendingDeletion] = useState(null); // { type: 'job'|'listings', jobId }
|
||||
|
||||
@@ -200,73 +201,45 @@ const JobGrid = () => {
|
||||
|
||||
return (
|
||||
<div className="jobGrid">
|
||||
<Space vertical align="start" style={{ width: '100%', marginBottom: '16px' }} spacing="medium">
|
||||
<div className="jobGrid__topbar">
|
||||
<Button type="primary" icon={<IconPlusCircle />} onClick={() => navigate('/jobs/new')}>
|
||||
New Job
|
||||
</Button>
|
||||
<div className="jobGrid__searchbar" style={{ width: '100%' }}>
|
||||
<Input prefix={<IconSearch />} showClear placeholder="Search" onChange={handleFilterChange} />
|
||||
<Button
|
||||
icon={<IconFilter />}
|
||||
style={{ marginLeft: '8px' }}
|
||||
onClick={() => {
|
||||
setShowFilterBar(!showFilterBar);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
{showFilterBar && (
|
||||
<div className="jobGrid__toolbar">
|
||||
<Space wrap style={{ marginBottom: '1rem' }}>
|
||||
<div className="jobGrid__toolbar__card">
|
||||
<div>
|
||||
<Text strong>Filter by:</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '.3rem' }}>
|
||||
<Select
|
||||
placeholder="Status"
|
||||
showClear
|
||||
onChange={(val) => setActivityFilter(val)}
|
||||
value={activityFilter}
|
||||
style={{ width: 140 }}
|
||||
>
|
||||
<Select.Option value={true}>Active</Select.Option>
|
||||
<Select.Option value={false}>Not Active</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
<div className="jobGrid__toolbar__card">
|
||||
<div>
|
||||
<Text strong>Sort by:</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '.3rem' }}>
|
||||
<Select
|
||||
placeholder="Sort By"
|
||||
style={{ width: 160 }}
|
||||
value={sortField}
|
||||
onChange={(val) => setSortField(val)}
|
||||
>
|
||||
<Select.Option value="name">Name</Select.Option>
|
||||
<Select.Option value="numberOfFoundListings">Number of Listings</Select.Option>
|
||||
<Select.Option value="enabled">Status</Select.Option>
|
||||
</Select>
|
||||
<Input
|
||||
className="jobGrid__topbar__search"
|
||||
prefix={<IconSearch />}
|
||||
showClear
|
||||
placeholder="Search"
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
style={{ width: 120 }}
|
||||
value={sortDir}
|
||||
onChange={(val) => setSortDir(val)}
|
||||
>
|
||||
<Select.Option value="asc">Ascending</Select.Option>
|
||||
<Select.Option value="desc">Descending</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<RadioGroup
|
||||
type="button"
|
||||
buttonSize="middle"
|
||||
value={activityFilter === null ? 'all' : String(activityFilter)}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setActivityFilter(v === 'all' ? null : v === 'true');
|
||||
}}
|
||||
>
|
||||
<Radio value="all">All</Radio>
|
||||
<Radio value="true">Active</Radio>
|
||||
<Radio value="false">Inactive</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<Select prefix="Sort by" style={{ width: 200 }} value={sortField} onChange={(val) => setSortField(val)}>
|
||||
<Select.Option value="name">Name</Select.Option>
|
||||
<Select.Option value="numberOfFoundListings">Number of Listings</Select.Option>
|
||||
<Select.Option value="enabled">Status</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
icon={sortDir === 'asc' ? <IconArrowUp /> : <IconArrowDown />}
|
||||
onClick={() => setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))}
|
||||
title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(jobsData?.result || []).length === 0 && (
|
||||
<Empty
|
||||
@@ -278,78 +251,70 @@ const JobGrid = () => {
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{(jobsData?.result || []).map((job) => (
|
||||
<Col key={job.id} xs={24} sm={12} md={8} lg={6} xl={4} xxl={6}>
|
||||
<Card
|
||||
className="jobGrid__card"
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
title={
|
||||
<div className="jobGrid__header">
|
||||
<Col key={job.id} xs={24} sm={12} md={12} lg={8} xl={8} xxl={6}>
|
||||
<Card className="jobGrid__card" bodyStyle={{ padding: '16px' }}>
|
||||
<div className="jobGrid__card__header">
|
||||
<div className="jobGrid__card__name">
|
||||
<span className={`jobGrid__card__dot${job.enabled ? ' jobGrid__card__dot--active' : ''}`} />
|
||||
<Title heading={5} ellipsis={{ showTooltip: true }} className="jobGrid__title">
|
||||
{job.name}
|
||||
</Title>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{job.isOnlyShared && (
|
||||
<Popover
|
||||
content={getPopoverContent(
|
||||
'This job has been shared with you by another user, therefor it is read-only.',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<IconAlertTriangle style={{ color: 'rgba(var(--semi-yellow-7), 1)', marginLeft: '8px' }} />
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
|
||||
{job.isOnlyShared && (
|
||||
<Popover
|
||||
content={getPopoverContent(
|
||||
'This job has been shared with you by another user, therefor it is read-only.',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<IconAlertTriangle style={{ color: 'rgba(var(--semi-yellow-7), 1)' }} />
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
{job.running && (
|
||||
<Tag color="green" variant="light" size="small">
|
||||
RUNNING
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="jobGrid__content">
|
||||
<Space vertical align="start" spacing={4} style={{ width: '100%', marginTop: 12 }}>
|
||||
<div className="jobGrid__infoItem">
|
||||
<Text type="secondary" icon={<IconSearch />} size="small">
|
||||
Is active:
|
||||
</Text>
|
||||
<Switch
|
||||
onChange={(checked) => onJobStatusChanged(job.id, checked)}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
checked={job.enabled}
|
||||
disabled={job.isOnlyShared}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
<div className="jobGrid__infoItem">
|
||||
<Text type="secondary" icon={<IconSearch />} size="small">
|
||||
Listings:
|
||||
</Text>
|
||||
<Tag color="blue" size="small" style={{ marginLeft: 'auto' }}>
|
||||
{job.numberOfFoundListings || 0}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="jobGrid__infoItem">
|
||||
<Text type="secondary" icon={<IconBriefcase />} size="small">
|
||||
Providers:
|
||||
</Text>
|
||||
<Tag color="cyan" size="small" style={{ marginLeft: 'auto' }}>
|
||||
{job.provider.length || 0}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="jobGrid__infoItem">
|
||||
<Text type="secondary" icon={<IconBell />} size="small">
|
||||
Adapters:
|
||||
</Text>
|
||||
<Tag color="purple" size="small" style={{ marginLeft: 'auto' }}>
|
||||
{job.notificationAdapter.length || 0}
|
||||
</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
<div className="jobGrid__card__stats">
|
||||
<div className="jobGrid__card__stat jobGrid__card__stat--blue">
|
||||
<span className="jobGrid__card__stat__number">{job.numberOfFoundListings || 0}</span>
|
||||
<span className="jobGrid__card__stat__label">
|
||||
<IconHome size="small" /> Listings
|
||||
</span>
|
||||
</div>
|
||||
<div className="jobGrid__card__stat jobGrid__card__stat--orange">
|
||||
<span className="jobGrid__card__stat__number">{job.provider.length || 0}</span>
|
||||
<span className="jobGrid__card__stat__label">
|
||||
<IconBriefcase size="small" /> Providers
|
||||
</span>
|
||||
</div>
|
||||
<div className="jobGrid__card__stat jobGrid__card__stat--purple">
|
||||
<span className="jobGrid__card__stat__number">{job.notificationAdapter.length || 0}</span>
|
||||
<span className="jobGrid__card__stat__label">
|
||||
<IconBell size="small" /> Adapters
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
|
||||
<div className="jobGrid__card__footer">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Switch
|
||||
onChange={(checked) => onJobStatusChanged(job.id, checked)}
|
||||
checked={job.enabled}
|
||||
disabled={job.isOnlyShared}
|
||||
size="small"
|
||||
/>
|
||||
<Text type="secondary" size="small">
|
||||
Active
|
||||
</Text>
|
||||
</div>
|
||||
<div className="jobGrid__actions">
|
||||
<Popover content={getPopoverContent('Run Job')}>
|
||||
<div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../cards/DashboardCardColors.less';
|
||||
|
||||
.jobGrid {
|
||||
&__card {
|
||||
height: 100%;
|
||||
@@ -12,55 +14,137 @@
|
||||
box-shadow: 0 0 15px -3px rgb(78 78 78 / 70%);
|
||||
background-color: rgba(36, 36, 36, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&__searchbar {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
&__card {
|
||||
border-radius: var(--semi-border-radius-medium);
|
||||
&__name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--semi-color-text-3);
|
||||
|
||||
&--active {
|
||||
background-color: #21aa21;
|
||||
}
|
||||
}
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .3rem;
|
||||
background: rgba(36, 36, 36, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--semi-border-radius-small);
|
||||
padding: 10px 4px 8px;
|
||||
|
||||
&__number {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--semi-color-text-0);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
color: var(--semi-color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&--blue {
|
||||
background: @color-blue-bg;
|
||||
border-color: @color-blue-border;
|
||||
.jobGrid__card__stat__number { color: @color-blue-text; }
|
||||
.jobGrid__card__stat__label { color: @color-blue-text; opacity: 0.7; }
|
||||
}
|
||||
|
||||
&--orange {
|
||||
background: @color-orange-bg;
|
||||
border-color: @color-orange-border;
|
||||
.jobGrid__card__stat__number { color: @color-orange-text; }
|
||||
.jobGrid__card__stat__label { color: @color-orange-text; opacity: 0.7; }
|
||||
}
|
||||
|
||||
&--purple {
|
||||
background: @color-purple-bg;
|
||||
border-color: @color-purple-border;
|
||||
.jobGrid__card__stat__number { color: @color-purple-text; }
|
||||
.jobGrid__card__stat__label { color: @color-purple-text; opacity: 0.7; }
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
&__topbar {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.jobGrid__topbar__search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.semi-button:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jobGrid__topbar__search {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.semi-radio-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.semi-select {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&__infoItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.semi-typography {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
|
||||
@@ -10,15 +10,15 @@ import {
|
||||
Row,
|
||||
Image,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Pagination,
|
||||
Toast,
|
||||
Divider,
|
||||
Input,
|
||||
Select,
|
||||
Popover,
|
||||
Empty,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
} from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
IconBriefcase,
|
||||
@@ -30,9 +30,10 @@ import {
|
||||
IconStar,
|
||||
IconStarStroked,
|
||||
IconSearch,
|
||||
IconFilter,
|
||||
IconActivity,
|
||||
IconEyeOpened,
|
||||
IconArrowUp,
|
||||
IconArrowDown,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||
@@ -64,8 +65,6 @@ const ListingsGrid = () => {
|
||||
const [jobNameFilter, setJobNameFilter] = useState(null);
|
||||
const [activityFilter, setActivityFilter] = useState(null);
|
||||
const [providerFilter, setProviderFilter] = useState(null);
|
||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [listingToDelete, setListingToDelete] = useState(null);
|
||||
|
||||
@@ -129,107 +128,84 @@ const ListingsGrid = () => {
|
||||
|
||||
return (
|
||||
<div className="listingsGrid">
|
||||
<div className="listingsGrid__searchbar">
|
||||
<Input prefix={<IconSearch />} showClear placeholder="Search" onChange={handleFilterChange} />
|
||||
<Popover content="Filter / Sort Results" style={{ color: 'white', padding: '.5rem' }}>
|
||||
<div>
|
||||
<Button
|
||||
icon={<IconFilter />}
|
||||
onClick={() => {
|
||||
setShowFilterBar(!showFilterBar);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
<div className="listingsGrid__topbar">
|
||||
<Input
|
||||
className="listingsGrid__topbar__search"
|
||||
prefix={<IconSearch />}
|
||||
showClear
|
||||
placeholder="Search"
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
type="button"
|
||||
buttonSize="middle"
|
||||
value={activityFilter === null ? 'all' : String(activityFilter)}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setActivityFilter(v === 'all' ? null : v === 'true');
|
||||
}}
|
||||
>
|
||||
<Radio value="all">All</Radio>
|
||||
<Radio value="true">Active</Radio>
|
||||
<Radio value="false">Inactive</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
type="button"
|
||||
buttonSize="middle"
|
||||
value={watchListFilter === null ? 'all' : String(watchListFilter)}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setWatchListFilter(v === 'all' ? null : v === 'true');
|
||||
}}
|
||||
>
|
||||
<Radio value="all">All</Radio>
|
||||
<Radio value="true">Watched</Radio>
|
||||
<Radio value="false">Unwatched</Radio>
|
||||
</RadioGroup>
|
||||
|
||||
<Select
|
||||
placeholder="Provider"
|
||||
showClear
|
||||
onChange={(val) => setProviderFilter(val)}
|
||||
value={providerFilter}
|
||||
style={{ width: 130 }}
|
||||
>
|
||||
{providers?.map((p) => (
|
||||
<Select.Option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="Job"
|
||||
showClear
|
||||
onChange={(val) => setJobNameFilter(val)}
|
||||
value={jobNameFilter}
|
||||
style={{ width: 130 }}
|
||||
>
|
||||
{jobs?.map((j) => (
|
||||
<Select.Option key={j.id} value={j.id}>
|
||||
{j.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select prefix="Sort by" style={{ width: 185 }} value={sortField} onChange={(val) => setSortField(val)}>
|
||||
<Select.Option value="job_name">Job Name</Select.Option>
|
||||
<Select.Option value="created_at">Listing Date</Select.Option>
|
||||
<Select.Option value="price">Price</Select.Option>
|
||||
<Select.Option value="provider">Provider</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
icon={sortDir === 'asc' ? <IconArrowUp /> : <IconArrowDown />}
|
||||
onClick={() => setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))}
|
||||
title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
|
||||
/>
|
||||
</div>
|
||||
{showFilterBar && (
|
||||
<div className="listingsGrid__toolbar">
|
||||
<Space wrap style={{ marginBottom: '1rem' }}>
|
||||
<div className="listingsGrid__toolbar__card">
|
||||
<div>
|
||||
<Text strong>Filter by:</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '.3rem' }}>
|
||||
<Select
|
||||
placeholder="Status"
|
||||
showClear
|
||||
onChange={(val) => setActivityFilter(val)}
|
||||
value={activityFilter}
|
||||
>
|
||||
<Select.Option value={true}>Active</Select.Option>
|
||||
<Select.Option value={false}>Not Active</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="Watchlist"
|
||||
showClear
|
||||
onChange={(val) => setWatchListFilter(val)}
|
||||
value={watchListFilter}
|
||||
>
|
||||
<Select.Option value={true}>Watched</Select.Option>
|
||||
<Select.Option value={false}>Not Watched</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="Provider"
|
||||
showClear
|
||||
onChange={(val) => setProviderFilter(val)}
|
||||
value={providerFilter}
|
||||
>
|
||||
{providers?.map((p) => (
|
||||
<Select.Option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="Job Name"
|
||||
showClear
|
||||
onChange={(val) => setJobNameFilter(val)}
|
||||
value={jobNameFilter}
|
||||
>
|
||||
{jobs?.map((j) => (
|
||||
<Select.Option key={j.id} value={j.id}>
|
||||
{j.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Divider layout="vertical" />
|
||||
|
||||
<div className="listingsGrid__toolbar__card">
|
||||
<div>
|
||||
<Text strong>Sort by:</Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '.3rem' }}>
|
||||
<Select
|
||||
placeholder="Sort By"
|
||||
style={{ width: 140 }}
|
||||
value={sortField}
|
||||
onChange={(val) => setSortField(val)}
|
||||
>
|
||||
<Select.Option value="job_name">Job Name</Select.Option>
|
||||
<Select.Option value="created_at">Listing Date</Select.Option>
|
||||
<Select.Option value="price">Price</Select.Option>
|
||||
<Select.Option value="provider">Provider</Select.Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
style={{ width: 120 }}
|
||||
value={sortDir}
|
||||
onChange={(val) => setSortDir(val)}
|
||||
>
|
||||
<Select.Option value="asc">Ascending</Select.Option>
|
||||
<Select.Option value="desc">Descending</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(listingsData?.result || []).length === 0 && (
|
||||
<Empty
|
||||
@@ -240,7 +216,7 @@ const ListingsGrid = () => {
|
||||
)}
|
||||
<Row gutter={[16, 16]}>
|
||||
{(listingsData?.result || []).map((item) => (
|
||||
<Col key={item.id} xs={24} sm={12} md={8} lg={6} xl={4} xxl={6}>
|
||||
<Col key={item.id} xs={24} sm={12} md={12} lg={8} xl={8} xxl={6}>
|
||||
<Card
|
||||
className={`listingsGrid__card ${!item.is_active ? 'listingsGrid__card--inactive' : ''}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
@@ -280,10 +256,11 @@ const ListingsGrid = () => {
|
||||
<Text strong ellipsis={{ showTooltip: true }} className="listingsGrid__title">
|
||||
{cap(item.title)}
|
||||
</Text>
|
||||
<Space vertical align="start" spacing={2} style={{ width: '100%', marginTop: 8 }}>
|
||||
<Text type="secondary" icon={<IconCart />} size="small">
|
||||
{item.price} €
|
||||
</Text>
|
||||
<div className="listingsGrid__price">
|
||||
<IconCart size="small" />
|
||||
{item.price} €
|
||||
</div>
|
||||
<div className="listingsGrid__meta">
|
||||
<Text
|
||||
type="secondary"
|
||||
icon={<IconMapPin />}
|
||||
@@ -305,18 +282,17 @@ const ListingsGrid = () => {
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="tertiary" size="small" icon={<IconActivity />}>
|
||||
Distance cannot be calculated, provide an address
|
||||
Distance cannot be calculated
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Divider margin=".6rem" />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div className="listingsGrid__actions">
|
||||
<div className="listingsGrid__linkButton" onClick={(e) => e.stopPropagation()}>
|
||||
<a href={item.link} target="_blank" rel="noopener noreferrer">
|
||||
<IconLink />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="secondary"
|
||||
size="small"
|
||||
@@ -324,7 +300,6 @@ const ListingsGrid = () => {
|
||||
onClick={() => navigate(`/listings/listing/${item.id}`)}
|
||||
icon={<IconEyeOpened />}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Remove"
|
||||
type="danger"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../cards/DashboardCardColors.less';
|
||||
|
||||
.listingsGrid {
|
||||
&__imageContainer {
|
||||
position: relative;
|
||||
@@ -5,12 +7,34 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__searchbar {
|
||||
&__topbar {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.listingsGrid__topbar__search {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.listingsGrid__topbar__search {
|
||||
width: 100%;
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.semi-radio-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.semi-select {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__watchButton {
|
||||
@@ -93,17 +117,27 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__toolbar {
|
||||
&__card {
|
||||
border-radius: var(--semi-border-radius-medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .3rem;
|
||||
background: rgba(36, 36, 36, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
}
|
||||
&__price {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: @color-green-text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin: 8px 0 6px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__setupButton {
|
||||
|
||||
Reference in New Issue
Block a user