diff --git a/web/components/Gallery.js b/web/components/Gallery.js deleted file mode 100644 index 0dd6a5f..0000000 --- a/web/components/Gallery.js +++ /dev/null @@ -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 ( -
- - {showPrev ? -
-
- - - -
- - -
- : null} - {showNext ? -
-
- - - -
-
- : null} -
- {images.map((img, index) => { - let cls = 'image-dot' - if (index === imageIndex) { - cls += ' selected' - } - - return
- })} -
-
) - } -} diff --git a/web/components/Main.js b/web/components/Main.js deleted file mode 100644 index 4138430..0000000 --- a/web/components/Main.js +++ /dev/null @@ -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 = ''; - //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(); - } else { - children.push(); - children.push(); - } - const content = ( -
- {children} -
); - - 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 ( -
- - - - -
-
-
-
-
- ) - } -} -export default Main; diff --git a/web/helpers/googleMaps.js b/web/helpers/googleMaps.js deleted file mode 100644 index 8add6f5..0000000 --- a/web/helpers/googleMaps.js +++ /dev/null @@ -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; -} diff --git a/web/index.js b/web/index.js deleted file mode 100644 index 7714743..0000000 --- a/web/index.js +++ /dev/null @@ -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 = (
); - -render(main, document.getElementById('root')); - diff --git a/web/lib/api.js b/web/lib/api.js deleted file mode 100644 index 26f7dea..0000000 --- a/web/lib/api.js +++ /dev/null @@ -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; -} diff --git a/web/lib/handlers.js b/web/lib/handlers.js deleted file mode 100644 index fb64ca3..0000000 --- a/web/lib/handlers.js +++ /dev/null @@ -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); -}; diff --git a/web/lib/helpers.js b/web/lib/helpers.js deleted file mode 100644 index c3948f8..0000000 --- a/web/lib/helpers.js +++ /dev/null @@ -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/") - diff --git a/web/lib/router.js b/web/lib/router.js deleted file mode 100644 index 588b588..0000000 --- a/web/lib/router.js +++ /dev/null @@ -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("&")}`); - } - } - } -} diff --git a/web/package.json b/web/package.json index 62f2c47..7466c6f 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } diff --git a/web/components/Filters.js b/web/src/components/Filters.js similarity index 70% rename from web/components/Filters.js rename to web/src/components/Filters.js index a8b91cc..e4266c1 100644 --- a/web/components/Filters.js +++ b/web/src/components/Filters.js @@ -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: { - params: {rooms} - }}); + 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) { + onCategoryClick (category) { + this.props.dispatch({ + type: 'UPDATE_ROUTE', + action: { + params: {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 (
@@ -119,7 +123,7 @@ export default class Filters extends React.Component { onChange={this.onMinPriceChange.bind(this)} value={formatFilterNumber(filters.minPrice)} /> - {" "} + {' '} DO {this.props.filters.priceDirty - ? - - + aria-hidden="true" + /> : null}
@@ -145,24 +147,36 @@ export default class Filters extends React.Component {
+ className={ + `filter-btn property-type-btn ${selectedCategory(CATEGORY_FLAT)}` + } + > Stan
+ className={ + `filter-btn property-type-btn ${selectedCategory(CATEGORY_HOUSE)}` + } + > Kuća
+ className={ + `filter-btn property-type-btn ${selectedCategory(CATEGORY_LAND)}` + } + > Zemljište
+ className={ + `filter-btn property-type-btn ${selectedCategory(CATEGORY_OFFICE)}` + } + > Poslovni prostor
@@ -180,7 +194,7 @@ export default class Filters extends React.Component { /> DO - {" "} + {' '} - + aria-hidden="true" + /> : null} @@ -221,8 +235,8 @@ export default class Filters extends React.Component { 3
4+
@@ -237,6 +251,6 @@ export default class Filters extends React.Component {
- ); + ) } } diff --git a/web/src/components/Gallery.js b/web/src/components/Gallery.js new file mode 100644 index 0000000..3cfe00d --- /dev/null +++ b/web/src/components/Gallery.js @@ -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 ( +
+ + {showPrev + ?
+
+ + + +
+ +
+ : null} + {showNext + ?
+
+ + + +
+
+ : null} +
+ {images.map((img, index) => { + let cls = 'image-dot' + if (index === imageIndex) { + cls += ' selected' + } + + return ( +
+ ) + })} +
+
+ ) + } +} diff --git a/web/components/ListingDetails.js b/web/src/components/ListingDetails.js similarity index 59% rename from web/components/ListingDetails.js rename to web/src/components/ListingDetails.js index 2c57258..eb101ca 100644 --- a/web/components/ListingDetails.js +++ b/web/src/components/ListingDetails.js @@ -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: { - toDispatch: { - type: 'BACK_TO_RESULTS' - }, - params: { - listingId: null + 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 (
-