mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
feat: Fredy UI redesign
* New design :)
This commit is contained in:
committed by
GitHub
parent
c78472bd19
commit
f30ec4645c
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Col, Row, Toast, Typography } from '@douyinfe/semi-ui-19';
|
||||
import { Button, Col, Row, Toast } from '@douyinfe/semi-ui-19';
|
||||
import {
|
||||
IconTerminal,
|
||||
IconStar,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { useSelector, useActions } from '../../services/state/store';
|
||||
import KpiCard from '../../components/cards/KpiCard.jsx';
|
||||
import PieChartCard from '../../components/cards/PieChartCard.jsx';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
|
||||
import './Dashboard.less';
|
||||
import { xhrPost } from '../../services/xhr.js';
|
||||
@@ -34,11 +35,12 @@ export default function Dashboard() {
|
||||
|
||||
const kpis = dashboard?.kpis || { totalJobs: 0, totalListings: 0, providersUsed: 0 };
|
||||
const pieData = dashboard?.pie || [];
|
||||
const { Text } = Typography;
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
<Text className="dashboard__section-label">General</Text>
|
||||
<Headline text="Dashboard" />
|
||||
|
||||
<div className="dashboard__section-label">General</div>
|
||||
<Row gutter={[16, 16]} className="dashboard__row">
|
||||
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
|
||||
<KpiCard
|
||||
@@ -51,7 +53,6 @@ export default function Dashboard() {
|
||||
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
|
||||
<KpiCard
|
||||
title="Last Search"
|
||||
valueFontSize="14px"
|
||||
value={
|
||||
dashboard?.general?.lastRun == null || dashboard?.general?.lastRun === 0
|
||||
? '---'
|
||||
@@ -69,7 +70,6 @@ export default function Dashboard() {
|
||||
? '---'
|
||||
: format(dashboard?.general?.nextRun)
|
||||
}
|
||||
valueFontSize="14px"
|
||||
icon={<IconDoubleChevronRight />}
|
||||
description="Next execution timestamp"
|
||||
/>
|
||||
@@ -96,7 +96,7 @@ export default function Dashboard() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Text className="dashboard__section-label">Overview</Text>
|
||||
<div className="dashboard__section-label">Overview</div>
|
||||
<Row gutter={[16, 16]} className="dashboard__row">
|
||||
<Col xs={24} sm={12} md={12} lg={6} xl={6}>
|
||||
<KpiCard
|
||||
@@ -132,10 +132,9 @@ export default function Dashboard() {
|
||||
value={`${
|
||||
!kpis.medianPriceOfListings
|
||||
? '---'
|
||||
: new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(kpis.medianPriceOfListings)
|
||||
: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(
|
||||
kpis.medianPriceOfListings,
|
||||
)
|
||||
}`}
|
||||
icon={<IconNoteMoney />}
|
||||
description="Median Price of listings"
|
||||
@@ -143,7 +142,7 @@ export default function Dashboard() {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Text className="dashboard__section-label">Provider Insights</Text>
|
||||
<div className="dashboard__section-label">Provider Insights</div>
|
||||
<div className="dashboard__pie-wrapper">
|
||||
<PieChartCard data={pieData} />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../tokens.less';
|
||||
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -5,13 +7,13 @@
|
||||
|
||||
&__section-label {
|
||||
display: block;
|
||||
font-size: 11px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: @text-xs;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #5a6478 !important;
|
||||
color: @color-faint;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 4px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&__row {
|
||||
@@ -22,9 +24,8 @@
|
||||
&__pie-wrapper {
|
||||
background: #23242a;
|
||||
border: 1px solid #37404e;
|
||||
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
border-radius: @radius-card;
|
||||
padding: 28px;
|
||||
max-height: 320px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '../../services/backupRestoreClient';
|
||||
import { IconSave, IconRefresh, IconSignal, IconHome, IconFolder } from '@douyinfe/semi-icons';
|
||||
import { debounce } from '../../utils';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
import './GeneralSettings.less';
|
||||
|
||||
function formatFromTimestamp(ts) {
|
||||
@@ -244,6 +245,7 @@ const GeneralSettings = function GeneralSettings() {
|
||||
|
||||
return (
|
||||
<div className="generalSettings">
|
||||
<Headline text="Settings" />
|
||||
{!loading && (
|
||||
<>
|
||||
<Tabs type="line">
|
||||
|
||||
@@ -1,17 +1,73 @@
|
||||
@import '../../tokens.less';
|
||||
|
||||
.generalSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
&__tab-content {
|
||||
padding: 20px 0;
|
||||
max-width: 860px;
|
||||
padding: @space-4 0;
|
||||
}
|
||||
|
||||
&__timePickerContainer {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
gap: @space-3;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__save-row {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: @space-2;
|
||||
}
|
||||
}
|
||||
|
||||
// InputNumber fix
|
||||
.semi-input-number {
|
||||
background: @color-elevated !important;
|
||||
border: 1px solid @color-border-bright !important;
|
||||
border-radius: @radius-input !important;
|
||||
color: @color-text !important;
|
||||
}
|
||||
|
||||
.semi-input-number-button-up,
|
||||
.semi-input-number-button-down {
|
||||
background: rgba(255,255,255,0.06) !important;
|
||||
border-color: @color-border-bright !important;
|
||||
color: @color-muted !important;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255,255,255,0.12) !important;
|
||||
color: @color-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
// TimePicker fix — scoped so it doesn't pollute modal headers
|
||||
.semi-timepicker .semi-input-wrapper,
|
||||
.semi-timepicker .semi-input-inset-label-wrapper {
|
||||
background: @color-elevated !important;
|
||||
border: 1px solid @color-border-bright !important;
|
||||
border-radius: @radius-input !important;
|
||||
color: @color-text !important;
|
||||
}
|
||||
|
||||
// Tabs styling
|
||||
.semi-tabs-bar-line .semi-tabs-tab {
|
||||
color: @color-faint;
|
||||
font-size: @text-base;
|
||||
transition: color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: @color-muted;
|
||||
}
|
||||
|
||||
&.semi-tabs-tab-active {
|
||||
color: @color-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-tabs-bar-line .semi-tabs-ink-bar {
|
||||
background-color: @color-accent !important;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@douyinfe/semi-ui-19';
|
||||
import { IconPlusCircle } from '@douyinfe/semi-icons';
|
||||
import JobGrid from '../../components/grid/jobs/JobGrid.jsx';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
import './Jobs.less';
|
||||
|
||||
export default function Jobs() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="jobs">
|
||||
<Headline
|
||||
text="Jobs"
|
||||
actions={
|
||||
<Button type="primary" theme="solid" icon={<IconPlusCircle />} onClick={() => navigate('/jobs/new')}>
|
||||
New Job
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<JobGrid />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@import '../../tokens.less';
|
||||
|
||||
.jobs {
|
||||
&__newButton {
|
||||
margin-top: 1rem !important;
|
||||
float: left;
|
||||
margin-bottom: 1rem !important;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Divider, Input, Switch, Button, TagInput, Toast, Select } from '@douyin
|
||||
import './JobMutation.less';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconBell,
|
||||
IconBriefcase,
|
||||
IconPaperclip,
|
||||
@@ -144,7 +145,19 @@ export default function JobMutator() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Headline text={jobToBeEdit ? 'Edit Job' : 'Create new Job'} />
|
||||
<Headline
|
||||
text={jobToBeEdit ? 'Edit Job' : 'Create new Job'}
|
||||
actions={
|
||||
<Button
|
||||
icon={<IconArrowLeft />}
|
||||
onClick={() => navigate('/jobs')}
|
||||
theme="borderless"
|
||||
style={{ color: '#909090' }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<form>
|
||||
<SegmentPart name="Name" Icon={IconPaperclip}>
|
||||
<Input
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
@import '../../../tokens.less';
|
||||
|
||||
.jobMutation {
|
||||
&__newButton {
|
||||
float: right;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: @space-4;
|
||||
}
|
||||
|
||||
&__specFilter {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
gap: @space-4;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__specFilterItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: @space-2;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
&__specFilterLabel {
|
||||
font-weight: 500;
|
||||
font-size: @text-sm;
|
||||
color: @color-muted;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: @space-3;
|
||||
margin-top: @space-4;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import * as timeService from '../../services/time/timeService.js';
|
||||
import { distanceMeters, getBoundsFromCoords } from './mapUtils.js';
|
||||
import { xhrPost } from '../../services/xhr.js';
|
||||
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
import './ListingDetail.less';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -278,7 +279,7 @@ export default function ListingDetail() {
|
||||
},
|
||||
{
|
||||
key: 'Provider',
|
||||
value: listing.provider.charAt(0).toUpperCase() + listing.provider.slice(1),
|
||||
value: listing.provider ? listing.provider.charAt(0).toUpperCase() + listing.provider.slice(1) : 'Unknown',
|
||||
Icon: <IconBriefcase />,
|
||||
},
|
||||
{
|
||||
@@ -290,40 +291,45 @@ export default function ListingDetail() {
|
||||
|
||||
return (
|
||||
<div className="listing-detail">
|
||||
<div className="listing-detail__back">
|
||||
<Button icon={<IconArrowLeft />} onClick={() => navigate(-1)} theme="borderless">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<Headline
|
||||
text={listing?.title || 'Listing Detail'}
|
||||
actions={
|
||||
<Button icon={<IconArrowLeft />} onClick={() => navigate(-1)} theme="borderless" style={{ color: '#909090' }}>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card className="listing-detail__card">
|
||||
<div className="listing-detail__header">
|
||||
<Space vertical align="start" spacing="tight">
|
||||
<Title heading={2} className="listing-detail__title">
|
||||
{listing.title}
|
||||
</Title>
|
||||
<Space align="center">
|
||||
<IconMapPin style={{ fontSize: '18px', color: 'var(--semi-color-primary)' }} />
|
||||
<Text type="secondary">{listing.address || 'No address provided'}</Text>
|
||||
</Space>
|
||||
<Space align="center">
|
||||
<IconMapPin style={{ fontSize: '18px', color: 'var(--semi-color-primary)' }} />
|
||||
{listing.address ? (
|
||||
<a
|
||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(listing.address)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="listing-detail__address-link"
|
||||
>
|
||||
{listing.address}
|
||||
</a>
|
||||
) : (
|
||||
<Text type="secondary">No address provided</Text>
|
||||
)}
|
||||
</Space>
|
||||
<Space wrap className="listing-detail__header-actions">
|
||||
<Button
|
||||
icon={
|
||||
listing.isWatched === 1 ? (
|
||||
<IconStar style={{ color: 'var(--semi-color-warning)' }} />
|
||||
) : (
|
||||
<IconStarStroked />
|
||||
)
|
||||
}
|
||||
icon={listing.isWatched === 1 ? <IconStar /> : <IconStarStroked />}
|
||||
onClick={handleWatch}
|
||||
theme="light"
|
||||
theme="borderless"
|
||||
className={`listing-detail__watch-btn${listing.isWatched === 1 ? ' listing-detail__watch-btn--active' : ''}`}
|
||||
>
|
||||
{listing.isWatched === 1 ? 'Watched' : 'Watch'}
|
||||
</Button>
|
||||
<Text link={{ href: listing.link, target: '_blank' }} icon={<IconLink />} underline>
|
||||
<a href={listing.link} target="_blank" rel="noopener noreferrer" className="listing-detail__open-btn">
|
||||
<IconLink style={{ marginRight: 6 }} />
|
||||
Open listing
|
||||
</Text>
|
||||
</a>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
@import '../../tokens.less';
|
||||
|
||||
.listing-detail {
|
||||
padding-bottom: 2rem;
|
||||
|
||||
&__back {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
&__card {
|
||||
background-color: rgba(36, 36, 36, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
@@ -45,14 +43,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 !important;
|
||||
word-break: break-word;
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__image-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
@@ -69,7 +59,61 @@
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.semi-image,
|
||||
.semi-image-img {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__address-link {
|
||||
color: @color-muted;
|
||||
text-decoration: none;
|
||||
font-size: @text-base;
|
||||
transition: color @transition-fast;
|
||||
&:hover {
|
||||
color: @color-text;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__watch-btn {
|
||||
color: @color-muted !important;
|
||||
border: 1px solid @color-border-bright !important;
|
||||
border-radius: @radius-btn !important;
|
||||
&:hover {
|
||||
color: @color-text !important;
|
||||
background: rgba(255,255,255,0.06) !important;
|
||||
}
|
||||
&--active {
|
||||
color: @color-accent !important;
|
||||
border-color: rgba(224,74,56,0.4) !important;
|
||||
background: rgba(224,74,56,0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__open-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid @color-border-bright;
|
||||
border-radius: @radius-btn;
|
||||
color: @color-muted;
|
||||
font-size: @text-base;
|
||||
font-family: @font-ui;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color @transition-fast, border-color @transition-fast, background @transition-fast;
|
||||
&:hover {
|
||||
color: @color-text;
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
*/
|
||||
|
||||
import ListingsGrid from '../../components/grid/listings/ListingsGrid.jsx';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
|
||||
export default function Listings() {
|
||||
return <ListingsGrid />;
|
||||
return (
|
||||
<>
|
||||
<Headline text="Listings" />
|
||||
<ListingsGrid />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { xhrDelete } from '../../services/xhr.js';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
|
||||
import Map from '../../components/map/Map.jsx';
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
|
||||
const RangeSlider = _RangeSlider?.default ?? _RangeSlider;
|
||||
|
||||
@@ -354,127 +355,130 @@ export default function MapView() {
|
||||
}, [listings, priceRange, homeAddress, distanceFilter]);
|
||||
|
||||
return (
|
||||
<div className="map-view-container">
|
||||
{!homeAddress && (
|
||||
<>
|
||||
<Headline text="Map View" />
|
||||
<div className="map-view-container">
|
||||
{!homeAddress && (
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="warning"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
style={{ marginBottom: '8px' }}
|
||||
description={
|
||||
<span>
|
||||
No home address set. Configure it in <Link to="/userSettings">user settings</Link> to use the distance
|
||||
filter.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="warning"
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
style={{ marginBottom: '8px' }}
|
||||
description={
|
||||
<span>
|
||||
No home address set. Configure it in <Link to="/userSettings">user settings</Link> to use the distance
|
||||
filter.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Banner
|
||||
fullMode={true}
|
||||
type="info"
|
||||
bordered
|
||||
closeIcon={null}
|
||||
style={{ marginBottom: '8px' }}
|
||||
description="Only listings with valid addresses are shown on this map."
|
||||
/>
|
||||
|
||||
<div className="map-view-container__map-wrapper">
|
||||
<Map
|
||||
mapContainerRef={mapContainer}
|
||||
style={style}
|
||||
show3dBuildings={show3dBuildings}
|
||||
onMapReady={handleMapReady}
|
||||
description="Only listings with valid addresses are shown on this map."
|
||||
/>
|
||||
|
||||
{/* Floating filter panel */}
|
||||
<div className="map-view-container__floating-panel">
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Job
|
||||
</Text>
|
||||
<Select
|
||||
placeholder="All jobs"
|
||||
showClear
|
||||
size="small"
|
||||
onChange={(val) => setJobId(val)}
|
||||
value={jobId}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
{jobs?.map((j) => (
|
||||
<Select.Option key={j.id} value={j.id}>
|
||||
{j.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="map-view-container__map-wrapper">
|
||||
<Map
|
||||
mapContainerRef={mapContainer}
|
||||
style={style}
|
||||
show3dBuildings={show3dBuildings}
|
||||
onMapReady={handleMapReady}
|
||||
/>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Distance
|
||||
</Text>
|
||||
<Select
|
||||
placeholder="None"
|
||||
size="small"
|
||||
onChange={(val) => setDistanceFilter(val)}
|
||||
value={distanceFilter}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
<Select.Option value={0}>None</Select.Option>
|
||||
<Select.Option value={5}>5 km</Select.Option>
|
||||
<Select.Option value={10}>10 km</Select.Option>
|
||||
<Select.Option value={15}>15 km</Select.Option>
|
||||
<Select.Option value={20}>20 km</Select.Option>
|
||||
<Select.Option value={25}>25 km</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Floating filter panel */}
|
||||
<div className="map-view-container__floating-panel">
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Job
|
||||
</Text>
|
||||
<Select
|
||||
placeholder="All jobs"
|
||||
showClear
|
||||
size="small"
|
||||
onChange={(val) => setJobId(val)}
|
||||
value={jobId}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
{jobs?.map((j) => (
|
||||
<Select.Option key={j.id} value={j.id}>
|
||||
{j.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Price (€)
|
||||
</Text>
|
||||
<div className="map-view-container__price-slider">
|
||||
<div className="map__rangesliderLabels">
|
||||
<span>{priceRange[0]}</span>
|
||||
<span>{priceRange[1]}</span>
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Distance
|
||||
</Text>
|
||||
<Select
|
||||
placeholder="None"
|
||||
size="small"
|
||||
onChange={(val) => setDistanceFilter(val)}
|
||||
value={distanceFilter}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
<Select.Option value={0}>None</Select.Option>
|
||||
<Select.Option value={5}>5 km</Select.Option>
|
||||
<Select.Option value={10}>10 km</Select.Option>
|
||||
<Select.Option value={15}>15 km</Select.Option>
|
||||
<Select.Option value={20}>20 km</Select.Option>
|
||||
<Select.Option value={25}>25 km</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Price (€)
|
||||
</Text>
|
||||
<div className="map-view-container__price-slider">
|
||||
<div className="map__rangesliderLabels">
|
||||
<span>{priceRange[0]}</span>
|
||||
<span>{priceRange[1]}</span>
|
||||
</div>
|
||||
<RangeSlider min={0} max={getMaxPrice()} step={100} value={priceRange} onInput={handlePriceRange} />
|
||||
</div>
|
||||
<RangeSlider min={0} max={getMaxPrice()} step={100} value={priceRange} onInput={handlePriceRange} />
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Style
|
||||
</Text>
|
||||
<Select size="small" value={style} onChange={(val) => handleMapStyle(val)} style={{ width: 110 }}>
|
||||
<Select.Option value="STANDARD">Standard</Select.Option>
|
||||
<Select.Option value="SATELLITE">Satellite</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
3D Buildings
|
||||
</Text>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={show3dBuildings}
|
||||
onChange={(v) => setShow3dBuildings(v)}
|
||||
disabled={style === 'SATELLITE'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
Style
|
||||
</Text>
|
||||
<Select size="small" value={style} onChange={(val) => handleMapStyle(val)} style={{ width: 110 }}>
|
||||
<Select.Option value="STANDARD">Standard</Select.Option>
|
||||
<Select.Option value="SATELLITE">Satellite</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="map-view-container__panel-row">
|
||||
<Text size="small" strong style={{ color: '#8892a4' }}>
|
||||
3D Buildings
|
||||
</Text>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={show3dBuildings}
|
||||
onChange={(v) => setShow3dBuildings(v)}
|
||||
disabled={style === 'SATELLITE'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ListingDeletionModal
|
||||
visible={deleteModalVisible}
|
||||
onConfirm={confirmListingDeletion}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ListingDeletionModal
|
||||
visible={deleteModalVisible}
|
||||
onConfirm={confirmListingDeletion}
|
||||
onCancel={() => {
|
||||
setDeleteModalVisible(false);
|
||||
setListingToDelete(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
@import '../../tokens.less';
|
||||
|
||||
.map-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -21,11 +23,11 @@
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
background: rgba(13, 15, 20, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid #262a3a;
|
||||
border-radius: 10px;
|
||||
background: rgba(22, 25, 38, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @radius-card;
|
||||
padding: 14px 16px;
|
||||
min-width: 220px;
|
||||
display: flex;
|
||||
@@ -183,13 +185,19 @@
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 50%;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
background: #0ab5b3;
|
||||
background: @color-accent;
|
||||
}
|
||||
|
||||
.range-slider .range-slider__range {
|
||||
background: #0ab5b3;
|
||||
background: @color-accent;
|
||||
}
|
||||
|
||||
.range-slider {
|
||||
background: rgba(255,255,255,0.12);
|
||||
border-radius: 4px;
|
||||
height: 4px !important;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
import { useState } from 'react';
|
||||
import { IconHorn } from '@douyinfe/semi-icons';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart.jsx';
|
||||
import { Banner, Button, Checkbox, Space } from '@douyinfe/semi-ui-19';
|
||||
import { Banner, Button, Checkbox, Space, Typography } from '@douyinfe/semi-ui-19';
|
||||
import NotificationAdapterMutator from '../../jobs/mutation/components/notificationAdapter/NotificationAdapterMutator.jsx';
|
||||
import Headline from '../../../components/headline/Headline.jsx';
|
||||
|
||||
export default function WatchlistManagement() {
|
||||
const [notificationChooserVisible, setNotificationChooserVisible] = useState(false);
|
||||
@@ -31,7 +30,9 @@ export default function WatchlistManagement() {
|
||||
description="You’ll receive notifications only for listings that are on your watch list. To add listings to it, open the 'Listings' section and tag the ones you want to follow."
|
||||
/>
|
||||
<Space />
|
||||
<Headline size={5} text="Notify me when:" style={{ marginTop: '1rem' }} />
|
||||
<Typography.Title heading={5} style={{ marginTop: '1rem' }}>
|
||||
Notify me when:
|
||||
</Typography.Title>
|
||||
|
||||
<Checkbox checked={activityChanges} onChange={(e) => setActivityChanges(e.target.checked)}>
|
||||
Listing state changes (e.g. listing becomes inactive)
|
||||
@@ -41,7 +42,9 @@ export default function WatchlistManagement() {
|
||||
</Checkbox>
|
||||
|
||||
<Space />
|
||||
<Headline size={5} text="Notify me with:" style={{ marginTop: '1rem' }} />
|
||||
<Typography.Title heading={5} style={{ marginTop: '1rem' }}>
|
||||
Notify me with:
|
||||
</Typography.Title>
|
||||
<Button onClick={() => setNotificationChooserVisible(true)}>Select notification method</Button>
|
||||
|
||||
<NotificationAdapterMutator
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
border-radius: 24px;
|
||||
padding: 3rem;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,16 +4,14 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Toast } from '@douyinfe/semi-ui-19';
|
||||
import { Toast, Button } from '@douyinfe/semi-ui-19';
|
||||
import { IconPlus } from '@douyinfe/semi-icons';
|
||||
import UserTable from '../../components/table/UserTable';
|
||||
import { useActions, useSelector } from '../../services/state/store';
|
||||
import { IconPlus } from '@douyinfe/semi-icons';
|
||||
import { Button } from '@douyinfe/semi-ui-19';
|
||||
import UserRemovalModal from './UserRemovalModal';
|
||||
import { xhrDelete } from '../../services/xhr';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Headline from '../../components/headline/Headline.jsx';
|
||||
import './Users.less';
|
||||
|
||||
const Users = function Users() {
|
||||
@@ -28,14 +26,13 @@ const Users = function Users() {
|
||||
await actions.user.getUsers();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const onUserRemoval = async () => {
|
||||
try {
|
||||
await xhrDelete('/api/admin/users', { userId: userIdToBeRemoved });
|
||||
Toast.success('User successfully remove');
|
||||
Toast.success('User successfully removed');
|
||||
setUserIdToBeRemoved(null);
|
||||
await actions.jobsData.getJobs();
|
||||
await actions.user.getUsers();
|
||||
@@ -46,30 +43,22 @@ const Users = function Users() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="users">
|
||||
<Headline
|
||||
text="Users"
|
||||
actions={
|
||||
<Button type="primary" theme="solid" icon={<IconPlus />} onClick={() => navigate('/users/new')}>
|
||||
New User
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
{userIdToBeRemoved && <UserRemovalModal onCancel={() => setUserIdToBeRemoved(null)} onOk={onUserRemoval} />}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
className="users__newButton"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => navigate('/users/new')}
|
||||
>
|
||||
New User
|
||||
</Button>
|
||||
|
||||
<UserTable
|
||||
user={users}
|
||||
onUserEdit={(userId) => {
|
||||
navigate(`/users/edit/${userId}`);
|
||||
}}
|
||||
onUserRemoval={(userId) => {
|
||||
setUserIdToBeRemoved(userId);
|
||||
//throw warning message that all jobs will be removed associated to this user
|
||||
//check if at least 1 admin is available
|
||||
}}
|
||||
onUserEdit={(userId) => navigate(`/users/edit/${userId}`)}
|
||||
onUserRemoval={(userId) => setUserIdToBeRemoved(userId)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@import '../../tokens.less';
|
||||
|
||||
.users {
|
||||
&__newButton {
|
||||
margin-top: 1rem !important;
|
||||
float: left;
|
||||
margin-bottom: 1rem !important;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { useActions } from '../../../services/state/store';
|
||||
import { Divider, Input, Switch, Button, Toast } from '@douyinfe/semi-ui-19';
|
||||
import './UserMutator.less';
|
||||
import { SegmentPart } from '../../../components/segment/SegmentPart';
|
||||
import { IconPlusCircle } from '@douyinfe/semi-icons';
|
||||
import { IconPlusCircle, IconArrowLeft } from '@douyinfe/semi-icons';
|
||||
import Headline from '../../../components/headline/Headline.jsx';
|
||||
|
||||
const UserMutator = function UserMutator() {
|
||||
const params = useParams();
|
||||
@@ -63,53 +64,70 @@ const UserMutator = function UserMutator() {
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="userMutator">
|
||||
<SegmentPart name="Username" helpText="The username used to login to Fredy">
|
||||
<Input
|
||||
type="text"
|
||||
label="Username"
|
||||
maxLength={30}
|
||||
placeholder="Username"
|
||||
autoFocus
|
||||
width={6}
|
||||
value={username}
|
||||
onChange={(val) => setUsername(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Password" helpText="The password used to login to Fredy">
|
||||
<Input
|
||||
mode="password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
width={6}
|
||||
value={password}
|
||||
onChange={(val) => setPassword(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Retype password" helpText="Retype the password to make sure they match">
|
||||
<Input
|
||||
mode="password"
|
||||
label="Retype password"
|
||||
placeholder="Retype password"
|
||||
width={6}
|
||||
value={password2}
|
||||
onChange={(val) => setPassword2(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Is user an admin?" helpText="Check this if the user is an administrator">
|
||||
<Switch checked={isAdmin} onChange={(checked) => setIsAdmin(checked)} />
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<Button type="danger" style={{ marginRight: '1rem' }} onClick={() => navigate('/users')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" icon={<IconPlusCircle />} onClick={saveUser}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
<>
|
||||
<Headline
|
||||
text={params.userId ? 'Edit User' : 'New User'}
|
||||
actions={
|
||||
<Button
|
||||
icon={<IconArrowLeft />}
|
||||
onClick={() => navigate('/users')}
|
||||
theme="borderless"
|
||||
style={{ color: '#909090' }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<form className="userMutator">
|
||||
<SegmentPart name="Username" helpText="The username used to login to Fredy">
|
||||
<Input
|
||||
type="text"
|
||||
label="Username"
|
||||
maxLength={30}
|
||||
placeholder="Username"
|
||||
autoFocus
|
||||
width={6}
|
||||
value={username}
|
||||
onChange={(val) => setUsername(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Password" helpText="The password used to login to Fredy">
|
||||
<Input
|
||||
mode="password"
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
width={6}
|
||||
value={password}
|
||||
onChange={(val) => setPassword(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Retype password" helpText="Retype the password to make sure they match">
|
||||
<Input
|
||||
mode="password"
|
||||
label="Retype password"
|
||||
placeholder="Retype password"
|
||||
width={6}
|
||||
value={password2}
|
||||
onChange={(val) => setPassword2(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Is user an admin?" helpText="Check this if the user is an administrator">
|
||||
<Switch checked={isAdmin} onChange={(checked) => setIsAdmin(checked)} />
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<div className="userMutator__actions">
|
||||
<Button size="small" theme="borderless" style={{ color: '#909090' }} onClick={() => navigate('/users')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="small" type="primary" theme="solid" icon={<IconPlusCircle />} onClick={saveUser}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@import '../../../tokens.less';
|
||||
|
||||
.userMutator {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: @space-2;
|
||||
margin-top: @space-2;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user