adding ability to tag listings eg if you have applied to it / adding ability to add notes to a listing

This commit is contained in:
orangecoding
2026-06-02 12:48:01 +02:00
parent 2bcec04d55
commit 317ef79336
25 changed files with 741 additions and 49 deletions

View File

@@ -20,6 +20,7 @@ import {
Banner,
Spin,
Toast,
TextArea,
} from '@douyinfe/semi-ui-19';
import {
IconArrowLeft,
@@ -44,6 +45,7 @@ import { xhrPost, xhrDelete } from '../../services/xhr.js';
import ListingDeletionModal from '../../components/ListingDeletionModal.jsx';
import Headline from '../../components/headline/Headline.jsx';
import StatusControl from '../../components/listings/StatusControl.jsx';
import './ListingDetail.less';
const { Title, Text } = Typography;
@@ -65,6 +67,8 @@ export default function ListingDetail() {
const map = useRef(null);
const [loading, setLoading] = useState(true);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [notesDraft, setNotesDraft] = useState('');
const [notesSaving, setNotesSaving] = useState(false);
useEffect(() => {
async function fetchListing() {
@@ -82,6 +86,10 @@ export default function ListingDetail() {
fetchListing();
}, [listingId]);
useEffect(() => {
setNotesDraft(listing?.notes ?? '');
}, [listing?.id, listing?.notes]);
const hasGeo =
listing?.latitude != null && listing?.longitude != null && listing?.latitude !== -1 && listing?.longitude !== -1;
@@ -271,6 +279,32 @@ export default function ListingDetail() {
}
};
const handleStatusChange = async (next) => {
try {
await actions.listingsData.setListingStatus(listing.id, next);
await actions.listingsData.getListing(listingId);
Toast.success(next ? `Marked as ${next}` : 'Status cleared');
} catch (e) {
console.error('Failed to update status:', e);
Toast.error('Failed to update status');
}
};
const handleSaveNotes = async () => {
if (!listing) return;
setNotesSaving(true);
try {
await actions.listingsData.setListingNotes(listing.id, notesDraft);
await actions.listingsData.getListing(listingId);
Toast.success('Notes saved');
} catch (e) {
console.error('Failed to save notes:', e);
Toast.error('Failed to save notes');
} finally {
setNotesSaving(false);
}
};
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
@@ -347,6 +381,7 @@ export default function ListingDetail() {
>
{listing.isWatched === 1 ? 'Watched' : 'Watch'}
</Button>
<StatusControl status={listing.status ?? null} onChange={handleStatusChange} />
<a href={listing.link} target="_blank" rel="noopener noreferrer" className="listing-detail__open-btn">
<IconLink style={{ marginRight: 6 }} />
Open listing
@@ -380,6 +415,32 @@ export default function ListingDetail() {
preview={!!listing.image_url}
/>
</div>
<div className="listing-detail__notes">
<Title heading={4} className="listing-detail__notes-title">
Notes
</Title>
<TextArea
value={notesDraft}
onChange={(val) => setNotesDraft(val)}
placeholder="Your private notes about this listing…"
rows={5}
autosize={{ minRows: 4, maxRows: 12 }}
className="listing-detail__notes-textarea"
showClear
/>
<Space className="listing-detail__notes-actions">
<Button
theme="solid"
type="primary"
loading={notesSaving}
disabled={notesSaving || (notesDraft ?? '') === (listing.notes ?? '')}
onClick={handleSaveNotes}
>
Store notes
</Button>
</Space>
</div>
</Col>
<Col span={24} lg={12}>
<div className="listing-detail__info-section">

View File

@@ -89,6 +89,49 @@
}
}
&__notes {
padding: 1.5rem;
border-top: 1px solid var(--semi-color-border);
}
&__notes-title {
margin-bottom: 1rem !important;
}
&__notes-actions {
margin-top: 0.75rem;
width: 100%;
justify-content: flex-end;
}
&__notes-textarea {
background: #2a2a2a !important;
border: 1px solid @color-border-bright !important;
border-radius: @radius-input !important;
box-shadow: none !important;
outline: none !important;
transition: border-color @transition-fast, background @transition-fast !important;
textarea {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
outline: none !important;
color: @color-text !important;
font-family: @font-ui !important;
font-size: @text-base !important;
}
&.semi-input-textarea-focus,
&:focus,
&:focus-within {
border-color: @color-accent !important;
box-shadow: none !important;
outline: none !important;
background: #2f2f2f !important;
}
}
&__watch-btn {
color: @color-muted !important;
border: 1px solid @color-border-bright !important;

View File

@@ -6,11 +6,15 @@
import ListingsOverview from '../../components/listings/ListingsOverview.jsx';
import Headline from '../../components/headline/Headline.jsx';
export default function Listings() {
/**
* @param {{ mode?: 'all' | 'watchlist' }} props
*/
export default function Listings({ mode = 'all' }) {
const title = mode === 'watchlist' ? 'Watchlist' : 'Listings';
return (
<>
<Headline text="Listings" />
<ListingsOverview />
<Headline text={title} />
<ListingsOverview mode={mode} />
</>
);
}