Skip to content

Commit cc302e9

Browse files
committed
add filters feature
1 parent d4e6880 commit cc302e9

File tree

11 files changed

+3983
-2162
lines changed

11 files changed

+3983
-2162
lines changed

client/api/divar.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ const httpClient = ofetch.create({
1717

1818
export type ProgressFunction<T> = (value: number, max: number, title: string, lastValue: T) => void;
1919

20+
export type City = {
21+
name: string,
22+
id: string,
23+
}
24+
25+
export const getCities = async (): Promise<City[]> => {
26+
// https://api.divar.ir/v8/search-bookmark/web/get-search-bar-empty-state
27+
const response = await ofetch('./divar_cities.json' , {
28+
baseURL: window.location.origin + window.location.pathname,
29+
responseType: 'json',
30+
headers: {
31+
'Content-Type': 'application/json',
32+
},
33+
});
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
return response.map((row: any) => ({
36+
id: row.city_id,
37+
name: row.city_slug.split('-').map((x: string) => `${x[0].toUpperCase()}${x.slice(1)}`).join(' '),
38+
}) satisfies City);
39+
}
40+
2041
export type House = {
2142
token: string,
2243
location: {
@@ -239,6 +260,7 @@ export const getDistrictHouses = async (filters: GetDistrictHousesFilters): Prom
239260

240261
export type GetAllHousesFilters = Omit<GetDistrictHousesFilters, 'district'> & {
241262
cityId: string,
263+
exact: boolean;
242264
};
243265

244266
export const getAllCityHouses = async (filters: GetAllHousesFilters, progressFn?: ProgressFunction<House[]>): Promise<House[]> => {
@@ -248,15 +270,17 @@ export const getAllCityHouses = async (filters: GetAllHousesFilters, progressFn?
248270
}
249271
progress(0, 1, '');
250272

273+
const totalSteps = filters.exact ? 3 : 2;
274+
251275
// Step 1: prepare
252276
const districts = await getDistricts(filters.cityId, (a, b, t) => {
253-
progress((a / b), 3, t);
277+
progress((a / b), totalSteps, t);
254278
});
255279

256280
// Step 2: fetch
257281
let passedDistricts = 0;
258282
for (const district of districts) {
259-
progress((passedDistricts / districts.length) + 1, 3, 'Fetching Districts Houses');
283+
progress((passedDistricts / districts.length) + 1, totalSteps, 'Fetching Districts Houses');
260284
const districtHouses = await getDistrictHouses({
261285
...filters,
262286
district,
@@ -266,23 +290,25 @@ export const getAllCityHouses = async (filters: GetAllHousesFilters, progressFn?
266290
passedDistricts+=1;
267291
}
268292

269-
// Step 3: verify
270-
let passedHouses = 0;
271-
const approximateHouses = [...returnValue];
272-
for (const approximateHouse of approximateHouses) {
273-
progress((passedHouses / approximateHouses.length) + 2, 3, 'Verifying Houses Data');
274-
try {
275-
const validatedHouse = await getHouse(approximateHouse.token);
276-
returnValue = [
277-
...returnValue.filter(x => x !== approximateHouse),
278-
validatedHouse,
279-
];
280-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
281-
} catch (_e) {
282-
continue;
293+
if (filters.exact) {
294+
// Step 3: verify
295+
let passedHouses = 0;
296+
const approximateHouses = [...returnValue];
297+
for (const approximateHouse of approximateHouses) {
298+
progress((passedHouses / approximateHouses.length) + 2, totalSteps, 'Verifying Houses Data');
299+
try {
300+
const validatedHouse = await getHouse(approximateHouse.token);
301+
returnValue = [
302+
...returnValue.filter(x => x !== approximateHouse),
303+
validatedHouse,
304+
];
305+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
306+
} catch (_e) {
307+
continue;
308+
}
309+
passedHouses += 1;
310+
await new Promise((r) => setTimeout(r, 1100));
283311
}
284-
passedHouses += 1;
285-
await new Promise((r) => setTimeout(r, 1100));
286312
}
287313
return returnValue;
288314
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getCities, type GetAllHousesFilters } from "@/api";
2+
import { useEffect, useState } from "react";
3+
import { useAsync } from "react-use";
4+
5+
export function HousesFilters({ value: _value, onChange }: { value: GetAllHousesFilters, onChange?: (newValue: GetAllHousesFilters) => void }) {
6+
const [value, setValue] = useState<GetAllHousesFilters>(_value);
7+
8+
const cities = useAsync(() => getCities());
9+
10+
useEffect(() => {
11+
onChange?.(value);
12+
}, [onChange, value]);
13+
return (
14+
<div className="flex flex-col justify-between items-stretch gap-6">
15+
<label className="select w-full">
16+
<span className="label">City</span>
17+
<select value={value.cityId} onChange={e => setValue(p => ({ ...p, cityId: e.target.value}))}>
18+
{ cities.value?.map((opt) => (
19+
<option key={opt.id} value={opt.id}>{opt.name}</option>
20+
))}
21+
</select>
22+
</label>
23+
24+
<label className="input w-full">
25+
<span className="label">Minimum Size</span>
26+
<input type="number" min={10} max={value.size?.[1] ?? 400} value={value.size?.[0] ?? 30} onChange={e => setValue(p => ({ ...p, size: [(e.target.valueAsNumber ?? 0), p?.size?.[1] ?? (e.target.valueAsNumber ?? 0) + 10]}))} step="5" />
27+
</label>
28+
29+
<label className="input w-full">
30+
<span className="label">Maximum Size</span>
31+
<input type="number" min={value.size?.[0] ?? 10} max={400} value={value.size?.[1] ?? 120} onChange={e => setValue(p => ({ ...p, size: [p?.size?.[0] ?? (e.target.valueAsNumber ?? 0) - 10, (e.target.valueAsNumber ?? 0)]}))} step="5" />
32+
</label>
33+
34+
<div className="flex items-center gap-2 flex-nowrap w-full justify-between">
35+
<label className="label">
36+
<input type="checkbox" defaultChecked={value.parking} onChange={e => setValue(p => ({ ...p, parking: e.target.checked || undefined }))} className="toggle" />
37+
Parking
38+
</label>
39+
<label className="label">
40+
<input type="checkbox" defaultChecked={value.elevator} onChange={e => setValue(p => ({ ...p, elevator: e.target.checked || undefined }))} className="toggle" />
41+
Elevator
42+
</label>
43+
<label className="label">
44+
<input type="checkbox" defaultChecked={value.balcony} onChange={e => setValue(p => ({ ...p, balcony: e.target.checked || undefined }))} className="toggle" />
45+
Balcony
46+
</label>
47+
</div>
48+
49+
<label className="label w-full">
50+
<input type="checkbox" defaultChecked={!value.exact} onChange={e => setValue(p => ({ ...p, exact: e.target.checked}))} className="checkbox" />
51+
Approximate Search
52+
</label>
53+
</div>
54+
)
55+
}

client/components/HousesMap.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
} from "@/api";
77

88

9+
const mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
10+
911
export function HousesMap({
1012
houses,
11-
mapStyle,
1213
children,
1314
}: {
1415
houses: House[],
15-
mapStyle: string,
1616
children?: ReactNode,
1717
}) {
1818

@@ -39,8 +39,8 @@ const gradientStops = useMemo<{ t: number; color: [number, number, number] }[]>(
3939
data,
4040
min: sortedPrices[Math.floor((sortedPrices.length - 1) * gradientStops[1].t)],
4141
max: sortedPrices[Math.floor((sortedPrices.length - 1) * gradientStops[gradientStops.length - 2].t)],
42-
longitude: isNaN(center[0]) ? 0 : center[0],
43-
latitude: isNaN(center[1]) ? 0 : center[1],
42+
longitude: isNaN(center[0]) ? 51.3347 : center[0],
43+
latitude: isNaN(center[1]) ? 35.7219 : center[1],
4444
};
4545
}, [houses, gradientStops]);
4646

client/components/MapStyleSelect.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

client/main.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@import "tailwindcss";
2-
@import "maplibre-gl/dist/maplibre-gl.css"
2+
@plugin "daisyui";
3+
@import "maplibre-gl/dist/maplibre-gl.css";

client/main.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { StrictMode } from 'react'
22
import { createRoot } from 'react-dom/client'
3-
import '@/main.css'
4-
import "@radix-ui/themes/styles.css";
5-
import { Theme } from '@radix-ui/themes';
63
import { Root } from '@/views/Root'
74

85
createRoot(document.getElementById('root')!).render(
96
<StrictMode>
10-
<Theme>
11-
<Root />
12-
</Theme>
7+
<Root />
138
</StrictMode>,
149
)

client/views/Root.tsx

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,28 @@
1-
import { Button, Progress } from "@radix-ui/themes";
21
import { useEffect, useState } from "react";
3-
import { VscRunAbove, VscStopCircle } from "react-icons/vsc";
42
import {
53
getAllCityHouses,
64
type GetAllHousesFilters,
75
type House,
86
} from "@/api";
9-
import { useAsync, useAsyncFn, useLocalStorage } from "react-use";
10-
import { MapStyleSelect } from "@/components/MapStyleSelect";
7+
import { useAsync, useAsyncFn } from "react-use";
118
import { ofetch } from "ofetch";
129
import { HousesMap } from "@/components/HousesMap";
13-
10+
import { VscPlay, VscSaveAll, VscSettingsGear } from "react-icons/vsc";
11+
import { HousesFilters } from "@/components/HousesFilters";
1412

1513

1614
export function Root() {
17-
const [filters] = useState<GetAllHousesFilters>(
15+
const [filters, setFilters] = useState<GetAllHousesFilters>(
1816
{
1917
cityId: '1',
20-
size: [30, 200],
18+
exact: false,
19+
size: [30, 120],
2120
}
2221
);
2322

24-
const [mapStyle, setMapStyle] = useLocalStorage<string>('map-style');
25-
2623
const [progress, setProgress] = useState<number>(0);
2724
const [progressText, setProgressText] = useState('');
2825

29-
useEffect(() => {
30-
console.log(progressText);
31-
}, [progressText]);
32-
3326
const lastSavedHouses = useAsync(() => ofetch<House[]>(new URL('./tehran.json', window.location.origin + window.location.pathname).href));
3427
const [houses, setHouses] = useState<House[]>([]);
3528
useEffect(() => {
@@ -41,41 +34,53 @@ export function Root() {
4134
setProgress(a / b);
4235
setProgressText(progressText);
4336
setHouses(currentHouses);
37+
}).then(() => {
38+
setProgress(0);
39+
setProgressText('');
4440
});
4541
}, [filters]);
4642

4743
return (
48-
<div className="w-svw h-svh relative">
49-
<HousesMap
50-
houses={houses ?? []}
51-
mapStyle={mapStyle!}
52-
/>
53-
<div className="absolute top-0 left-0 w-full p-2 flex items-center justify-center gap-2 backdrop-contrast-50 backdrop-blur-sm">
54-
<Progress size="2" max={1} value={progress} color="green" variant="classic" />
55-
<Button
44+
<div className="h-svh w-svw relative">
45+
<div className="flex items-center justify-between backdrop-blur-sm bg-black z-10 absolute top-0 left-0 w-full p-2 gap-2">
46+
<div className="flex items-center flex-col justify-between grow gap-1 h-full">
47+
<label htmlFor="progress-bar" className="text-xs h-4">{progressText ? `${progressText}...` : ''}</label>
48+
<progress id="progress-bar" value={progress} max="1" className="progress w-full h-2" />
49+
</div>
50+
<button
51+
onClick={()=>document.querySelector<HTMLDialogElement>('#filters-dialog')!.showModal()}
52+
disabled={crawlHouses.loading}
53+
className="shrink-0 btn btn-sm btn-soft"
54+
>
55+
<VscSettingsGear className="size-4" /> Settings
56+
</button>
57+
<button
5658
onClick={() => {
5759
startCrawl();
5860
}}
59-
color={crawlHouses.loading ? 'gray' : 'green'}
60-
size="2"
61-
variant="classic"
61+
disabled={crawlHouses.loading}
62+
className="shrink-0 btn btn-sm btn-primary"
6263
>
63-
64-
{crawlHouses.loading ? (
65-
<>
66-
<VscStopCircle />
67-
Stop
68-
</>
69-
) : (
70-
<>
71-
<VscRunAbove />
72-
Start Crawl Divar
73-
</>
74-
)}
75-
</Button>
76-
|
77-
<MapStyleSelect value={mapStyle} onValueChange={setMapStyle} size="2" />
64+
<VscPlay className="size-4" /> Run
65+
</button>
7866
</div>
67+
68+
<dialog id="filters-dialog" className="modal">
69+
<div className="modal-box">
70+
<h3 className="font-bold text-lg">Filters</h3>
71+
<div className="modal-action">
72+
<form method="dialog" className="w-full">
73+
<HousesFilters value={filters} onChange={setFilters} />
74+
<div className="divider" />
75+
<button className="btn btn-block btn-primary"><VscSaveAll /> OK</button>
76+
</form>
77+
</div>
78+
</div>
79+
</dialog>
80+
81+
<HousesMap
82+
houses={houses ?? []}
83+
/>
7984
</div>
8085
);
8186
}

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<link href="/client/main.css" rel="stylesheet">
78
<title>Melk-Map</title>
89
</head>
910
<body>

0 commit comments

Comments
 (0)