Paw-perty, an AirBnB clone, is a dog-siting booking application that allows users to:
- Freely explore listings in various cities
- Reserve a spot in a friendly new home
- Become a super-host by creating your own listing
- Leave a trail of ratings and reviews of a previous stay
- Javascript
- React.js
- Redux
- HTML5 / CSS / Sass
- JQuery
- Ruby on Rails
- PostgreSQL
- JBuilder
- Google Maps API
- Googlle Geocoding API
- Amazon Web Services - S3
- Webpack
- Clone this repo git clone: https://github.com/kcheng16/paw-perty
- Ensure to have postgreSQL installed and running
- Install NPM packages: npm install
- Install Ruby gems: bundle install
- Create the Database: rails db:create
- Setup the datebase: rails db:setup
- Start your local server: rails s
- Start webpack bundlers: npm run start
Users are given a similar AirBnB experience of filling in a form and publishing a listing. Each page is displayed using the component's state to determine which information to render by taking advantage of Reacts single-page application feature.
User will be allowed to change to the next "page" once the current page's input field is filled and saved into the component's state. The "Next" button will appear which adds or subtracts the component's state: page index. The button will change to a submit button and all information from previous pages can be submitted once they reach the "final page".
isCurrentPageInputFilled(){
switch (this.state.localState.pageIndex){
case 0:
return this.state.title.length !== 0
case 1:
return this.state.description.length !== 0
case 2:
return this.state.street_address.length !== 0 &&
this.state.city.length !== 0 &&
this.state.postal_code.length !== 0 &&
this.state.country.length !== 0
case 3:
return this.state.num_of_beds !== 0
case 4:
return this.state.photoFile.length === 5
case 5:
return this.state.price !== 0
default:
return false
}
}
...
{this.isCurrentPageInputFilled() ?
<button
onClick={(e) => {
if (this.state.localState.pageIndex !== 5 ) {
this.addPageIndex();
} else {
this.handleSubmit(e);
}
}}
>
{this.state.localState.pageIndex !== 5 ? "Next" : "Submit"}
</button>
: ""
}
Listings are fetched depending on the search field city name input. The app reads the :city
wildcard from the URL to construct its query to the database. Only listings similar to that city name will be displayed and marked within Google Maps. The map will pan over to the closest city to the user's input. Users can then click on the customized marker and view the information window about that listing.
/app/controllers/api/listings_controller.rb
def index
@listings = Listing.with_attached_photos.all
if params[:city]
@listings = Listing.where(city: params[:city])
render :index
else
render :index
end
end
The listing being created will be marked on the map using geocoding, searching for the best possible result on the map using the listings: address, city, postal code, and country.
/frontend/components/listings/listings_create_form.jsx
handleSubmit(e){
e.preventDefault();
let geocoder = new google.maps.Geocoder()
geocoder.geocode(
{address: `${this.state.street_address},${this.state.city}, ${this.state.postal_code},${this.state.country}`},
(results, status) => {
if (status == 'OK') {
this.setState(
{longitude: results[0].geometry.location.lng(), latitude: results[0].geometry.location.lat()}...
...this.props.createListing(formData)
)
}
)
}
Map markers are created using a single imported function managed from the marker manager utility. Markers are set onto the map using the longitude and latitude for each listing provided within the user's city search. Event listeners are provided to only display a single info-window at a time before being set to the map.
#frontend/util/marker_manager.js
updateMarkers(listings) {
let thisMap = this.map
let marker;
listings.forEach(listing => {
if(!(listing.id in this.markers)){
this.markers[listing.id] = listing
marker = this.createMarker(listing)
marker.addListener("click", () => {
if(this.infoWindow){
this.infoWindow.close()
}
})
this.createMarkerInfoWindow(listing, marker, thisMap)
marker.setMap(thisMap)
}
});
}
Newly created markers are given its own information window. Information windows will hold content on how that listings information will be displayed upon triggering of an on-click event listener. It's marker location will then be set onto the map using the listings longitude and latitude. Upon opening a new information window, the previous information window will be closed.
createMarkerInfoWindow(listing, marker, thisMap){
const contentString =
`<a className="info-window" href="/#/listings/${listing.id}">`+
`<div><img style="display: inline-block; height: 200px; width: 100%; object-fit: cover;" src="${listing.images[0] ? listing.images[0] : listing.photos[0]}"/></div>`+
`<div class="info-window-text">`+
`<div>${listing.average_rating ? listing.average_rating : "0 reviews"}</div>`+
`<div style="padding: 5px 0 10px 0; font-size: 18px;">${listing.title}</div>`+
`<div style="font-weight: 800;">${listing.price} Doge coins / night</div>`+
`</div>`+
"</a>";
let infoWindow = new google.maps.InfoWindow({
content: contentString,
});
marker.addListener("click", () => {
infoWindow.open({
anchor: marker,
thisMap,
shouldFocus: true,
});
this.infoWindow = infoWindow
});
thisMap.addListener('click', function() {
if (infoWindow) infoWindow.close();
});
}
- Update listing images using Active Storage
- Reservation black-out dates