Background:
Willow was built to be a clone of the real estate website Zillow.
Functionality & MVPs
This app will allow users to search for, create, update, destroy and manage likes of house listings in their chosen area. Coming soon is booking tours.
User Auth user can login/demo login, logout, and create account
Listings - CRUD
listings can be added, updated, and removed, and most recent listings displayed on spash page.
Favorites - CRUD favorites can be created and destroyed, and viewed in profile
Bookings - CRUD tour bookings can be created, viewed in profile, updated, and deleted
Maps - Google Maps
App utilizes Google Maps API to display location of listings
Search
Users can search for listings in their chosen location, filtering by city, state, number of bedrooms/bathrooms, and price Coming soon is text based search
Technologies and Libraries Used:
React and Redux
React library used to organize code into functional components, each with their own unique behavior on render, and their own logic to manage the local state. For example, the listing index below keeps track of state variables (bedrooms, bathrooms, price, state, city) used to filter results by the user's choice.
function ListingIndex() {
const dispatch = useDispatch();
const listings = useSelector(getListings);
const [bedrooms, setBedrooms] = useState(null);
const [bathrooms, setBathrooms] = useState(null);
const [price, setPrice] = useState(null);
const [state, setState] = useState(null);
const [city, setCity] = useState(null);
useEffect(() => {
// dispatch action to fetch listings with selected filters
//dispatch(fetchListings({ bedrooms, bathrooms, price, state, city }));
dispatch(fetchListings())
}, [dispatch, bedrooms, bathrooms, price, state, city]);
const filteredListings = listings.filter(listing => {
// Check if the listing matches the selected filter options
if (bedrooms && listing.bedrooms <= bedrooms) {
return false;
}
if (bathrooms && listing.bathrooms <= bathrooms) {
return false;
}
if (price && listing.price <= parseInt(price)) {
return false;
}
if (city && listing.city !== city) {
return false;
}
if (state && listing.state !== state) {
return false;
}
// If all filter options are null or match the listing, include the listing
return true;
});
if (!listings){
return null;
}
return (
<div>
<div className="filters">
<div>
<label>Bedrooms:</label>
<select onChange={(e) => setBedrooms(e.target.value)}>
<option value="">Any</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
</select>
</div>
<div>
<label>Bathrooms:</label>
<select onChange={(e) => setBathrooms(e.target.value)}>
<option value="">Any</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
</select>
</div>
<div>
<label>Price:</label>
<select onChange={(e) => setPrice(e.target.value)}>
<option value="">Any</option>
<option value="500000">$500,000+</option>
<option value="1000000">$1,000,000+</option>
<option value="2000000">$2,000,000+</option>
</select>
</div>
<div>
<label>City:</label>
<select onChange={(e) => setCity(e.target.value)}>
<option value="">Any</option>
<option value="Los Angeles">Los Angeles</option>
<option value="New York">New York City</option>
<option value="Houston">Houston</option>
<option value="Miami">Miami</option>
</select>
</div>
<div>
<label>State:</label>
<select onChange={(e) => setState(e.target.value)}>
<option value="">Any</option>
<option value="CA">California</option>
<option value="NY">New York</option>
<option value="TX">Texas</option>
<option value="FL">Florida</option>
</select>
</div>
</div>
<div className="container">
{filteredListings.map(listing => (
<Link to={`/listings/${listing.id}`} key={listing.id} className="card-link">
<div className="card">
<img src={listing.photos[0]} alt={listing.title} />
<div className="card-body">
<h3>${Math.floor(listing.price).toLocaleString()}</h3>
<p>{listing.bedrooms} bd <span>|</span> {listing.bathrooms} ba <span>|</span> {listing.square_feet} sqft</p>
<p>{listing.address}</p>
</div>
</div>
</Link>
))}
</div>
</div>
);
}
export default ListingIndex;
Application state was saved into redux store in order to manage state of relevant data, both globally and local state of each functional component. Utilized thunk middleware to connect Ruby on Rails backend to React-Redux frontend.
const rootReducer = combineReducers({
session,
listings,
favorites
});
export const configureStore = (preloadedState) => {
return createStore(rootReducer, preloadedState, enhancer);
};
export default configureStore;
Reducers for Listings, Session and Favorites handled state changes as necessary:
const favoritesReducer = (state = {}, action) => {
const newState = { ...state }
switch (action.type) {
case SET_CURRENT_USER:
return action.favorites ? action.favorites : {};
case CREATE_FAVORITE:
//return {...state, ...action.payload}
newState[action.payload.id] = action.payload;
return newState;
case DELETE_FAVORITE:
delete newState[action.payload]
return newState;
default:
return state;
}
};
const listingsReducer = (state = {}, action) => {
const newState = {...state}
switch (action.type) {
case "SET_LISTINGS":
return { ...state, ...action.payload };
case "RECEIVE_LISTING":
newState[action.listing.id] = action.listing
return newState;
case "POST_LISTING":
return {...state, [action.listing.id]: action.listing};
case "UPDATE_LISTING":
return {...state, [action.listing.id]: action.listing};
case "DELETE_LISTING":
delete newState[action.listingID];
return newState;
default:
return state;
}
};
const sessionReducer = (state = initialState, action) => {
let newState = {...state}
switch (action.type) {
case SET_CURRENT_USER:
return { ...state, user: action.user };
case REMOVE_CURRENT_USER:
return { ...state, user: null };
default:
return state;
}
};
useEffect hooks used in Listing Show component to render google maps element on page with every render, with marker at given listing location.
useEffect(() => {
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyAV4WKaME8NfVDjcMKlZtvSKn3oe-MiyXU`;
script.onload = () => {
const map = new window.google.maps.Map(document.getElementById("map"), {
center: { lat: 37.7749, lng: -122.4194 }, // San Francisco as default center
zoom: 13, // Default zoom level
});
setMap(map);
};
document.body.appendChild(script);
}, []);
useEffect(() => {
if (map && listing) {
const geocoder = new window.google.maps.Geocoder();
geocoder.geocode(
{ address: `${listing.address} ${listing.city} ${listing.state}` },
(results, status) => {
if (status === "OK") {
const marker = new window.google.maps.Marker({
position: results[0].geometry.location,
map,
});
map.setCenter(marker.getPosition());
} else {
console.error("Geocode was not successful for the following reason:", status);
}
}
);
}
}, [map, listing]);
Future Features are to include text based search, and include google maps element on search results page showing location of all listings in a given area.