Reorganize & reformat
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import {galleryImageUrl} from '../lib/helpers';
|
||||
|
||||
export default class Gallery extends React.Component {
|
||||
onPrevClick (e) {
|
||||
this.props.dispatch({type: 'PREV_IMAGE'});
|
||||
}
|
||||
|
||||
onNextClick (e) {
|
||||
this.props.dispatch({type: 'NEXT_IMAGE'});
|
||||
}
|
||||
|
||||
onImageDotClick (index, e) {
|
||||
this.props.dispatch({type: 'VIEW_IMAGE', action: {index}})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {images, imageIndex} = this.props;
|
||||
if (!images || images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showPrev = imageIndex > 0;
|
||||
const showNext = imageIndex < images.length - 1;
|
||||
|
||||
return (
|
||||
<div className="ld-image-container">
|
||||
<img src={galleryImageUrl(images[imageIndex])}></img>
|
||||
{showPrev ?
|
||||
<div
|
||||
className='prev-button'
|
||||
onClick={this.onPrevClick.bind(this)}>
|
||||
<div>
|
||||
<svg fill="white" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 512 512">
|
||||
<path d="M213.7,256L213.7,256L213.7,256L380.9,81.9c4.2-4.3,4.1-11.4-0.2-15.8l-29.9-30.6c-4.3-4.4-11.3-4.5-15.5-0.2L131.1,247.9 c-2.2,2.2-3.2,5.2-3,8.1c-0.1,3,0.9,5.9,3,8.1l204.2,212.7c4.2,4.3,11.2,4.2,15.5-0.2l29.9-30.6c4.3-4.4,4.4-11.5,0.2-15.8 L213.7,256z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
: null}
|
||||
{showNext ?
|
||||
<div
|
||||
className='next-button'
|
||||
onClick={this.onNextClick.bind(this)}>
|
||||
<div>
|
||||
<svg fill="white" version="1.1" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 512 512">
|
||||
<path d="M298.3,256L298.3,256L298.3,256L131.1,81.9c-4.2-4.3-4.1-11.4,0.2-15.8l29.9-30.6c4.3-4.4,11.3-4.5,15.5-0.2l204.2,212.7 c2.2,2.2,3.2,5.2,3,8.1c0.1,3-0.9,5.9-3,8.1L176.7,476.8c-4.2,4.3-11.2,4.2-15.5-0.2L131.3,446c-4.3-4.4-4.4-11.5-0.2-15.8 L298.3,256z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div className="image-dots">
|
||||
{images.map((img, index) => {
|
||||
let cls = 'image-dot'
|
||||
if (index === imageIndex) {
|
||||
cls += ' selected'
|
||||
}
|
||||
|
||||
return <div key={img} onClick={this.onImageDotClick.bind(this, index)} className={cls}></div>
|
||||
})}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
import React from 'react';
|
||||
import Filters from './Filters';
|
||||
import Listings from './Listings';
|
||||
import ListingDetails from './ListingDetails';
|
||||
import { pacSelectFirst } from '../helpers/googleMaps';
|
||||
import { loadProperties, loadSeen, loadListing} from '../lib/api'
|
||||
import { handleMessage } from '../lib/handlers'
|
||||
import Router from '../lib/router';
|
||||
|
||||
class Main extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const state = {
|
||||
listingDetails: false,
|
||||
listings: (new Map()),
|
||||
imageIndex: 0,
|
||||
page: 0,
|
||||
sort: 'relevance',
|
||||
filters: {
|
||||
rooms: {},
|
||||
category: {}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.initialState) {
|
||||
props.initialState.sort = props.initialState.sort || state.sort;
|
||||
state.filters.rooms = props.initialState.rooms;
|
||||
state.filters.category = props.initialState.category;
|
||||
state.sort = props.initialState.sort || state.sort;
|
||||
state.listingId = props.initialState.listingId;
|
||||
state.bounds = props.initialState.bounds;
|
||||
state.zoom = props.initialState.zoom;
|
||||
if (state.listingId) {
|
||||
state.listingDetails = true;
|
||||
}
|
||||
state.filters.minSize = props.initialState.minSize;
|
||||
state.filters.maxSize = props.initialState.maxSize;
|
||||
state.filters.minPrice = props.initialState.minPrice;
|
||||
state.filters.maxPrice = props.initialState.maxPrice;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
this.router = new Router(this, props.initialState);
|
||||
}
|
||||
|
||||
dispatch ({type, action = {}}) {
|
||||
handleMessage({type, action}, this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const uluru = {lat: 43.845031, lng: 18.4019262};
|
||||
const opts = {
|
||||
//zoom: 13,
|
||||
//center: uluru,
|
||||
streetViewControl: false,
|
||||
mapTypeControl: false
|
||||
};
|
||||
|
||||
if (!this.state.bounds) {
|
||||
opts.zoom = 13;
|
||||
opts.center= uluru;
|
||||
};
|
||||
|
||||
const map = new google.maps.Map(this.refs.map, opts);
|
||||
window.gmap = map;
|
||||
|
||||
//const marker = new google.maps.Marker({
|
||||
//position: uluru,
|
||||
//map: map
|
||||
//});
|
||||
|
||||
var control = document.createElement('div');
|
||||
control.classList.add('filters-btn-toggle');
|
||||
control.innerHTML = '<button>Filters</button>';
|
||||
//control.style = "top: 200px;"
|
||||
control["style"]= "top: 200px;"
|
||||
|
||||
var input = document.getElementById('gmaps-places-input');
|
||||
|
||||
pacSelectFirst(input);
|
||||
var options = {
|
||||
componentRestrictions: {country: "BA"}
|
||||
};
|
||||
|
||||
const regularIdle = () => {
|
||||
this.dispatch({type: 'UPDATE_ROUTE', action: {params: {
|
||||
bounds: map.getBounds().toUrlValue(),
|
||||
zoom: map.getZoom()
|
||||
}}});
|
||||
|
||||
this.dispatch({type: 'MAP_IDLE'});
|
||||
};
|
||||
|
||||
// Check if initial bounds are passed-in
|
||||
|
||||
google.maps.event.addListenerOnce(map, 'idle', () => {
|
||||
|
||||
if (this.state.bounds) {
|
||||
const [ lat1, lng1, lat2, lng2 ] = this.state.bounds.split(",");
|
||||
const sw = new google.maps.LatLng({lat: parseFloat(lat1), lng: parseFloat(lng1)});
|
||||
const ne = new google.maps.LatLng({lat: parseFloat(lat2), lng: parseFloat(lng2)});
|
||||
|
||||
const initialBounds = new google.maps.LatLngBounds(sw, ne);
|
||||
//map.fitBounds(initialBounds);
|
||||
|
||||
const originalMaxZoom = map.maxZoom;
|
||||
const originalMinZoom = map.minZoom;
|
||||
map.setOptions({maxZoom: parseInt(this.state.zoom), minZoom: parseInt(this.state.zoom)});
|
||||
map.fitBounds(initialBounds);
|
||||
map.setOptions({maxZoom: originalMaxZoom, minZoom: originalMinZoom});
|
||||
}
|
||||
|
||||
});
|
||||
map.addListener('idle', regularIdle);
|
||||
var searchBox = new google.maps.places.Autocomplete(input, options);
|
||||
|
||||
searchBox.addListener('place_changed', () => {
|
||||
var place = searchBox.getPlace();
|
||||
|
||||
if (!place.geometry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (place.geometry.viewport) {
|
||||
map.fitBounds(place.geometry.viewport);
|
||||
|
||||
} else {
|
||||
map.setCenter(place.geometry.location);
|
||||
map.setZoom(18);
|
||||
}
|
||||
this.dispatch({type: 'SEARCH_PLACE_CHANGED'});
|
||||
});
|
||||
|
||||
control.addEventListener('click', (e) => {
|
||||
this.setState({
|
||||
mapClicked: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
control.index = 1;
|
||||
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control);
|
||||
this.map = map;
|
||||
|
||||
// TODO: if state contains listingId reload
|
||||
if (this.state.listingId) {
|
||||
loadListing(this.state.listingId).then(l => l.text()).then(l => {
|
||||
this.dispatch({type: 'VIEW_LISTING_DETAILS', action: {
|
||||
id: this.state.listingId,
|
||||
listing: JSON.parse(l)
|
||||
}});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeAllMarkers () {
|
||||
if (this.markers) {
|
||||
this.markers.forEach((m) => m.marker.setMap(null));
|
||||
}
|
||||
}
|
||||
|
||||
onCloseClick(e) {
|
||||
if (this.state.mapClicked) {
|
||||
setTimeout(() => {
|
||||
google.maps.event.trigger(this.map, 'resize');
|
||||
}, 100);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mapClicked: false
|
||||
});
|
||||
}
|
||||
|
||||
findMarker (id) {
|
||||
if (!this.markers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this.markers.findIndex(m => m.id === id);
|
||||
return this.markers[index];
|
||||
}
|
||||
|
||||
isSeen (id) {
|
||||
const seen = loadSeen();
|
||||
return seen.findIndex(s => s === id) !== -1
|
||||
}
|
||||
|
||||
loadPins () {
|
||||
const map = this.map;
|
||||
const {
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category
|
||||
} = this.state.filters;
|
||||
|
||||
const bounds = map.getBounds();
|
||||
const properties = loadProperties({
|
||||
bounds: bounds.toUrlValue(),
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category,
|
||||
page: this.state.page,
|
||||
pins: true
|
||||
});
|
||||
|
||||
|
||||
const markerExists = (id) => {
|
||||
return this.findMarker(id) != null;
|
||||
}
|
||||
|
||||
properties
|
||||
.then(p => {
|
||||
return {
|
||||
body: p.text(),
|
||||
totalCount: p.headers.get('X-Total-Count')
|
||||
};
|
||||
})
|
||||
.then(({body, totalCount}) => {
|
||||
body.then(p => {
|
||||
const data = JSON.parse(p);
|
||||
|
||||
const listingExists = (id) => {
|
||||
return data.findIndex(l => l._id === id) !== -1
|
||||
};
|
||||
|
||||
|
||||
const newMarkers = [];
|
||||
|
||||
if (this.markers) {
|
||||
this.markers.forEach((m) => {
|
||||
if (!listingExists(m.id)) {
|
||||
m.marker.setMap(null);
|
||||
} else {
|
||||
newMarkers.push(m);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for(const [index, prop] of data.entries()) {
|
||||
const myLatLng = {lat: prop.loc[0], lng: prop.loc[1]};
|
||||
|
||||
if (!markerExists(prop._id)) {
|
||||
|
||||
const marker = new google.maps.Marker({
|
||||
position : myLatLng,
|
||||
map : map,
|
||||
//title : prop.title,
|
||||
icon : this.isSeen(prop._id) ? this.visitedMarkerIcon() : this.defaultMarkerIcon(),
|
||||
id : prop._id
|
||||
});
|
||||
|
||||
marker.addListener('mouseover', () => {
|
||||
if (marker.id !== this.state.listingId) {
|
||||
if (this.isSeen(marker.id)) {
|
||||
marker.setIcon(this.visitedHoveredMarkerIcon());
|
||||
} else {
|
||||
marker.setIcon(this.hoveredMarkerIcon());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
marker.addListener('mouseout', () => {
|
||||
if (marker.id !== this.state.listingId) {
|
||||
if (this.isSeen(marker.id)) {
|
||||
marker.setIcon(this.visitedMarkerIcon());
|
||||
} else {
|
||||
marker.setIcon(this.defaultMarkerIcon());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
marker.addListener('click', () => {
|
||||
// Maybe move out and call when popping state
|
||||
if (this.state.listingId) {
|
||||
const prevSelected = this.findMarker(this.state.listingId);
|
||||
if (prevSelected) {
|
||||
prevSelected.marker.setIcon(this.visitedMarkerIcon());
|
||||
}
|
||||
}
|
||||
|
||||
marker.setIcon(this.selectedMarkerIcon());
|
||||
|
||||
|
||||
loadListing(prop._id).then(l => l.text()).then(l => {
|
||||
|
||||
this.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
toDispatch: {
|
||||
type: 'VIEW_LISTING_DETAILS', action: {
|
||||
id: prop._id,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
},
|
||||
params: {
|
||||
listingId: prop._id
|
||||
}
|
||||
}});
|
||||
|
||||
//this.dispatch({type: 'UPDATE_ROUTE', action: {type: 'VIEW_LISTING_DETAILS', action: {
|
||||
//id: prop._id,
|
||||
//listing: JSON.parse(l)
|
||||
//}}});
|
||||
|
||||
this.dispatch({type: 'VIEW_LISTING_DETAILS', action: {
|
||||
id: prop._id,
|
||||
listing: JSON.parse(l)
|
||||
}});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
newMarkers.push({
|
||||
marker,
|
||||
id: prop._id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
type: 'PINS_LOADED',
|
||||
action: {
|
||||
newMarkers
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Refreshes search
|
||||
*/
|
||||
refreshListings(more = false) {
|
||||
|
||||
// TODO: move somewhere else
|
||||
|
||||
if (!more && this.state.listingId) {
|
||||
|
||||
loadListing(this.state.listingId).then(l => l.text()).then(l => {
|
||||
this.dispatch({type: 'VIEW_LISTING_DETAILS', action: {
|
||||
id: this.state.listingId,
|
||||
listing: JSON.parse(l)
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
if (!more) {
|
||||
this.loadPins();
|
||||
}
|
||||
|
||||
const map = this.map;
|
||||
const {
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category
|
||||
} = this.state.filters;
|
||||
|
||||
const bounds = map.getBounds();
|
||||
const properties = loadProperties({
|
||||
bounds: bounds.toUrlValue(),
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category,
|
||||
page: this.state.page,
|
||||
sort: this.state.sort
|
||||
});
|
||||
|
||||
|
||||
properties
|
||||
.then(p => {
|
||||
return {
|
||||
body: p.text(),
|
||||
totalCount: p.headers.get('X-Total-Count')
|
||||
};
|
||||
})
|
||||
.then(({body, totalCount}) => {
|
||||
body.then(p => {
|
||||
const data = JSON.parse(p);
|
||||
|
||||
this.dispatch({
|
||||
type: 'LISTINGS_LOADED',
|
||||
action: {
|
||||
listings: data,
|
||||
more,
|
||||
totalCount
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Get default marker icon
|
||||
*/
|
||||
defaultMarkerIcon () {
|
||||
const sf = 0.5;
|
||||
const width = 48;
|
||||
const height = 64;
|
||||
const icon = {
|
||||
url : "static/images/pins_sprite.png",
|
||||
size : new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize : new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin : new google.maps.Point(0, 36 * sf)
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get visited marker icon
|
||||
*/
|
||||
visitedMarkerIcon () {
|
||||
const sf = 0.5;
|
||||
const width = 48;
|
||||
const height = 64;
|
||||
const icon = {
|
||||
url : "static/images/pins_sprite.png",
|
||||
size : new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize : new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin : new google.maps.Point(152 * sf, 36 * sf)
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/*
|
||||
* Visited hovered marker icon
|
||||
*/
|
||||
visitedHoveredMarkerIcon () {
|
||||
const sf = 0.5;
|
||||
const width = 61;
|
||||
const height = 82;
|
||||
const icon = {
|
||||
url : "static/images/pins_sprite.png",
|
||||
size : new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize : new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin : new google.maps.Point(480 * sf, 18 * sf)
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hovered marker icon
|
||||
*/
|
||||
hoveredMarkerIcon () {
|
||||
const sf = 0.5;
|
||||
const width = 61;
|
||||
const height = 82;
|
||||
const icon = {
|
||||
url : "static/images/pins_sprite.png",
|
||||
size : new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize : new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin : new google.maps.Point(303 * sf, 18 * sf)
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
/*
|
||||
* Selected marker icon
|
||||
*/
|
||||
selectedMarkerIcon () {
|
||||
const sf = 0.5;
|
||||
const width = 73;
|
||||
const height = 100;
|
||||
const icon = {
|
||||
url : "static/images/pins_sprite.png",
|
||||
size : new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize : new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin : new google.maps.Point(655 * sf, 1 * sf)
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
renderRightContent() {
|
||||
|
||||
const children = [];
|
||||
|
||||
if (this.state.listingDetails) {
|
||||
const listing = this.state.listing; //this.state.listings.get(this.state.listingId);
|
||||
children.push(<ListingDetails
|
||||
listing={listing}
|
||||
imageIndex={this.state.imageIndex}
|
||||
dispatch={this.dispatch.bind(this)}
|
||||
descriptionExpanded={this.state.descriptionExpanded}
|
||||
/>);
|
||||
} else {
|
||||
children.push(<Filters filters={this.state.filters} dispatch={this.dispatch.bind(this)} onClose={this.onCloseClick.bind(this)}/>);
|
||||
children.push(<Listings
|
||||
sort={this.state.sort}
|
||||
totalCount={this.state.totalCount}
|
||||
loadingMore={this.state.loadingMore}
|
||||
listings={this.state.listings}
|
||||
dispatch={this.dispatch.bind(this)}
|
||||
/>);
|
||||
}
|
||||
const content = (
|
||||
<div className="right-content">
|
||||
{children}
|
||||
</div>);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
const leftStyle = {};
|
||||
const rightStyle = {};
|
||||
const listingDetails = true;
|
||||
|
||||
let leftClass = 'left-base';
|
||||
let rightClass = 'right-base';
|
||||
|
||||
if (this.state.mapClicked) {
|
||||
leftClass = 'left-hidden';
|
||||
rightClass = 'right-shown';
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<a className="hamburger-menu">K</a>
|
||||
<span className="title">Kiwi</span>
|
||||
<input
|
||||
id="gmaps-places-input"
|
||||
placeholder="Unesite adresu, naselje ili grad"
|
||||
className="where-to"
|
||||
type="text"></input>
|
||||
<div className="view-types">
|
||||
<a className="view-type-left">
|
||||
<i className="btn-select-map fa fa-list"></i>
|
||||
</a>
|
||||
<a className="view-type-right">
|
||||
<i className="view-type-map-icon fa fa-map-marker"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right" style={rightStyle} className={rightClass}>
|
||||
{this.renderRightContent()}
|
||||
</div>
|
||||
|
||||
<div id="left" style={leftStyle} className={leftClass}>
|
||||
<div id="map" ref="map">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default Main;
|
||||
@@ -1,29 +0,0 @@
|
||||
export const pacSelectFirst = (input) => {
|
||||
// store the original event binding function
|
||||
var _addEventListener = (input.addEventListener) ? input.addEventListener : input.attachEvent;
|
||||
|
||||
function addEventListenerWrapper(type, listener) {
|
||||
// Simulate a 'down arrow' keypress on hitting 'return' when no pac suggestion is selected,
|
||||
// and then trigger the original listener.
|
||||
if (type == "keydown") {
|
||||
var orig_listener = listener;
|
||||
listener = function(event) {
|
||||
var suggestion_selected = $(".pac-item-selected").length > 0;
|
||||
if (event.which == 13 && !suggestion_selected) {
|
||||
var simulated_downarrow = $.Event("keydown", {
|
||||
keyCode: 40,
|
||||
which: 40
|
||||
});
|
||||
orig_listener.apply(input, [simulated_downarrow]);
|
||||
}
|
||||
|
||||
orig_listener.apply(input, [event]);
|
||||
};
|
||||
}
|
||||
|
||||
_addEventListener.apply(input, [type, listener]);
|
||||
}
|
||||
|
||||
input.addEventListener = addEventListenerWrapper;
|
||||
input.attachEvent = addEventListenerWrapper;
|
||||
}
|
||||
59
web/index.js
59
web/index.js
@@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import Main from './components/Main';
|
||||
|
||||
const getInitialState = (url) => {
|
||||
console.log('PARSING URL:', url);
|
||||
const params = window.location.search.substr(1).split("&");
|
||||
|
||||
const initialState = {
|
||||
rooms: {},
|
||||
category: {}
|
||||
}
|
||||
|
||||
for(const param of params) {
|
||||
const [key, value] = param.split("=");
|
||||
console.log('analyzing param ', key, value);
|
||||
if (key === "rooms" && value !== '') {
|
||||
|
||||
console.log("IT's ROOMS");
|
||||
value.split(",").forEach(k => {
|
||||
console.log("IT's ROOMS", k);
|
||||
initialState.rooms[parseInt(k)] = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (key === "category" && value !== '') {
|
||||
value.split(",").forEach(k => {
|
||||
initialState.category[parseInt(k)] = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (key === "sort") {
|
||||
initialState.sort = value;
|
||||
}
|
||||
|
||||
if (key === "bounds") {
|
||||
initialState.bounds = value;
|
||||
}
|
||||
|
||||
if (key === "listingId") {
|
||||
initialState.listingId = value;
|
||||
}
|
||||
|
||||
if (key === "zoom") {
|
||||
initialState.zoom = parseInt(value);
|
||||
}
|
||||
|
||||
if (["minSize", "maxSize", "minPrice", "maxPrice"].includes(key)) {
|
||||
initialState[key] = parseFloat(value);
|
||||
}
|
||||
}
|
||||
|
||||
return initialState;
|
||||
}
|
||||
|
||||
const main = (<Main initialState={getInitialState(window.location)}/>);
|
||||
|
||||
render(main, document.getElementById('root'));
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import fetch from 'isomorphic-fetch';
|
||||
|
||||
export const loadListing = (id) => {
|
||||
let url = `http://localhost:3001/api/search/listings/${id}`;
|
||||
|
||||
return fetch(url, {
|
||||
//credentials: 'include'
|
||||
});
|
||||
};
|
||||
|
||||
export const loadProperties = ({
|
||||
bounds,
|
||||
minPrice = '',
|
||||
maxPrice = '',
|
||||
minSize = '',
|
||||
maxSize = '',
|
||||
rooms = {},
|
||||
category = {},
|
||||
page = 1,
|
||||
pins = false,
|
||||
sort = ''
|
||||
}) => {
|
||||
const allRooms = Object
|
||||
.keys(rooms)
|
||||
.filter((v) => rooms[v])
|
||||
.join(',');
|
||||
|
||||
const allCategories = Object
|
||||
.keys(category)
|
||||
.filter((v) => category[v])
|
||||
.join(',');
|
||||
|
||||
// TODO: handle errors
|
||||
//return fetch(process.env.API_URL + '/api/search', {
|
||||
let url = `http://localhost:3001/api/search/listings?bounds=${bounds}&minPrice=${minPrice}&maxPrice=${maxPrice}&rooms=${allRooms}&minSize=${minSize}&maxSize=${maxSize}&category=${allCategories}&page=${page}&pins=${pins}&sort=${sort}`
|
||||
|
||||
return fetch(url, {
|
||||
//credentials: 'include'
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const markSeen = (id) => {
|
||||
const seen = JSON.parse(window.localStorage.getItem('seen') || '[]');
|
||||
seen.push(id);
|
||||
window.localStorage.setItem('seen', JSON.stringify(seen));
|
||||
}
|
||||
|
||||
export const loadSeen = (id) => {
|
||||
const seen = JSON.parse(window.localStorage.getItem('seen') || '[]');
|
||||
return seen;
|
||||
//return seen.findIndex(s => s === id) !== -1;
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
import { markSeen } from './api';
|
||||
|
||||
const setMaxPrice = ({ type, action }, component) => {
|
||||
const maxPrice = parseFloat(action.maxPrice);
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
maxPrice: isNaN(maxPrice) ? undefined : maxPrice,
|
||||
priceDirty: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setMinPrice = ({ type, action }, component) => {
|
||||
const minPrice = parseFloat(action.minPrice);
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
minPrice: isNaN(minPrice) ? undefined : minPrice,
|
||||
priceDirty: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setMinSize = ({ type, action }, component) => {
|
||||
const minSize = parseFloat(action.minSize);
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
minSize: isNaN(minSize) ? undefined : minSize,
|
||||
sizeDirty: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setMaxSize = ({ type, action }, component) => {
|
||||
const maxSize = parseFloat(action.maxSize);
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
maxSize: isNaN(maxSize) ? undefined : maxSize,
|
||||
sizeDirty: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const viewListingDetails = ({ type, action }, component) => {
|
||||
const scrollElem = document.querySelector('.right-content');
|
||||
component.savedScrollTop = scrollElem.scrollTop;
|
||||
|
||||
//component.router.listingId = action.id;
|
||||
|
||||
component.setState({
|
||||
listingDetails: true,
|
||||
listingId: action.id,
|
||||
descriptionExpanded: false,
|
||||
imageIndex: 0,
|
||||
listing: action.listing
|
||||
}, () => {
|
||||
//component.router.update();
|
||||
markSeen(action.id);
|
||||
const m = component.findMarker(action.id);
|
||||
if (m) {
|
||||
m.marker.setIcon(component.selectedMarkerIcon());
|
||||
}
|
||||
|
||||
scrollElem.scrollTop = 0
|
||||
});
|
||||
};
|
||||
|
||||
const listingsLoaded = ({ type, action }, component) => {
|
||||
const currentListings = new Map();
|
||||
|
||||
for (const listing of action.listings) {
|
||||
currentListings.set(listing._id, listing);
|
||||
}
|
||||
|
||||
component.setState({
|
||||
listings: action.more ? (new Map([...component.state.listings, ...currentListings])) : currentListings,
|
||||
loadingMore: false,
|
||||
totalCount: action.totalCount
|
||||
});
|
||||
};
|
||||
|
||||
const pinsLoaded = ({ type, action }, component) => {
|
||||
component.setState({
|
||||
}, () => {
|
||||
component.markers = action.newMarkers;
|
||||
});
|
||||
};
|
||||
|
||||
const expandDescription = ({ type, action }, component) => {
|
||||
component.setState({
|
||||
descriptionExpanded: true
|
||||
});
|
||||
};
|
||||
|
||||
const prevImage = ({ type, action }, component) => {
|
||||
const index = component.state.imageIndex;
|
||||
if (index > 0) {
|
||||
component.setState({
|
||||
imageIndex: index - 1
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const nextImage = ({ type, action }, component) => {
|
||||
const index = component.state.imageIndex;
|
||||
component.setState({
|
||||
imageIndex: index + 1
|
||||
});
|
||||
};
|
||||
|
||||
const viewImage = ({ type, action }, component) => {
|
||||
component.setState({
|
||||
imageIndex: action.index
|
||||
});
|
||||
};
|
||||
|
||||
const searchPlaceChanged = ({ type, action }, component) => {
|
||||
component.setState({
|
||||
listingDetails: false,
|
||||
page: 0
|
||||
});
|
||||
};
|
||||
|
||||
const setRooms = ({ type, action }, component) => {
|
||||
const prevRooms = component.state.filters.rooms || {};
|
||||
|
||||
component.setState(
|
||||
{
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
rooms: {
|
||||
...prevRooms,
|
||||
[action.rooms]: !prevRooms[action.rooms]
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const updateSearch = ({ type, action }, component) => {
|
||||
component.setState(
|
||||
{
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
sizeDirty: false,
|
||||
priceDirty: false
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setCategory = ({type, action}, component) => {
|
||||
|
||||
const prevCategory = component.state.filters.category || {};
|
||||
|
||||
component.setState(
|
||||
{
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
category: {
|
||||
...prevCategory,
|
||||
[action.category]: !prevCategory[action.category]
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onListingMouseOver = ({type, action}, component) => {
|
||||
const marker = component.findMarker(action.id);
|
||||
if (marker) {
|
||||
const seen = component.isSeen(action.id);
|
||||
if (seen) {
|
||||
marker.marker.setIcon(component.visitedHoveredMarkerIcon());
|
||||
} else {
|
||||
marker.marker.setIcon(component.hoveredMarkerIcon());
|
||||
}
|
||||
|
||||
marker.marker.setAnimation(google.maps.Animation.BOUNCE);
|
||||
setTimeout(() => {
|
||||
marker.marker.setAnimation(null);
|
||||
|
||||
if (seen) {
|
||||
marker.marker.setIcon(component.visitedMarkerIcon());
|
||||
} else {
|
||||
marker.marker.setIcon(component.defaultMarkerIcon());
|
||||
}
|
||||
} , 710);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const backToResults = ({type, action}, component) => {
|
||||
const prevSelected = component.findMarker(component.state.listingId);
|
||||
component.setState({
|
||||
listingId: null,
|
||||
listingDetails: false
|
||||
}, () => {
|
||||
//component.router.update();
|
||||
|
||||
if (prevSelected) {
|
||||
prevSelected.marker.setIcon(component.visitedMarkerIcon());
|
||||
}
|
||||
|
||||
const scrollElem = document.querySelector('.right-content');
|
||||
scrollElem.scrollTop = component.savedScrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
const loadMoreListings = ({type, action}, component) => {
|
||||
const currentPage = component.state.page;
|
||||
if (currentPage * 20 < component.state.totalCount) {
|
||||
component.setState({
|
||||
loadingMore: true,
|
||||
page: currentPage + 1
|
||||
}, () => {
|
||||
component.refreshListings(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapIdle = ({type, action}, component) => {
|
||||
component.setState({
|
||||
page: 0
|
||||
}, () => {
|
||||
const scrollElem = document.querySelector('.right-content');
|
||||
scrollElem.scrollTop = 0;
|
||||
component.refreshListings();
|
||||
})
|
||||
}
|
||||
|
||||
const sortChange = ({type, action}, component) => {
|
||||
component.setState({
|
||||
sort: action.sort,
|
||||
page: 0
|
||||
}, () => {
|
||||
//component.router.update();
|
||||
component.refreshListings();
|
||||
});
|
||||
}
|
||||
|
||||
const updateRoute = ({type, action}, component) => {
|
||||
component.router.update(action);
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
SET_MIN_PRICE: setMinPrice,
|
||||
SET_MAX_PRICE: setMaxPrice,
|
||||
SET_MIN_SIZE: setMinSize,
|
||||
SET_MAX_SIZE: setMaxSize,
|
||||
LISTINGS_LOADED: listingsLoaded,
|
||||
EXPAND_DESCRIPTION: expandDescription,
|
||||
PREV_IMAGE: prevImage,
|
||||
NEXT_IMAGE: nextImage,
|
||||
VIEW_IMAGE: viewImage,
|
||||
SEARCH_PLACE_CHANGED: searchPlaceChanged,
|
||||
SET_ROOMS: setRooms,
|
||||
VIEW_LISTING_DETAILS: viewListingDetails,
|
||||
UPDATE_SEARCH: updateSearch,
|
||||
SET_CATEGORY: setCategory,
|
||||
ON_LISTING_MOUSE_OVER: onListingMouseOver,
|
||||
BACK_TO_RESULTS: backToResults,
|
||||
LOAD_MORE_LISTINGS: loadMoreListings,
|
||||
MAP_IDLE: mapIdle,
|
||||
PINS_LOADED: pinsLoaded,
|
||||
SORT_CHANGE: sortChange,
|
||||
UPDATE_ROUTE: updateRoute
|
||||
};
|
||||
|
||||
export const handleMessage = ({ type, action }, component) => {
|
||||
if (!handlers[type]) {
|
||||
throw new `Unhandled message: ${type}`();
|
||||
}
|
||||
|
||||
return handlers[type]({ type, action }, component);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
export const formatPrice = (p) => {
|
||||
if (isNaN(p)) {
|
||||
return 'Po dogovoru'
|
||||
}
|
||||
|
||||
return p.toLocaleString('bs') + ' KM';
|
||||
}
|
||||
|
||||
export const formatFilterNumber = (num) => {
|
||||
if (isNaN(num) || num == null) {
|
||||
return ''
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
export const galleryImageUrl = (img) =>
|
||||
img && img.replace("upload/", "upload/w_500/")
|
||||
|
||||
|
||||
export const listingImageUrl = (img) =>
|
||||
img && img.replace("upload/", "upload/w_205/")
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import clone from 'lodash.clonedeep';
|
||||
|
||||
export default class Router {
|
||||
constructor(comp, initialState) {
|
||||
this.component = comp;
|
||||
this.state = clone(initialState) || {};
|
||||
|
||||
|
||||
window.onpopstate = (event) => {
|
||||
const state = event.state;
|
||||
if (state) {
|
||||
if (state.toDispatch) {
|
||||
this.component.dispatch(state.toDispatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update (state) {
|
||||
const params = [];
|
||||
if (state.params) {
|
||||
|
||||
let cloned = clone(state);
|
||||
|
||||
if (cloned.params.rooms != null) {
|
||||
this.state.rooms[cloned.params.rooms] = !this.state.rooms[cloned.params.rooms];
|
||||
}
|
||||
|
||||
if (cloned.params.category != null) {
|
||||
this.state.category[cloned.params.category] = !this.state.category[cloned.params.category];
|
||||
}
|
||||
|
||||
delete cloned.params['rooms'];
|
||||
delete cloned.params['category'];
|
||||
|
||||
this.state = Object.assign(this.state, cloned.params);
|
||||
|
||||
const {
|
||||
listingId,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
minSize,
|
||||
maxSize,
|
||||
bounds,
|
||||
sort,
|
||||
rooms = {},
|
||||
category = {},
|
||||
zoom
|
||||
} = this.state;
|
||||
|
||||
if (listingId) {
|
||||
params.push(`listingId=${listingId}`);
|
||||
}
|
||||
|
||||
params.push(`sort=${sort}`);
|
||||
params.push(`bounds=${bounds}`);
|
||||
params.push(`zoom=${zoom}`);
|
||||
if (maxPrice) {
|
||||
params.push(`maxPrice=${maxPrice}`);
|
||||
}
|
||||
|
||||
if (minPrice) {
|
||||
params.push(`minPrice=${minPrice}`);
|
||||
}
|
||||
|
||||
if (minSize) {
|
||||
params.push(`minSize=${minSize}`);
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
params.push(`maxSize=${maxSize}`);
|
||||
}
|
||||
params.push(`rooms=${Object.keys(rooms).filter(v => rooms[v]).join(",")}`);
|
||||
params.push(`category=${Object.keys(category).filter(v => category[v]).join(",")}`);
|
||||
}
|
||||
|
||||
if (state.toDispatch) {
|
||||
window.history.pushState(state, '', `/?${params.join("&")}`);
|
||||
} else {
|
||||
const oldState = window.history.state;
|
||||
if (oldState) {
|
||||
const newState = Object.assign(oldState, state);
|
||||
window.history.replaceState(newState, '',`/?${params.join("&")}`);
|
||||
} else {
|
||||
|
||||
window.history.replaceState(state, '',`/?${params.join("&")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --content-base ./dist --hot --inline --host 0.0.0.0",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"format": "prettier-standard 'src/**/*.js'"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -21,7 +22,9 @@
|
||||
"babel-loader": "^6.2.7",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"eslint": "^3.19.0",
|
||||
"prettier": "^0.22.0",
|
||||
"prettier-standard": "^3.0.1",
|
||||
"webpack": "^1.13.3",
|
||||
"webpack-dev-server": "^1.16.2"
|
||||
}
|
||||
|
||||
@@ -1,81 +1,85 @@
|
||||
import React from "react";
|
||||
import { formatFilterNumber } from "../lib/helpers";
|
||||
import React from 'react'
|
||||
import {formatFilterNumber} from '../lib/helpers'
|
||||
import {
|
||||
CATEGORY_FLAT,
|
||||
CATEGORY_HOUSE,
|
||||
CATEGORY_OFFICE,
|
||||
CATEGORY_LAND
|
||||
} from '../../crawler/enums';
|
||||
} from '../../../crawler/enums'
|
||||
|
||||
export default class Filters extends React.Component {
|
||||
onCloseClick(e) {
|
||||
onCloseClick (e) {
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
this.props.onClose()
|
||||
}
|
||||
}
|
||||
|
||||
onMaxPriceChange(e) {
|
||||
const maxPrice = e.target.value;
|
||||
onMaxPriceChange (e) {
|
||||
const maxPrice = e.target.value
|
||||
|
||||
this.props.dispatch({
|
||||
type: "SET_MAX_PRICE",
|
||||
type: 'SET_MAX_PRICE',
|
||||
action: {maxPrice}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
onMinPriceChange(e) {
|
||||
const minPrice = e.target.value;
|
||||
onMinPriceChange (e) {
|
||||
const minPrice = e.target.value
|
||||
|
||||
this.props.dispatch({
|
||||
type: "SET_MIN_PRICE",
|
||||
type: 'SET_MIN_PRICE',
|
||||
action: {minPrice}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
onMaxSizeChange(e) {
|
||||
onMaxSizeChange (e) {
|
||||
this.props.dispatch({
|
||||
type: "SET_MAX_SIZE",
|
||||
action: { maxSize: e.target.value }
|
||||
});
|
||||
type: 'SET_MAX_SIZE',
|
||||
action: {maxSize: e.target.value}
|
||||
})
|
||||
}
|
||||
|
||||
onMinSizeChange(e) {
|
||||
onMinSizeChange (e) {
|
||||
this.props.dispatch({
|
||||
type: "SET_MIN_SIZE",
|
||||
action: { minSize: e.target.value }
|
||||
});
|
||||
type: 'SET_MIN_SIZE',
|
||||
action: {minSize: e.target.value}
|
||||
})
|
||||
}
|
||||
|
||||
onRoomsClick(rooms) {
|
||||
this.props.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
onRoomsClick (rooms) {
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
params: {rooms}
|
||||
}});
|
||||
}
|
||||
})
|
||||
|
||||
this.props.dispatch({type: 'SET_ROOMS', action: {rooms}});
|
||||
this.props.dispatch({type: 'SET_ROOMS', action: {rooms}})
|
||||
}
|
||||
|
||||
onCategoryClick(category) {
|
||||
|
||||
this.props.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
onCategoryClick (category) {
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
params: {category}
|
||||
}});
|
||||
}
|
||||
})
|
||||
|
||||
this.props.dispatch({type: 'SET_CATEGORY', action: {category}});
|
||||
this.props.dispatch({type: 'SET_CATEGORY', action: {category}})
|
||||
}
|
||||
|
||||
onRefreshClick() {
|
||||
|
||||
this.updateSearch();
|
||||
onRefreshClick () {
|
||||
this.updateSearch()
|
||||
}
|
||||
|
||||
onKeyPress (e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.updateSearch();
|
||||
this.updateSearch()
|
||||
}
|
||||
}
|
||||
|
||||
updateSearch () {
|
||||
const {minPrice, maxPrice, minSize, maxSize} = this.props.filters;
|
||||
const {minPrice, maxPrice, minSize, maxSize} = this.props.filters
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
@@ -87,14 +91,14 @@ export default class Filters extends React.Component {
|
||||
maxSize
|
||||
}
|
||||
}
|
||||
});
|
||||
this.props.dispatch({ type: "UPDATE_SEARCH" });
|
||||
})
|
||||
this.props.dispatch({type: 'UPDATE_SEARCH'})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filters } = this.props;
|
||||
const selectedRooms = val => filters.rooms[val] ? "selected" : "";
|
||||
const selectedCategory = val => filters.category[val] ? "selected": "";
|
||||
render () {
|
||||
const {filters} = this.props
|
||||
const selectedRooms = val => filters.rooms[val] ? 'selected' : ''
|
||||
const selectedCategory = val => filters.category[val] ? 'selected' : ''
|
||||
|
||||
return (
|
||||
<div className="filters">
|
||||
@@ -119,7 +123,7 @@ export default class Filters extends React.Component {
|
||||
onChange={this.onMinPriceChange.bind(this)}
|
||||
value={formatFilterNumber(filters.minPrice)}
|
||||
/>
|
||||
{" "}
|
||||
{' '}
|
||||
DO
|
||||
|
||||
<input
|
||||
@@ -128,13 +132,11 @@ export default class Filters extends React.Component {
|
||||
value={formatFilterNumber(filters.maxPrice)}
|
||||
/>
|
||||
{this.props.filters.priceDirty
|
||||
?
|
||||
<i
|
||||
? <i
|
||||
onClick={this.onRefreshClick.bind(this)}
|
||||
className="fa fa-refresh fa-refresh-custom"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
|
||||
aria-hidden="true"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,24 +147,36 @@ export default class Filters extends React.Component {
|
||||
<div className="filter-content">
|
||||
<div
|
||||
onClick={this.onCategoryClick.bind(this, CATEGORY_FLAT)}
|
||||
className={`filter-btn property-type-btn ${selectedCategory(CATEGORY_FLAT)}`}>
|
||||
className={
|
||||
`filter-btn property-type-btn ${selectedCategory(CATEGORY_FLAT)}`
|
||||
}
|
||||
>
|
||||
Stan
|
||||
</div>
|
||||
<div
|
||||
onClick={this.onCategoryClick.bind(this, CATEGORY_HOUSE)}
|
||||
className={`filter-btn property-type-btn ${selectedCategory(CATEGORY_HOUSE)}`}>
|
||||
className={
|
||||
`filter-btn property-type-btn ${selectedCategory(CATEGORY_HOUSE)}`
|
||||
}
|
||||
>
|
||||
Kuća
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-content">
|
||||
<div
|
||||
onClick={this.onCategoryClick.bind(this, CATEGORY_LAND)}
|
||||
className={`filter-btn property-type-btn ${selectedCategory(CATEGORY_LAND)}`}>
|
||||
className={
|
||||
`filter-btn property-type-btn ${selectedCategory(CATEGORY_LAND)}`
|
||||
}
|
||||
>
|
||||
Zemljište
|
||||
</div>
|
||||
<div
|
||||
onClick={this.onCategoryClick.bind(this, CATEGORY_OFFICE)}
|
||||
className={`filter-btn property-type-btn ${selectedCategory(CATEGORY_OFFICE)}`}>
|
||||
className={
|
||||
`filter-btn property-type-btn ${selectedCategory(CATEGORY_OFFICE)}`
|
||||
}
|
||||
>
|
||||
Poslovni prostor
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +194,7 @@ export default class Filters extends React.Component {
|
||||
/>
|
||||
|
||||
DO
|
||||
{" "}
|
||||
{' '}
|
||||
<input
|
||||
onKeyPress={this.onKeyPress.bind(this)}
|
||||
value={formatFilterNumber(filters.maxSize)}
|
||||
@@ -190,8 +204,8 @@ export default class Filters extends React.Component {
|
||||
? <i
|
||||
onClick={this.onRefreshClick.bind(this)}
|
||||
className="fa fa-refresh fa-refresh-custom"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
aria-hidden="true"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,8 +235,8 @@ export default class Filters extends React.Component {
|
||||
3
|
||||
</div>
|
||||
<div
|
||||
onClick={this.onRoomsClick.bind(this, "4+")}
|
||||
className={`filter-btn property-rooms-btn ${selectedRooms("4+")}`}
|
||||
onClick={this.onRoomsClick.bind(this, '4+')}
|
||||
className={`filter-btn property-rooms-btn ${selectedRooms('4+')}`}
|
||||
>
|
||||
4+
|
||||
</div>
|
||||
@@ -237,6 +251,6 @@ export default class Filters extends React.Component {
|
||||
<div className="clear-both" />
|
||||
<div className="filter-bottom" />
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
87
web/src/components/Gallery.js
Normal file
87
web/src/components/Gallery.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import {galleryImageUrl} from '../lib/helpers'
|
||||
|
||||
export default class Gallery extends React.Component {
|
||||
onPrevClick (e) {
|
||||
this.props.dispatch({type: 'PREV_IMAGE'})
|
||||
}
|
||||
|
||||
onNextClick (e) {
|
||||
this.props.dispatch({type: 'NEXT_IMAGE'})
|
||||
}
|
||||
|
||||
onImageDotClick (index, e) {
|
||||
this.props.dispatch({type: 'VIEW_IMAGE', action: {index}})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {images, imageIndex} = this.props
|
||||
if (!images || images.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const showPrev = imageIndex > 0
|
||||
const showNext = imageIndex < images.length - 1
|
||||
|
||||
return (
|
||||
<div className="ld-image-container">
|
||||
<img src={galleryImageUrl(images[imageIndex])} />
|
||||
{showPrev
|
||||
? <div className="prev-button" onClick={this.onPrevClick.bind(this)}>
|
||||
<div>
|
||||
<svg
|
||||
fill="white"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M213.7,256L213.7,256L213.7,256L380.9,81.9c4.2-4.3,4.1-11.4-0.2-15.8l-29.9-30.6c-4.3-4.4-11.3-4.5-15.5-0.2L131.1,247.9 c-2.2,2.2-3.2,5.2-3,8.1c-0.1,3,0.9,5.9,3,8.1l204.2,212.7c4.2,4.3,11.2,4.2,15.5-0.2l29.9-30.6c4.3-4.4,4.4-11.5,0.2-15.8 L213.7,256z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
: null}
|
||||
{showNext
|
||||
? <div className="next-button" onClick={this.onNextClick.bind(this)}>
|
||||
<div>
|
||||
<svg
|
||||
fill="white"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
d="M298.3,256L298.3,256L298.3,256L131.1,81.9c-4.2-4.3-4.1-11.4,0.2-15.8l29.9-30.6c4.3-4.4,11.3-4.5,15.5-0.2l204.2,212.7 c2.2,2.2,3.2,5.2,3,8.1c0.1,3-0.9,5.9-3,8.1L176.7,476.8c-4.2,4.3-11.2,4.2-15.5-0.2L131.3,446c-4.3-4.4-4.4-11.5-0.2-15.8 L298.3,256z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div className="image-dots">
|
||||
{images.map((img, index) => {
|
||||
let cls = 'image-dot'
|
||||
if (index === imageIndex) {
|
||||
cls += ' selected'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={img}
|
||||
onClick={this.onImageDotClick.bind(this, index)}
|
||||
className={cls}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,57 @@
|
||||
import React from 'react';
|
||||
import Gallery from './gallery';
|
||||
import {formatPrice} from '../lib/helpers';
|
||||
import React from 'react'
|
||||
import Gallery from './gallery'
|
||||
import {formatPrice} from '../lib/helpers'
|
||||
|
||||
export default class ListingDetails extends React.Component {
|
||||
onReadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch({type: 'EXPAND_DESCRIPTION'});
|
||||
e.preventDefault()
|
||||
this.props.dispatch({type: 'EXPAND_DESCRIPTION'})
|
||||
}
|
||||
|
||||
onBackClick() {
|
||||
this.props.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
onBackClick () {
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
toDispatch: {
|
||||
type: 'BACK_TO_RESULTS'
|
||||
},
|
||||
params: {
|
||||
listingId: null
|
||||
}
|
||||
}});
|
||||
this.props.dispatch({type: 'BACK_TO_RESULTS'});
|
||||
}
|
||||
})
|
||||
this.props.dispatch({type: 'BACK_TO_RESULTS'})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {listing, descriptionExpanded} = this.props;
|
||||
render () {
|
||||
const {listing, descriptionExpanded} = this.props
|
||||
if (!listing) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const descriptionClasses = descriptionExpanded ? "ld-description expanded" : "ld-description";
|
||||
const images = listing.images.map((image) => ({original: image, thumbnail: image}))
|
||||
const descriptionClasses = descriptionExpanded
|
||||
? 'ld-description expanded'
|
||||
: 'ld-description'
|
||||
const images = listing.images.map(image => ({
|
||||
original: image,
|
||||
thumbnail: image
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="ld-container">
|
||||
<div className="ld-header">
|
||||
<div className="back-to-results">
|
||||
<button className="back-to-results-btn"
|
||||
onClick={this.onBackClick.bind(this)}>
|
||||
<i className="fa fa-arrow-left" aria-hidden="true"></i>
|
||||
<button
|
||||
className="back-to-results-btn"
|
||||
onClick={this.onBackClick.bind(this)}
|
||||
>
|
||||
<i className="fa fa-arrow-left" aria-hidden="true" />
|
||||
<span>
|
||||
Nazad na rezultate
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<button className="hide-listing">
|
||||
<i className="fa fa-thumbs-o-down" aria-hidden="true"></i>
|
||||
<i className="fa fa-thumbs-o-down" aria-hidden="true" />
|
||||
<span>
|
||||
Sakrij
|
||||
</span>
|
||||
@@ -52,7 +62,8 @@ export default class ListingDetails extends React.Component {
|
||||
<Gallery
|
||||
dispatch={this.props.dispatch}
|
||||
images={listing.images}
|
||||
imageIndex={this.props.imageIndex} />
|
||||
imageIndex={this.props.imageIndex}
|
||||
/>
|
||||
<div className="ld-price-address-box">
|
||||
<div className="ld-price">
|
||||
{formatPrice(listing.price)}
|
||||
@@ -66,19 +77,19 @@ export default class ListingDetails extends React.Component {
|
||||
|
||||
<div className="ld-features">
|
||||
<div className="ld-feature-box">
|
||||
<i className="fa fa-bed"></i>
|
||||
<i className="fa fa-bed" />
|
||||
{listing.rooms} sobe
|
||||
</div>
|
||||
<div className="ld-feature-box">
|
||||
<i className="fa fa-home"></i>
|
||||
<i className="fa fa-home" />
|
||||
{listing.size}m2
|
||||
</div>
|
||||
<div className="ld-feature-box">
|
||||
<i className="fa fa-home"></i>
|
||||
<i className="fa fa-home" />
|
||||
{listing.floor}. sprat
|
||||
</div>
|
||||
<div className="ld-feature-box">
|
||||
<i className="fa fa-home"></i>
|
||||
<i className="fa fa-home" />
|
||||
--
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,15 +100,15 @@ export default class ListingDetails extends React.Component {
|
||||
{listing.longDescription}
|
||||
</div>
|
||||
{!descriptionExpanded
|
||||
?
|
||||
<div className="ld-read-more">
|
||||
<a href="" onClick={this.onReadMore.bind(this)}>Pročitajte više</a>
|
||||
? <div className="ld-read-more">
|
||||
<a href="" onClick={this.onReadMore.bind(this)}>
|
||||
Pročitajte više
|
||||
</a>
|
||||
</div>
|
||||
: null}
|
||||
<div className="ld-footer">
|
||||
<div className="ld-footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import React from 'react';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {formatPrice, listingImageUrl} from '../lib/helpers';
|
||||
import {loadListing} from '../lib/api';
|
||||
import React from 'react'
|
||||
import {findDOMNode} from 'react-dom'
|
||||
import {formatPrice, listingImageUrl} from '../lib/helpers'
|
||||
import {loadListing} from '../lib/api'
|
||||
|
||||
export default class Listings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.handleScroll = (e) => {
|
||||
const node = e.target;
|
||||
const offset = node.scrollHeight - node.scrollTop - node.clientHeight;
|
||||
this.handleScroll = e => {
|
||||
const node = e.target
|
||||
const offset = node.scrollHeight - node.scrollTop - node.clientHeight
|
||||
|
||||
if (this.props && this.props.loadingMore) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (offset < 50) {
|
||||
this.props.dispatch({type: 'LOAD_MORE_LISTINGS'});
|
||||
this.props.dispatch({type: 'LOAD_MORE_LISTINGS'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onListingClick(id) {
|
||||
onListingClick (id) {
|
||||
loadListing(id).then(l => l.text()).then(l => {
|
||||
this.props.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
toDispatch: {
|
||||
type: 'VIEW_LISTING_DETAILS', action: {
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
@@ -33,13 +36,17 @@ export default class Listings extends React.Component {
|
||||
params: {
|
||||
listingId: id
|
||||
}
|
||||
}});
|
||||
}
|
||||
})
|
||||
|
||||
this.props.dispatch({type: 'VIEW_LISTING_DETAILS', action: {
|
||||
this.props.dispatch({
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id,
|
||||
listing: JSON.parse(l)
|
||||
}});
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMouseEnter (id) {
|
||||
@@ -48,40 +55,46 @@ export default class Listings extends React.Component {
|
||||
action: {
|
||||
id
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
renderListings () {
|
||||
const {listings = (new Map())} = this.props;
|
||||
const {listings = new Map()} = this.props
|
||||
|
||||
if (listings.size === 0) {
|
||||
return (<div className="listings-no-results">
|
||||
<i className="fa fa-frown-o fa-frown-o-custom" aria-hidden="true"></i>
|
||||
return (
|
||||
<div className="listings-no-results">
|
||||
<i className="fa fa-frown-o fa-frown-o-custom" aria-hidden="true" />
|
||||
|
||||
<h4>Nema rezultata</h4>
|
||||
<h5>
|
||||
Nema oglasa koji ispunjavaju vaše uslove pretrage.
|
||||
</h5>
|
||||
</div>)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const rendered = [];
|
||||
const rendered = []
|
||||
|
||||
for(const l of listings.values()) {
|
||||
const {images} = l;
|
||||
for (const l of listings.values()) {
|
||||
const {images} = l
|
||||
|
||||
rendered.push(
|
||||
<div
|
||||
key={l._id}
|
||||
onMouseEnter={this.onMouseEnter.bind(this, l._id)}
|
||||
className="property-list-item"
|
||||
onClick={this.onListingClick.bind(this, l._id)}>
|
||||
onClick={this.onListingClick.bind(this, l._id)}
|
||||
>
|
||||
<div className="pli-image">
|
||||
<img src={listingImageUrl(images[0])} alt=""></img>
|
||||
<img src={listingImageUrl(images[0])} alt="" />
|
||||
</div>
|
||||
<div className="pli-details">
|
||||
<div className="price">{formatPrice(l.price)}</div>
|
||||
<div className="description">{l.rooms ? `${l.rooms} sobe, `: null}{l.size ? `${l.size}m2`: null}</div>
|
||||
<div className="description">
|
||||
{l.rooms ? `${l.rooms} sobe, ` : null}
|
||||
{l.size ? `${l.size}m2` : null}
|
||||
</div>
|
||||
<div className="address">
|
||||
<div className="street">
|
||||
{l.address}
|
||||
@@ -93,46 +106,50 @@ export default class Listings extends React.Component {
|
||||
<div className="hours-ago">Prije {l.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return rendered;
|
||||
return rendered
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.attachScrollListener();
|
||||
this.attachScrollListener()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeScrollListener();
|
||||
this.removeScrollListener()
|
||||
}
|
||||
|
||||
attachScrollListener () {
|
||||
const listings = findDOMNode(this.refs.listings);
|
||||
listings.parentNode.addEventListener('scroll', this.handleScroll);
|
||||
const listings = findDOMNode(this.refs.listings)
|
||||
listings.parentNode.addEventListener('scroll', this.handleScroll)
|
||||
}
|
||||
|
||||
removeScrollListener () {
|
||||
const listings = findDOMNode(this.refs.listings);
|
||||
listings.parentNode.removeEventListener('scroll', this.handleScroll);
|
||||
const listings = findDOMNode(this.refs.listings)
|
||||
listings.parentNode.removeEventListener('scroll', this.handleScroll)
|
||||
}
|
||||
|
||||
onSortChange (e) {
|
||||
this.props.dispatch({type: 'UPDATE_ROUTE', action: {
|
||||
this.props.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
params: {
|
||||
sort: e.target.value
|
||||
}
|
||||
}});
|
||||
}
|
||||
})
|
||||
|
||||
this.props.dispatch({type: 'SORT_CHANGE', action: {
|
||||
this.props.dispatch({
|
||||
type: 'SORT_CHANGE',
|
||||
action: {
|
||||
sort: e.target.value
|
||||
}});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
const {listings = (new Map()), totalCount, sort} = this.props;
|
||||
const {listings = new Map(), totalCount, sort} = this.props
|
||||
|
||||
return (
|
||||
<div ref="listings" className="listings">
|
||||
@@ -143,7 +160,8 @@ export default class Listings extends React.Component {
|
||||
value={sort}
|
||||
onChange={this.onSortChange.bind(this)}
|
||||
name="listings-type"
|
||||
id="listings-type">
|
||||
id="listings-type"
|
||||
>
|
||||
<option value="relevance">Relevantno</option>
|
||||
<option value="newest">Najnovije</option>
|
||||
<option value="price-min">Cijena: od najmanje</option>
|
||||
@@ -159,6 +177,7 @@ export default class Listings extends React.Component {
|
||||
<div className="listings-items">
|
||||
{this.renderListings()}
|
||||
</div>
|
||||
</div>)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
592
web/src/components/Main.js
Normal file
592
web/src/components/Main.js
Normal file
@@ -0,0 +1,592 @@
|
||||
import React from 'react'
|
||||
import Filters from './Filters'
|
||||
import Listings from './Listings'
|
||||
import ListingDetails from './ListingDetails'
|
||||
import {pacSelectFirst} from '../helpers/googleMaps'
|
||||
import {loadProperties, loadSeen, loadListing} from '../lib/api'
|
||||
import {handleMessage} from '../lib/handlers'
|
||||
import Router from '../lib/router'
|
||||
|
||||
class Main extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const state = {
|
||||
listingDetails: false,
|
||||
listings: new Map(),
|
||||
imageIndex: 0,
|
||||
page: 0,
|
||||
sort: 'relevance',
|
||||
filters: {
|
||||
rooms: {},
|
||||
category: {}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.initialState) {
|
||||
props.initialState.sort = props.initialState.sort || state.sort
|
||||
state.filters.rooms = props.initialState.rooms
|
||||
state.filters.category = props.initialState.category
|
||||
state.sort = props.initialState.sort || state.sort
|
||||
state.listingId = props.initialState.listingId
|
||||
state.bounds = props.initialState.bounds
|
||||
state.zoom = props.initialState.zoom
|
||||
if (state.listingId) {
|
||||
state.listingDetails = true
|
||||
}
|
||||
state.filters.minSize = props.initialState.minSize
|
||||
state.filters.maxSize = props.initialState.maxSize
|
||||
state.filters.minPrice = props.initialState.minPrice
|
||||
state.filters.maxPrice = props.initialState.maxPrice
|
||||
}
|
||||
|
||||
this.state = state
|
||||
this.router = new Router(this, props.initialState)
|
||||
}
|
||||
|
||||
dispatch ({type, action = {}}) {
|
||||
handleMessage({type, action}, this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const uluru = {lat: 43.845031, lng: 18.4019262}
|
||||
const opts = {
|
||||
//zoom: 13,
|
||||
//center: uluru,
|
||||
streetViewControl: false,
|
||||
mapTypeControl: false
|
||||
}
|
||||
|
||||
if (!this.state.bounds) {
|
||||
opts.zoom = 13
|
||||
opts.center = uluru
|
||||
}
|
||||
|
||||
const map = new google.maps.Map(this.refs.map, opts)
|
||||
window.gmap = map
|
||||
|
||||
//const marker = new google.maps.Marker({
|
||||
//position: uluru,
|
||||
//map: map
|
||||
//});
|
||||
|
||||
var control = document.createElement('div')
|
||||
control.classList.add('filters-btn-toggle')
|
||||
control.innerHTML = '<button>Filters</button>'
|
||||
//control.style = "top: 200px;"
|
||||
control['style'] = 'top: 200px;'
|
||||
|
||||
var input = document.getElementById('gmaps-places-input')
|
||||
|
||||
pacSelectFirst(input)
|
||||
var options = {
|
||||
componentRestrictions: {country: 'BA'}
|
||||
}
|
||||
|
||||
const regularIdle = () => {
|
||||
this.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
params: {
|
||||
bounds: map.getBounds().toUrlValue(),
|
||||
zoom: map.getZoom()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.dispatch({type: 'MAP_IDLE'})
|
||||
}
|
||||
|
||||
// Check if initial bounds are passed-in
|
||||
|
||||
google.maps.event.addListenerOnce(map, 'idle', () => {
|
||||
if (this.state.bounds) {
|
||||
const [lat1, lng1, lat2, lng2] = this.state.bounds.split(',')
|
||||
const sw = new google.maps.LatLng({
|
||||
lat: parseFloat(lat1),
|
||||
lng: parseFloat(lng1)
|
||||
})
|
||||
const ne = new google.maps.LatLng({
|
||||
lat: parseFloat(lat2),
|
||||
lng: parseFloat(lng2)
|
||||
})
|
||||
|
||||
const initialBounds = new google.maps.LatLngBounds(sw, ne)
|
||||
//map.fitBounds(initialBounds);
|
||||
|
||||
const originalMaxZoom = map.maxZoom
|
||||
const originalMinZoom = map.minZoom
|
||||
map.setOptions({
|
||||
maxZoom: parseInt(this.state.zoom),
|
||||
minZoom: parseInt(this.state.zoom)
|
||||
})
|
||||
map.fitBounds(initialBounds)
|
||||
map.setOptions({maxZoom: originalMaxZoom, minZoom: originalMinZoom})
|
||||
}
|
||||
})
|
||||
map.addListener('idle', regularIdle)
|
||||
var searchBox = new google.maps.places.Autocomplete(input, options)
|
||||
|
||||
searchBox.addListener('place_changed', () => {
|
||||
var place = searchBox.getPlace()
|
||||
|
||||
if (!place.geometry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (place.geometry.viewport) {
|
||||
map.fitBounds(place.geometry.viewport)
|
||||
} else {
|
||||
map.setCenter(place.geometry.location)
|
||||
map.setZoom(18)
|
||||
}
|
||||
this.dispatch({type: 'SEARCH_PLACE_CHANGED'})
|
||||
})
|
||||
|
||||
control.addEventListener('click', e => {
|
||||
this.setState({
|
||||
mapClicked: true
|
||||
})
|
||||
})
|
||||
|
||||
control.index = 1
|
||||
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control)
|
||||
this.map = map
|
||||
|
||||
// TODO: if state contains listingId reload
|
||||
if (this.state.listingId) {
|
||||
loadListing(this.state.listingId).then(l => l.text()).then(l => {
|
||||
this.dispatch({
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id: this.state.listingId,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
removeAllMarkers () {
|
||||
if (this.markers) {
|
||||
this.markers.forEach(m => m.marker.setMap(null))
|
||||
}
|
||||
}
|
||||
|
||||
onCloseClick (e) {
|
||||
if (this.state.mapClicked) {
|
||||
setTimeout(
|
||||
() => {
|
||||
google.maps.event.trigger(this.map, 'resize')
|
||||
},
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mapClicked: false
|
||||
})
|
||||
}
|
||||
|
||||
findMarker (id) {
|
||||
if (!this.markers) {
|
||||
return null
|
||||
}
|
||||
|
||||
const index = this.markers.findIndex(m => m.id === id)
|
||||
return this.markers[index]
|
||||
}
|
||||
|
||||
isSeen (id) {
|
||||
const seen = loadSeen()
|
||||
return seen.findIndex(s => s === id) !== -1
|
||||
}
|
||||
|
||||
loadPins () {
|
||||
const map = this.map
|
||||
const {
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category
|
||||
} = this.state.filters
|
||||
|
||||
const bounds = map.getBounds()
|
||||
const properties = loadProperties({
|
||||
bounds: bounds.toUrlValue(),
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category,
|
||||
page: this.state.page,
|
||||
pins: true
|
||||
})
|
||||
|
||||
const markerExists = id => {
|
||||
return this.findMarker(id) != null
|
||||
}
|
||||
|
||||
properties
|
||||
.then(p => {
|
||||
return {
|
||||
body: p.text(),
|
||||
totalCount: p.headers.get('X-Total-Count')
|
||||
}
|
||||
})
|
||||
.then(({body, totalCount}) => {
|
||||
body.then(p => {
|
||||
const data = JSON.parse(p)
|
||||
|
||||
const listingExists = id => {
|
||||
return data.findIndex(l => l._id === id) !== -1
|
||||
}
|
||||
|
||||
const newMarkers = []
|
||||
|
||||
if (this.markers) {
|
||||
this.markers.forEach(m => {
|
||||
if (!listingExists(m.id)) {
|
||||
m.marker.setMap(null)
|
||||
} else {
|
||||
newMarkers.push(m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const [index, prop] of data.entries()) {
|
||||
const myLatLng = {lat: prop.loc[0], lng: prop.loc[1]}
|
||||
|
||||
if (!markerExists(prop._id)) {
|
||||
const marker = new google.maps.Marker({
|
||||
position: myLatLng,
|
||||
map: map,
|
||||
//title : prop.title,
|
||||
icon: this.isSeen(prop._id)
|
||||
? this.visitedMarkerIcon()
|
||||
: this.defaultMarkerIcon(),
|
||||
id: prop._id
|
||||
})
|
||||
|
||||
marker.addListener('mouseover', () => {
|
||||
if (marker.id !== this.state.listingId) {
|
||||
if (this.isSeen(marker.id)) {
|
||||
marker.setIcon(this.visitedHoveredMarkerIcon())
|
||||
} else {
|
||||
marker.setIcon(this.hoveredMarkerIcon())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
marker.addListener('mouseout', () => {
|
||||
if (marker.id !== this.state.listingId) {
|
||||
if (this.isSeen(marker.id)) {
|
||||
marker.setIcon(this.visitedMarkerIcon())
|
||||
} else {
|
||||
marker.setIcon(this.defaultMarkerIcon())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
marker.addListener('click', () => {
|
||||
// Maybe move out and call when popping state
|
||||
if (this.state.listingId) {
|
||||
const prevSelected = this.findMarker(this.state.listingId)
|
||||
if (prevSelected) {
|
||||
prevSelected.marker.setIcon(this.visitedMarkerIcon())
|
||||
}
|
||||
}
|
||||
|
||||
marker.setIcon(this.selectedMarkerIcon())
|
||||
|
||||
loadListing(prop._id).then(l => l.text()).then(l => {
|
||||
this.dispatch({
|
||||
type: 'UPDATE_ROUTE',
|
||||
action: {
|
||||
toDispatch: {
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id: prop._id,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
},
|
||||
params: {
|
||||
listingId: prop._id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//this.dispatch({type: 'UPDATE_ROUTE', action: {type: 'VIEW_LISTING_DETAILS', action: {
|
||||
//id: prop._id,
|
||||
//listing: JSON.parse(l)
|
||||
//}}});
|
||||
|
||||
this.dispatch({
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id: prop._id,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
newMarkers.push({
|
||||
marker,
|
||||
id: prop._id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
type: 'PINS_LOADED',
|
||||
action: {
|
||||
newMarkers
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Refreshes search
|
||||
*/
|
||||
refreshListings (more = false) {
|
||||
// TODO: move somewhere else
|
||||
|
||||
if (!more && this.state.listingId) {
|
||||
loadListing(this.state.listingId).then(l => l.text()).then(l => {
|
||||
this.dispatch({
|
||||
type: 'VIEW_LISTING_DETAILS',
|
||||
action: {
|
||||
id: this.state.listingId,
|
||||
listing: JSON.parse(l)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!more) {
|
||||
this.loadPins()
|
||||
}
|
||||
|
||||
const map = this.map
|
||||
const {
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category
|
||||
} = this.state.filters
|
||||
|
||||
const bounds = map.getBounds()
|
||||
const properties = loadProperties({
|
||||
bounds: bounds.toUrlValue(),
|
||||
rooms,
|
||||
minSize,
|
||||
maxSize,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
category,
|
||||
page: this.state.page,
|
||||
sort: this.state.sort
|
||||
})
|
||||
|
||||
properties
|
||||
.then(p => {
|
||||
return {
|
||||
body: p.text(),
|
||||
totalCount: p.headers.get('X-Total-Count')
|
||||
}
|
||||
})
|
||||
.then(({body, totalCount}) => {
|
||||
body.then(p => {
|
||||
const data = JSON.parse(p)
|
||||
|
||||
this.dispatch({
|
||||
type: 'LISTINGS_LOADED',
|
||||
action: {
|
||||
listings: data,
|
||||
more,
|
||||
totalCount
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Get default marker icon
|
||||
*/
|
||||
defaultMarkerIcon () {
|
||||
const sf = 0.5
|
||||
const width = 48
|
||||
const height = 64
|
||||
const icon = {
|
||||
url: 'static/images/pins_sprite.png',
|
||||
size: new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize: new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin: new google.maps.Point(0, 36 * sf)
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
/*
|
||||
* Get visited marker icon
|
||||
*/
|
||||
visitedMarkerIcon () {
|
||||
const sf = 0.5
|
||||
const width = 48
|
||||
const height = 64
|
||||
const icon = {
|
||||
url: 'static/images/pins_sprite.png',
|
||||
size: new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize: new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin: new google.maps.Point(152 * sf, 36 * sf)
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
/*
|
||||
* Visited hovered marker icon
|
||||
*/
|
||||
visitedHoveredMarkerIcon () {
|
||||
const sf = 0.5
|
||||
const width = 61
|
||||
const height = 82
|
||||
const icon = {
|
||||
url: 'static/images/pins_sprite.png',
|
||||
size: new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize: new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin: new google.maps.Point(480 * sf, 18 * sf)
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
/*
|
||||
* Hovered marker icon
|
||||
*/
|
||||
hoveredMarkerIcon () {
|
||||
const sf = 0.5
|
||||
const width = 61
|
||||
const height = 82
|
||||
const icon = {
|
||||
url: 'static/images/pins_sprite.png',
|
||||
size: new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize: new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin: new google.maps.Point(303 * sf, 18 * sf)
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
/*
|
||||
* Selected marker icon
|
||||
*/
|
||||
selectedMarkerIcon () {
|
||||
const sf = 0.5
|
||||
const width = 73
|
||||
const height = 100
|
||||
const icon = {
|
||||
url: 'static/images/pins_sprite.png',
|
||||
size: new google.maps.Size(width * sf, height * sf),
|
||||
scaledSize: new google.maps.Size(730 * sf, 102 * sf),
|
||||
origin: new google.maps.Point(655 * sf, 1 * sf)
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
|
||||
renderRightContent () {
|
||||
const children = []
|
||||
|
||||
if (this.state.listingDetails) {
|
||||
const listing = this.state.listing //this.state.listings.get(this.state.listingId);
|
||||
children.push(
|
||||
<ListingDetails
|
||||
listing={listing}
|
||||
imageIndex={this.state.imageIndex}
|
||||
dispatch={this.dispatch.bind(this)}
|
||||
descriptionExpanded={this.state.descriptionExpanded}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
children.push(
|
||||
<Filters
|
||||
filters={this.state.filters}
|
||||
dispatch={this.dispatch.bind(this)}
|
||||
onClose={this.onCloseClick.bind(this)}
|
||||
/>
|
||||
)
|
||||
children.push(
|
||||
<Listings
|
||||
sort={this.state.sort}
|
||||
totalCount={this.state.totalCount}
|
||||
loadingMore={this.state.loadingMore}
|
||||
listings={this.state.listings}
|
||||
dispatch={this.dispatch.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const content = (
|
||||
<div className="right-content">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
render () {
|
||||
const leftStyle = {}
|
||||
const rightStyle = {}
|
||||
const listingDetails = true
|
||||
|
||||
let leftClass = 'left-base'
|
||||
let rightClass = 'right-base'
|
||||
|
||||
if (this.state.mapClicked) {
|
||||
leftClass = 'left-hidden'
|
||||
rightClass = 'right-shown'
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<a className="hamburger-menu">K</a>
|
||||
<span className="title">Kiwi</span>
|
||||
<input
|
||||
id="gmaps-places-input"
|
||||
placeholder="Unesite adresu, naselje ili grad"
|
||||
className="where-to"
|
||||
type="text"
|
||||
/>
|
||||
<div className="view-types">
|
||||
<a className="view-type-left">
|
||||
<i className="btn-select-map fa fa-list" />
|
||||
</a>
|
||||
<a className="view-type-right">
|
||||
<i className="view-type-map-icon fa fa-map-marker" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right" style={rightStyle} className={rightClass}>
|
||||
{this.renderRightContent()}
|
||||
</div>
|
||||
|
||||
<div id="left" style={leftStyle} className={leftClass}>
|
||||
<div id="map" ref="map" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default Main
|
||||
31
web/src/helpers/googleMaps.js
Normal file
31
web/src/helpers/googleMaps.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export const pacSelectFirst = input => {
|
||||
// store the original event binding function
|
||||
var _addEventListener = input.addEventListener
|
||||
? input.addEventListener
|
||||
: input.attachEvent
|
||||
|
||||
function addEventListenerWrapper (type, listener) {
|
||||
// Simulate a 'down arrow' keypress on hitting 'return' when no pac suggestion is selected,
|
||||
// and then trigger the original listener.
|
||||
if (type == 'keydown') {
|
||||
var orig_listener = listener
|
||||
listener = function (event) {
|
||||
var suggestion_selected = $('.pac-item-selected').length > 0
|
||||
if (event.which == 13 && !suggestion_selected) {
|
||||
var simulated_downarrow = $.Event('keydown', {
|
||||
keyCode: 40,
|
||||
which: 40
|
||||
})
|
||||
orig_listener.apply(input, [simulated_downarrow])
|
||||
}
|
||||
|
||||
orig_listener.apply(input, [event])
|
||||
}
|
||||
}
|
||||
|
||||
_addEventListener.apply(input, [type, listener])
|
||||
}
|
||||
|
||||
input.addEventListener = addEventListenerWrapper
|
||||
input.attachEvent = addEventListenerWrapper
|
||||
}
|
||||
57
web/src/index.js
Normal file
57
web/src/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import Main from './components/Main'
|
||||
|
||||
const getInitialState = url => {
|
||||
console.log('PARSING URL:', url)
|
||||
const params = window.location.search.substr(1).split('&')
|
||||
|
||||
const initialState = {
|
||||
rooms: {},
|
||||
category: {}
|
||||
}
|
||||
|
||||
for (const param of params) {
|
||||
const [key, value] = param.split('=')
|
||||
console.log('analyzing param ', key, value)
|
||||
if (key === 'rooms' && value !== '') {
|
||||
console.log("IT's ROOMS")
|
||||
value.split(',').forEach(k => {
|
||||
console.log("IT's ROOMS", k)
|
||||
initialState.rooms[parseInt(k)] = true
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'category' && value !== '') {
|
||||
value.split(',').forEach(k => {
|
||||
initialState.category[parseInt(k)] = true
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'sort') {
|
||||
initialState.sort = value
|
||||
}
|
||||
|
||||
if (key === 'bounds') {
|
||||
initialState.bounds = value
|
||||
}
|
||||
|
||||
if (key === 'listingId') {
|
||||
initialState.listingId = value
|
||||
}
|
||||
|
||||
if (key === 'zoom') {
|
||||
initialState.zoom = parseInt(value)
|
||||
}
|
||||
|
||||
if (['minSize', 'maxSize', 'minPrice', 'maxPrice'].includes(key)) {
|
||||
initialState[key] = parseFloat(value)
|
||||
}
|
||||
}
|
||||
|
||||
return initialState
|
||||
}
|
||||
|
||||
const main = <Main initialState={getInitialState(window.location)} />
|
||||
|
||||
render(main, document.getElementById('root'))
|
||||
56
web/src/lib/api.js
Normal file
56
web/src/lib/api.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import fetch from 'isomorphic-fetch'
|
||||
|
||||
export const loadListing = id => {
|
||||
let url = `http://localhost:3001/api/search/listings/${id}`
|
||||
|
||||
return fetch(
|
||||
url,
|
||||
{
|
||||
//credentials: 'include'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const loadProperties = (
|
||||
{
|
||||
bounds,
|
||||
minPrice = '',
|
||||
maxPrice = '',
|
||||
minSize = '',
|
||||
maxSize = '',
|
||||
rooms = {},
|
||||
category = {},
|
||||
page = 1,
|
||||
pins = false,
|
||||
sort = ''
|
||||
}
|
||||
) => {
|
||||
const allRooms = Object.keys(rooms).filter(v => rooms[v]).join(',')
|
||||
|
||||
const allCategories = Object.keys(category)
|
||||
.filter(v => category[v])
|
||||
.join(',')
|
||||
|
||||
// TODO: handle errors
|
||||
//return fetch(process.env.API_URL + '/api/search', {
|
||||
let url = `http://localhost:3001/api/search/listings?bounds=${bounds}&minPrice=${minPrice}&maxPrice=${maxPrice}&rooms=${allRooms}&minSize=${minSize}&maxSize=${maxSize}&category=${allCategories}&page=${page}&pins=${pins}&sort=${sort}`
|
||||
|
||||
return fetch(
|
||||
url,
|
||||
{
|
||||
//credentials: 'include'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const markSeen = id => {
|
||||
const seen = JSON.parse(window.localStorage.getItem('seen') || '[]')
|
||||
seen.push(id)
|
||||
window.localStorage.setItem('seen', JSON.stringify(seen))
|
||||
}
|
||||
|
||||
export const loadSeen = id => {
|
||||
const seen = JSON.parse(window.localStorage.getItem('seen') || '[]')
|
||||
return seen
|
||||
//return seen.findIndex(s => s === id) !== -1;
|
||||
}
|
||||
311
web/src/lib/handlers.js
Normal file
311
web/src/lib/handlers.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import {markSeen} from './api'
|
||||
|
||||
const setMaxPrice = ({type, action}, component) => {
|
||||
const maxPrice = parseFloat(action.maxPrice)
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
maxPrice: isNaN(maxPrice) ? undefined : maxPrice,
|
||||
priceDirty: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setMinPrice = ({type, action}, component) => {
|
||||
const minPrice = parseFloat(action.minPrice)
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
minPrice: isNaN(minPrice) ? undefined : minPrice,
|
||||
priceDirty: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setMinSize = ({type, action}, component) => {
|
||||
const minSize = parseFloat(action.minSize)
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
minSize: isNaN(minSize) ? undefined : minSize,
|
||||
sizeDirty: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setMaxSize = ({type, action}, component) => {
|
||||
const maxSize = parseFloat(action.maxSize)
|
||||
component.setState({
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
maxSize: isNaN(maxSize) ? undefined : maxSize,
|
||||
sizeDirty: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewListingDetails = ({type, action}, component) => {
|
||||
const scrollElem = document.querySelector('.right-content')
|
||||
component.savedScrollTop = scrollElem.scrollTop
|
||||
|
||||
//component.router.listingId = action.id;
|
||||
|
||||
component.setState(
|
||||
{
|
||||
listingDetails: true,
|
||||
listingId: action.id,
|
||||
descriptionExpanded: false,
|
||||
imageIndex: 0,
|
||||
listing: action.listing
|
||||
},
|
||||
() => {
|
||||
//component.router.update();
|
||||
markSeen(action.id)
|
||||
const m = component.findMarker(action.id)
|
||||
if (m) {
|
||||
m.marker.setIcon(component.selectedMarkerIcon())
|
||||
}
|
||||
|
||||
scrollElem.scrollTop = 0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const listingsLoaded = ({type, action}, component) => {
|
||||
const currentListings = new Map()
|
||||
|
||||
for (const listing of action.listings) {
|
||||
currentListings.set(listing._id, listing)
|
||||
}
|
||||
|
||||
component.setState({
|
||||
listings: action.more
|
||||
? new Map([...component.state.listings, ...currentListings])
|
||||
: currentListings,
|
||||
loadingMore: false,
|
||||
totalCount: action.totalCount
|
||||
})
|
||||
}
|
||||
|
||||
const pinsLoaded = ({type, action}, component) => {
|
||||
component.setState({}, () => {
|
||||
component.markers = action.newMarkers
|
||||
})
|
||||
}
|
||||
|
||||
const expandDescription = ({type, action}, component) => {
|
||||
component.setState({
|
||||
descriptionExpanded: true
|
||||
})
|
||||
}
|
||||
|
||||
const prevImage = ({type, action}, component) => {
|
||||
const index = component.state.imageIndex
|
||||
if (index > 0) {
|
||||
component.setState({
|
||||
imageIndex: index - 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const nextImage = ({type, action}, component) => {
|
||||
const index = component.state.imageIndex
|
||||
component.setState({
|
||||
imageIndex: index + 1
|
||||
})
|
||||
}
|
||||
|
||||
const viewImage = ({type, action}, component) => {
|
||||
component.setState({
|
||||
imageIndex: action.index
|
||||
})
|
||||
}
|
||||
|
||||
const searchPlaceChanged = ({type, action}, component) => {
|
||||
component.setState({
|
||||
listingDetails: false,
|
||||
page: 0
|
||||
})
|
||||
}
|
||||
|
||||
const setRooms = ({type, action}, component) => {
|
||||
const prevRooms = component.state.filters.rooms || {}
|
||||
|
||||
component.setState(
|
||||
{
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
rooms: {
|
||||
...prevRooms,
|
||||
[action.rooms]: !prevRooms[action.rooms]
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateSearch = ({type, action}, component) => {
|
||||
component.setState(
|
||||
{
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
sizeDirty: false,
|
||||
priceDirty: false
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const setCategory = ({type, action}, component) => {
|
||||
const prevCategory = component.state.filters.category || {}
|
||||
|
||||
component.setState(
|
||||
{
|
||||
page: 0,
|
||||
filters: {
|
||||
...component.state.filters,
|
||||
category: {
|
||||
...prevCategory,
|
||||
[action.category]: !prevCategory[action.category]
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
component.refreshListings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const onListingMouseOver = ({type, action}, component) => {
|
||||
const marker = component.findMarker(action.id)
|
||||
if (marker) {
|
||||
const seen = component.isSeen(action.id)
|
||||
if (seen) {
|
||||
marker.marker.setIcon(component.visitedHoveredMarkerIcon())
|
||||
} else {
|
||||
marker.marker.setIcon(component.hoveredMarkerIcon())
|
||||
}
|
||||
|
||||
marker.marker.setAnimation(google.maps.Animation.BOUNCE)
|
||||
setTimeout(
|
||||
() => {
|
||||
marker.marker.setAnimation(null)
|
||||
|
||||
if (seen) {
|
||||
marker.marker.setIcon(component.visitedMarkerIcon())
|
||||
} else {
|
||||
marker.marker.setIcon(component.defaultMarkerIcon())
|
||||
}
|
||||
},
|
||||
710
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const backToResults = ({type, action}, component) => {
|
||||
const prevSelected = component.findMarker(component.state.listingId)
|
||||
component.setState(
|
||||
{
|
||||
listingId: null,
|
||||
listingDetails: false
|
||||
},
|
||||
() => {
|
||||
//component.router.update();
|
||||
|
||||
if (prevSelected) {
|
||||
prevSelected.marker.setIcon(component.visitedMarkerIcon())
|
||||
}
|
||||
|
||||
const scrollElem = document.querySelector('.right-content')
|
||||
scrollElem.scrollTop = component.savedScrollTop
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const loadMoreListings = ({type, action}, component) => {
|
||||
const currentPage = component.state.page
|
||||
if (currentPage * 20 < component.state.totalCount) {
|
||||
component.setState(
|
||||
{
|
||||
loadingMore: true,
|
||||
page: currentPage + 1
|
||||
},
|
||||
() => {
|
||||
component.refreshListings(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapIdle = ({type, action}, component) => {
|
||||
component.setState(
|
||||
{
|
||||
page: 0
|
||||
},
|
||||
() => {
|
||||
const scrollElem = document.querySelector('.right-content')
|
||||
scrollElem.scrollTop = 0
|
||||
component.refreshListings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const sortChange = ({type, action}, component) => {
|
||||
component.setState(
|
||||
{
|
||||
sort: action.sort,
|
||||
page: 0
|
||||
},
|
||||
() => {
|
||||
//component.router.update();
|
||||
component.refreshListings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateRoute = ({type, action}, component) => {
|
||||
component.router.update(action)
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
SET_MIN_PRICE: setMinPrice,
|
||||
SET_MAX_PRICE: setMaxPrice,
|
||||
SET_MIN_SIZE: setMinSize,
|
||||
SET_MAX_SIZE: setMaxSize,
|
||||
LISTINGS_LOADED: listingsLoaded,
|
||||
EXPAND_DESCRIPTION: expandDescription,
|
||||
PREV_IMAGE: prevImage,
|
||||
NEXT_IMAGE: nextImage,
|
||||
VIEW_IMAGE: viewImage,
|
||||
SEARCH_PLACE_CHANGED: searchPlaceChanged,
|
||||
SET_ROOMS: setRooms,
|
||||
VIEW_LISTING_DETAILS: viewListingDetails,
|
||||
UPDATE_SEARCH: updateSearch,
|
||||
SET_CATEGORY: setCategory,
|
||||
ON_LISTING_MOUSE_OVER: onListingMouseOver,
|
||||
BACK_TO_RESULTS: backToResults,
|
||||
LOAD_MORE_LISTINGS: loadMoreListings,
|
||||
MAP_IDLE: mapIdle,
|
||||
PINS_LOADED: pinsLoaded,
|
||||
SORT_CHANGE: sortChange,
|
||||
UPDATE_ROUTE: updateRoute
|
||||
}
|
||||
|
||||
export const handleMessage = ({type, action}, component) => {
|
||||
if (!handlers[type]) {
|
||||
throw new `Unhandled message: ${type}`()
|
||||
}
|
||||
|
||||
return handlers[type]({type, action}, component)
|
||||
}
|
||||
20
web/src/lib/helpers.js
Normal file
20
web/src/lib/helpers.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export const formatPrice = p => {
|
||||
if (isNaN(p)) {
|
||||
return 'Po dogovoru'
|
||||
}
|
||||
|
||||
return p.toLocaleString('bs') + ' KM'
|
||||
}
|
||||
|
||||
export const formatFilterNumber = num => {
|
||||
if (isNaN(num) || num == null) {
|
||||
return ''
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
export const galleryImageUrl = img =>
|
||||
img && img.replace('upload/', 'upload/w_500/')
|
||||
|
||||
export const listingImageUrl = img =>
|
||||
img && img.replace('upload/', 'upload/w_205/')
|
||||
95
web/src/lib/router.js
Normal file
95
web/src/lib/router.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import clone from 'lodash.clonedeep'
|
||||
|
||||
export default class Router {
|
||||
constructor (comp, initialState) {
|
||||
this.component = comp
|
||||
this.state = clone(initialState) || {}
|
||||
|
||||
window.onpopstate = event => {
|
||||
const state = event.state
|
||||
if (state) {
|
||||
if (state.toDispatch) {
|
||||
this.component.dispatch(state.toDispatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update (state) {
|
||||
const params = []
|
||||
if (state.params) {
|
||||
let cloned = clone(state)
|
||||
|
||||
if (cloned.params.rooms != null) {
|
||||
this.state.rooms[cloned.params.rooms] = !this.state.rooms[
|
||||
cloned.params.rooms
|
||||
]
|
||||
}
|
||||
|
||||
if (cloned.params.category != null) {
|
||||
this.state.category[cloned.params.category] = !this.state.category[
|
||||
cloned.params.category
|
||||
]
|
||||
}
|
||||
|
||||
delete cloned.params['rooms']
|
||||
delete cloned.params['category']
|
||||
|
||||
this.state = Object.assign(this.state, cloned.params)
|
||||
|
||||
const {
|
||||
listingId,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
minSize,
|
||||
maxSize,
|
||||
bounds,
|
||||
sort,
|
||||
rooms = {},
|
||||
category = {},
|
||||
zoom
|
||||
} = this.state
|
||||
|
||||
if (listingId) {
|
||||
params.push(`listingId=${listingId}`)
|
||||
}
|
||||
|
||||
params.push(`sort=${sort}`)
|
||||
params.push(`bounds=${bounds}`)
|
||||
params.push(`zoom=${zoom}`)
|
||||
if (maxPrice) {
|
||||
params.push(`maxPrice=${maxPrice}`)
|
||||
}
|
||||
|
||||
if (minPrice) {
|
||||
params.push(`minPrice=${minPrice}`)
|
||||
}
|
||||
|
||||
if (minSize) {
|
||||
params.push(`minSize=${minSize}`)
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
params.push(`maxSize=${maxSize}`)
|
||||
}
|
||||
params.push(
|
||||
`rooms=${Object.keys(rooms).filter(v => rooms[v]).join(',')}`
|
||||
)
|
||||
params.push(
|
||||
`category=${Object.keys(category).filter(v => category[v]).join(',')}`
|
||||
)
|
||||
}
|
||||
|
||||
if (state.toDispatch) {
|
||||
window.history.pushState(state, '', `/?${params.join('&')}`)
|
||||
} else {
|
||||
const oldState = window.history.state
|
||||
if (oldState) {
|
||||
const newState = Object.assign(oldState, state)
|
||||
window.history.replaceState(newState, '', `/?${params.join('&')}`)
|
||||
} else {
|
||||
window.history.replaceState(state, '', `/?${params.join('&')}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
entry: ["./index.js"],
|
||||
entry: ["./src/index.js"],
|
||||
output: {
|
||||
path: __dirname + "/dist",
|
||||
filename: "app.bundle.js",
|
||||
|
||||
929
web/yarn.lock
929
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user