mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1899765c |
@@ -64,12 +64,10 @@ listingsRouter.get('/table', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
listingsRouter.get('/map', async (req, res) => {
|
listingsRouter.get('/map', async (req, res) => {
|
||||||
const { jobId, minPrice, maxPrice } = req.query || {};
|
const { jobId } = req.query || {};
|
||||||
|
|
||||||
res.body = listingStorage.getListingsForMap({
|
res.body = listingStorage.getListingsForMap({
|
||||||
jobId: nullOrEmpty(jobId) ? null : jobId,
|
jobId: nullOrEmpty(jobId) ? null : jobId,
|
||||||
minPrice: minPrice ? parseInt(minPrice, 10) : null,
|
|
||||||
maxPrice: maxPrice ? parseInt(maxPrice, 10) : null,
|
|
||||||
userId: req.session.currentUser,
|
userId: req.session.currentUser,
|
||||||
isAdmin: isAdminFn(req),
|
isAdmin: isAdminFn(req),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -432,14 +432,11 @@ export const updateListingGeocoordinates = (id, latitude, longitude) => {
|
|||||||
*
|
*
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
* @param {string} [params.jobId]
|
* @param {string} [params.jobId]
|
||||||
* @param {boolean} [params.activeOnly=true]
|
|
||||||
* @param {number} [params.minPrice]
|
|
||||||
* @param {number} [params.maxPrice]
|
|
||||||
* @param {string} [params.userId]
|
* @param {string} [params.userId]
|
||||||
* @param {boolean} [params.isAdmin=false]
|
* @param {boolean} [params.isAdmin=false]
|
||||||
* @returns {{listings: Object[], maxPrice: number}} Object containing listings and maxPrice.
|
* @returns {{listings: Object[], maxPrice: number}} Object containing listings and maxPrice.
|
||||||
*/
|
*/
|
||||||
export const getListingsForMap = ({ jobId, minPrice, maxPrice, userId = null, isAdmin = false } = {}) => {
|
export const getListingsForMap = ({ jobId, userId = null, isAdmin = false } = {}) => {
|
||||||
const baseWhereParts = [
|
const baseWhereParts = [
|
||||||
'l.latitude IS NOT NULL',
|
'l.latitude IS NOT NULL',
|
||||||
'l.longitude IS NOT NULL',
|
'l.longitude IS NOT NULL',
|
||||||
@@ -461,15 +458,6 @@ export const getListingsForMap = ({ jobId, minPrice, maxPrice, userId = null, is
|
|||||||
}
|
}
|
||||||
|
|
||||||
const wherePartsForListings = [...baseWhereParts];
|
const wherePartsForListings = [...baseWhereParts];
|
||||||
if (minPrice !== undefined && minPrice !== null) {
|
|
||||||
params.minPrice = minPrice;
|
|
||||||
wherePartsForListings.push('l.price >= @minPrice');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxPrice !== undefined && maxPrice !== null) {
|
|
||||||
params.maxPrice = maxPrice;
|
|
||||||
wherePartsForListings.push('l.price <= @maxPrice');
|
|
||||||
}
|
|
||||||
|
|
||||||
const listings = SqliteConnection.query(
|
const listings = SqliteConnection.query(
|
||||||
`SELECT l.*, j.name AS job_name
|
`SELECT l.*, j.name AS job_name
|
||||||
@@ -479,17 +467,8 @@ export const getListingsForMap = ({ jobId, minPrice, maxPrice, userId = null, is
|
|||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxPriceRow = SqliteConnection.query(
|
|
||||||
`SELECT MAX(l.price) AS maxPrice
|
|
||||||
FROM listings l
|
|
||||||
LEFT JOIN jobs j ON j.id = l.job_id
|
|
||||||
WHERE ${baseWhereParts.join(' AND ')}`,
|
|
||||||
params,
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listings,
|
listings,
|
||||||
maxPrice: maxPriceRow?.maxPrice || 0,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1159
package-lock.json
generated
1159
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "18.0.0",
|
"version": "18.0.1",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@douyinfe/semi-icons": "^2.90.11",
|
"@douyinfe/semi-icons": "^2.90.13",
|
||||||
"@douyinfe/semi-ui": "2.90.11",
|
"@douyinfe/semi-ui": "2.90.13",
|
||||||
"@sendgrid/mail": "8.1.6",
|
"@sendgrid/mail": "8.1.6",
|
||||||
"@vitejs/plugin-react": "5.1.2",
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
@@ -85,6 +85,7 @@
|
|||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-chartjs-2": "^5.3.1",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
|
"react-range-slider-input": "^3.3.2",
|
||||||
"react-router": "7.12.0",
|
"react-router": "7.12.0",
|
||||||
"react-router-dom": "7.12.0",
|
"react-router-dom": "7.12.0",
|
||||||
"restana": "5.1.0",
|
"restana": "5.1.0",
|
||||||
@@ -96,9 +97,9 @@
|
|||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.5",
|
"@babel/core": "7.28.6",
|
||||||
"@babel/eslint-parser": "7.28.5",
|
"@babel/eslint-parser": "7.28.6",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.6",
|
||||||
"@babel/preset-react": "7.28.5",
|
"@babel/preset-react": "7.28.5",
|
||||||
"chai": "6.2.2",
|
"chai": "6.2.2",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
@@ -111,6 +112,6 @@
|
|||||||
"lint-staged": "16.2.7",
|
"lint-staged": "16.2.7",
|
||||||
"mocha": "11.7.5",
|
"mocha": "11.7.5",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"prettier": "3.7.4"
|
"prettier": "3.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import { useSelector, useActions } from '../../services/state/store.js';
|
import { useSelector, useActions } from '../../services/state/store.js';
|
||||||
import { Select, Slider, Space, Typography, Button, Popover, Divider, Switch, Banner } from '@douyinfe/semi-ui';
|
import { Select, Space, Typography, Button, Popover, Divider, Switch, Banner } from '@douyinfe/semi-ui';
|
||||||
import { IconFilter } from '@douyinfe/semi-icons';
|
import { IconFilter } from '@douyinfe/semi-icons';
|
||||||
import no_image from '../../assets/no_image.jpg';
|
import no_image from '../../assets/no_image.jpg';
|
||||||
|
import RangeSlider from 'react-range-slider-input';
|
||||||
|
import 'react-range-slider-input/dist/style.css';
|
||||||
import './Map.less';
|
import './Map.less';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@@ -65,23 +67,31 @@ export default function MapView() {
|
|||||||
const markers = useRef([]);
|
const markers = useRef([]);
|
||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
const listings = useSelector((state) => state.listingsData.mapListings);
|
const listings = useSelector((state) => state.listingsData.mapListings);
|
||||||
const maxPriceFromStore = useSelector((state) => state.listingsData.maxPrice);
|
|
||||||
const [style, setStyle] = useState('STANDARD');
|
const [style, setStyle] = useState('STANDARD');
|
||||||
const [show3dBuildings, setShow3dBuildings] = useState(false);
|
const [show3dBuildings, setShow3dBuildings] = useState(false);
|
||||||
|
|
||||||
const jobs = useSelector((state) => state.jobsData.jobs);
|
const jobs = useSelector((state) => state.jobsData.jobs);
|
||||||
const [jobId, setJobId] = useState(null);
|
const [jobId, setJobId] = useState(null);
|
||||||
const [priceRange, setPriceRange] = useState([0, 100000]);
|
const [priceRange, setPriceRange] = useState([0, 0]);
|
||||||
const [showFilterBar, setShowFilterBar] = useState(false);
|
const [showFilterBar, setShowFilterBar] = useState(false);
|
||||||
|
|
||||||
const lastJobIdRef = useRef('__INITIAL__');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (maxPriceFromStore > 0 && lastJobIdRef.current !== jobId) {
|
setPriceRange([0, getMaxPrice()]);
|
||||||
setPriceRange([0, maxPriceFromStore]);
|
}, [listings]);
|
||||||
lastJobIdRef.current = jobId;
|
|
||||||
}
|
const getMaxPrice = () => {
|
||||||
}, [maxPriceFromStore, jobId]);
|
return listings.reduce((max, item) => {
|
||||||
|
const price = Number(item.price);
|
||||||
|
return Number.isFinite(price) && price > max ? price : max;
|
||||||
|
}, -Infinity);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterListings = () => {
|
||||||
|
const min = priceRange[0];
|
||||||
|
const max = priceRange[1] && priceRange[1] > 0 ? priceRange[1] : getMaxPrice();
|
||||||
|
|
||||||
|
return listings.filter((listing) => listing.price && listing.price >= min && listing.price <= max);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (map.current) return;
|
if (map.current) return;
|
||||||
@@ -97,13 +107,22 @@ export default function MapView() {
|
|||||||
|
|
||||||
map.current.addControl(
|
map.current.addControl(
|
||||||
new maplibregl.NavigationControl({
|
new maplibregl.NavigationControl({
|
||||||
showCompass: false,
|
showCompass: true,
|
||||||
visualizePitch: true,
|
visualizePitch: true,
|
||||||
visualizeRoll: true,
|
visualizeRoll: true,
|
||||||
}),
|
}),
|
||||||
'top-right',
|
'top-right',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
map.current.addControl(
|
||||||
|
new maplibregl.GeolocateControl({
|
||||||
|
positionOptions: {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
},
|
||||||
|
trackUserLocation: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
map.current.remove();
|
map.current.remove();
|
||||||
};
|
};
|
||||||
@@ -199,14 +218,12 @@ export default function MapView() {
|
|||||||
const fetchListings = async () => {
|
const fetchListings = async () => {
|
||||||
actions.listingsData.getListingsForMap({
|
actions.listingsData.getListingsForMap({
|
||||||
jobId,
|
jobId,
|
||||||
minPrice: priceRange[0] > 0 ? priceRange[0] : null,
|
|
||||||
maxPrice: maxPriceFromStore > 0 && priceRange[1] < maxPriceFromStore ? priceRange[1] : null,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchListings();
|
fetchListings();
|
||||||
}, [jobId, priceRange]);
|
}, [jobId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map.current) return;
|
if (!map.current) return;
|
||||||
@@ -214,7 +231,7 @@ export default function MapView() {
|
|||||||
markers.current.forEach((marker) => marker.remove());
|
markers.current.forEach((marker) => marker.remove());
|
||||||
markers.current = [];
|
markers.current = [];
|
||||||
|
|
||||||
listings.forEach((listing) => {
|
filterListings().forEach((listing) => {
|
||||||
if (
|
if (
|
||||||
listing.latitude != null &&
|
listing.latitude != null &&
|
||||||
listing.longitude != null &&
|
listing.longitude != null &&
|
||||||
@@ -247,7 +264,7 @@ export default function MapView() {
|
|||||||
markers.current.push(marker);
|
markers.current.push(marker);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [listings]);
|
}, [listings, priceRange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-view-container">
|
<div className="map-view-container">
|
||||||
@@ -285,7 +302,9 @@ export default function MapView() {
|
|||||||
placeholder="Job"
|
placeholder="Job"
|
||||||
showClear
|
showClear
|
||||||
style={{ width: 150 }}
|
style={{ width: 150 }}
|
||||||
onChange={(val) => setJobId(val)}
|
onChange={(val) => {
|
||||||
|
setJobId(val);
|
||||||
|
}}
|
||||||
value={jobId}
|
value={jobId}
|
||||||
>
|
>
|
||||||
{jobs?.map((j) => (
|
{jobs?.map((j) => (
|
||||||
@@ -302,13 +321,18 @@ export default function MapView() {
|
|||||||
<Text strong>Price Range (€):</Text>
|
<Text strong>Price Range (€):</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: 250, padding: '0 10px' }}>
|
<div style={{ width: 250, padding: '0 10px' }}>
|
||||||
<Slider
|
<div className="map__rangesliderLabels">
|
||||||
range
|
<span>{priceRange[0]} €</span>
|
||||||
|
<span>{priceRange[1]} €</span>
|
||||||
|
</div>
|
||||||
|
<RangeSlider
|
||||||
min={0}
|
min={0}
|
||||||
max={maxPriceFromStore || 100000}
|
max={getMaxPrice()}
|
||||||
step={100}
|
step={100}
|
||||||
value={priceRange}
|
value={priceRange}
|
||||||
onChange={(val) => setPriceRange(val)}
|
onInput={(val) => {
|
||||||
|
setPriceRange(val);
|
||||||
|
}}
|
||||||
tipFormatter={(val) => `${val} €`}
|
tipFormatter={(val) => `${val} €`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,11 +63,22 @@
|
|||||||
border-bottom-color: var(--semi-color-bg-1) !important;
|
border-bottom-color: var(--semi-color-bg-1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maplibregl-ctrl-group {
|
.map {
|
||||||
background: var(--semi-color-bg-1) !important;
|
&__rangesliderLabels{
|
||||||
}
|
color: white;
|
||||||
|
display: flex;
|
||||||
.maplibregl-ctrl-group button {
|
justify-content: space-between;
|
||||||
background-color: var(--semi-color-bg-1) !important;
|
margin-bottom: .3rem;
|
||||||
border-color: var(--semi-color-border) !important;
|
font-size: .7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.range-slider .range-slider__thumb {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 50%;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2196f3;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user