Add proper buy listing support with type-aware UI filters and display
This commit is contained in:
parent
c7ac448f15
commit
6d8f69610f
6 changed files with 416 additions and 87 deletions
|
|
@ -101,6 +101,24 @@ export function FilterPanel({ onSubmit, isLoading, listingCount }: FilterPanelPr
|
|||
},
|
||||
});
|
||||
|
||||
// Watch listing_type to make filters type-aware
|
||||
const watchedListingType = form.watch('listing_type');
|
||||
|
||||
// Update price defaults when listing type changes
|
||||
useEffect(() => {
|
||||
if (watchedListingType === ListingType.BUY) {
|
||||
form.setValue('min_price', 300000);
|
||||
form.setValue('max_price', 600000);
|
||||
} else {
|
||||
form.setValue('min_price', 2000);
|
||||
form.setValue('max_price', 3000);
|
||||
}
|
||||
// Clear furnish types when switching to BUY
|
||||
if (watchedListingType === ListingType.BUY) {
|
||||
setSelectedFurnishTypes([]);
|
||||
}
|
||||
}, [watchedListingType, form]);
|
||||
|
||||
const handleFormSubmit = (action: 'fetch-data' | 'visualize') => {
|
||||
return form.handleSubmit((values) => {
|
||||
const params: ParameterValues = {
|
||||
|
|
@ -400,29 +418,31 @@ export function FilterPanel({ onSubmit, isLoading, listingCount }: FilterPanelPr
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel className="text-xs">Furnishing</FormLabel>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{[
|
||||
{ value: FurnishType.FURNISHED, label: 'Furnished' },
|
||||
{ value: FurnishType.PART_FURNISHED, label: 'Part' },
|
||||
{ value: FurnishType.UNFURNISHED, label: 'Unfurn.' },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => toggleFurnishType(option.value)}
|
||||
className={`px-2 py-1 text-xs rounded-md border transition-colors ${
|
||||
selectedFurnishTypes.includes(option.value)
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-muted border-input'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
{watchedListingType === ListingType.RENT && (
|
||||
<div>
|
||||
<FormLabel className="text-xs">Furnishing</FormLabel>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{[
|
||||
{ value: FurnishType.FURNISHED, label: 'Furnished' },
|
||||
{ value: FurnishType.PART_FURNISHED, label: 'Part' },
|
||||
{ value: FurnishType.UNFURNISHED, label: 'Unfurn.' },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => toggleFurnishType(option.value)}
|
||||
className={`px-2 py-1 text-xs rounded-md border transition-colors ${
|
||||
selectedFurnishTypes.includes(option.value)
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-muted border-input'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
|
@ -456,33 +476,32 @@ export function FilterPanel({ onSubmit, isLoading, listingCount }: FilterPanelPr
|
|||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Availability */}
|
||||
{/* Availability / Recency */}
|
||||
<AccordionItem value="availability">
|
||||
<AccordionTrigger className="py-2 text-sm font-medium">
|
||||
Availability
|
||||
{watchedListingType === ListingType.RENT ? 'Availability' : 'Recency'}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="available_from"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xs">Available From</FormLabel>
|
||||
<FormControl>
|
||||
<Calendar29
|
||||
onSelect={field.onChange}
|
||||
selected={field.value}
|
||||
rawInputValue={availableFromRawInput}
|
||||
onChangeRawInputValue={setAvailableFromRawInput}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-xs">
|
||||
Rental listings only
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{watchedListingType === ListingType.RENT && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="available_from"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xs">Available From</FormLabel>
|
||||
<FormControl>
|
||||
<Calendar29
|
||||
onSelect={field.onChange}
|
||||
selected={field.value}
|
||||
rawInputValue={availableFromRawInput}
|
||||
onChangeRawInputValue={setAvailableFromRawInput}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="last_seen_days"
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ export function PropertyCard({
|
|||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="font-semibold text-base truncate">
|
||||
£{property.total_price.toLocaleString()}
|
||||
<span className="text-muted-foreground font-normal text-sm">/mo</span>
|
||||
{property.listing_type !== 'BUY' && (
|
||||
<span className="text-muted-foreground font-normal text-sm">/mo</span>
|
||||
)}
|
||||
</div>
|
||||
{priceIndicator && (
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${priceIndicator.color}`}>
|
||||
|
|
@ -119,7 +121,9 @@ export function PropertyCard({
|
|||
<div>
|
||||
<div className="font-semibold text-xl">
|
||||
£{property.total_price.toLocaleString()}
|
||||
<span className="text-muted-foreground font-normal text-sm">/mo</span>
|
||||
{property.listing_type !== 'BUY' && (
|
||||
<span className="text-muted-foreground font-normal text-sm">/mo</span>
|
||||
)}
|
||||
</div>
|
||||
{priceIndicator && (
|
||||
<span className={`inline-block mt-1 text-xs px-2 py-0.5 rounded ${priceIndicator.color}`}>
|
||||
|
|
@ -145,10 +149,18 @@ export function PropertyCard({
|
|||
<PoundSterling className="h-4 w-4 text-muted-foreground" />
|
||||
<span><strong>£{property.qmprice}</strong>/m²</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Available <strong>{property.available_from}</strong></span>
|
||||
</div>
|
||||
{property.listing_type !== 'BUY' && property.available_from && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Available <strong>{property.available_from}</strong></span>
|
||||
</div>
|
||||
)}
|
||||
{property.listing_type === 'BUY' && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span>Seen <strong>{lastSeenDays}d</strong> ago</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agency and last seen */}
|
||||
|
|
|
|||
71
crawler/frontend/src/types/index.ts
Normal file
71
crawler/frontend/src/types/index.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// TypeScript types for the frontend application
|
||||
|
||||
// GeoJSON types
|
||||
export interface PropertyPriceHistory {
|
||||
id: number;
|
||||
price: number;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
export interface PropertyProperties {
|
||||
url: string;
|
||||
city: string;
|
||||
country: string;
|
||||
qm: number;
|
||||
qmprice: number;
|
||||
total_price: number;
|
||||
rooms: number;
|
||||
agency: string;
|
||||
available_from: string;
|
||||
last_seen: string;
|
||||
photo_thumbnail: string;
|
||||
price_history: PropertyPriceHistory[];
|
||||
listing_type?: 'RENT' | 'BUY';
|
||||
}
|
||||
|
||||
export interface PropertyFeature {
|
||||
type: 'Feature';
|
||||
geometry: {
|
||||
type: 'Point';
|
||||
coordinates: [number, number]; // [longitude, latitude]
|
||||
};
|
||||
properties: PropertyProperties;
|
||||
}
|
||||
|
||||
export interface GeoJSONFeatureCollection {
|
||||
type: 'FeatureCollection';
|
||||
features: PropertyFeature[];
|
||||
}
|
||||
|
||||
// Task status types
|
||||
export enum TaskStatus {
|
||||
PENDING = 'PENDING',
|
||||
STARTED = 'STARTED',
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILURE = 'FAILURE',
|
||||
REVOKED = 'REVOKED',
|
||||
}
|
||||
|
||||
export interface TaskStatusResponse {
|
||||
status: TaskStatus;
|
||||
result: string; // JSON string containing { progress: number }
|
||||
}
|
||||
|
||||
export interface TaskResult {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface RefreshListingsResponse {
|
||||
task_id: string;
|
||||
}
|
||||
|
||||
// API error type
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode: number
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue