635 lines
16 KiB
JavaScript
635 lines
16 KiB
JavaScript
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: {}
|
|
},
|
|
mobileView: 'MAP',
|
|
contact: {
|
|
message: '',
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
valid: true
|
|
}
|
|
}
|
|
|
|
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>Filteri</button>'
|
|
//control.style = "top: 200px;"
|
|
// TODO: enable this
|
|
//control['style'] = 'top: 200px;'
|
|
|
|
var input = document.getElementById('gmaps-places-input')
|
|
|
|
pacSelectFirst(input)
|
|
input.addEventListener('focus', (e) => {
|
|
e.target.value = '';
|
|
});
|
|
|
|
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)
|
|
}
|
|
|
|
document.activeElement.blur();
|
|
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
|
|
contactFormOpen={this.state.contactFormOpen}
|
|
contact={this.state.contact}
|
|
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
|
|
}
|
|
|
|
onMobileListViewClick (e) {
|
|
e.preventDefault()
|
|
this.dispatch({
|
|
type: 'MOBILE_LIST_VIEW'
|
|
})
|
|
}
|
|
|
|
onMobileMapViewClick (e) {
|
|
e.preventDefault()
|
|
this.dispatch({
|
|
type: 'MOBILE_MAP_VIEW'
|
|
})
|
|
}
|
|
|
|
render () {
|
|
const leftStyle = {}
|
|
const rightStyle = {}
|
|
const listingDetails = true
|
|
|
|
let leftClass = 'left-base'
|
|
let rightClass = 'right-base'
|
|
|
|
if (this.state.listingId || 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">KIVI</span>
|
|
<input
|
|
id="gmaps-places-input"
|
|
placeholder="Unesite adresu, naselje ili grad"
|
|
className="where-to"
|
|
type="text"
|
|
/>
|
|
<div className="view-types">
|
|
<a onClick={this.onMobileListViewClick.bind(this)}className="view-type-left">
|
|
<i className="btn-select-map fa fa-list" />
|
|
</a>
|
|
<a onClick={this.onMobileMapViewClick.bind(this)} 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}>
|
|
{this.state.mobileView === 'LIST' && !this.state.listingDetails &&
|
|
<div className="map-list-view">
|
|
<Listings
|
|
sort={this.state.sort}
|
|
totalCount={this.state.totalCount}
|
|
loadingMore={this.state.loadingMore}
|
|
listings={this.state.listings}
|
|
dispatch={this.dispatch.bind(this)}
|
|
/>
|
|
</div>}
|
|
<div id="map" ref="map" className={this.state.mobileView !== 'MAP' && "hide"} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
export default Main
|