Skip to content

Commit 3039cb6

Browse files
Merge pull request #12 from ProblemSetters/all-solutions
Merge solution
2 parents 9811228 + 48d03d2 commit 3039cb6

File tree

19 files changed

+1341
-441
lines changed

19 files changed

+1341
-441
lines changed

output/.gitkeep

Whitespace-only changes.

package-lock.json

Lines changed: 326 additions & 439 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"h8k-design": "^1.0.16",
88
"react": "^18.2.0",
99
"react-dom": "^18.2.0",
10+
"react-router-dom": "^6.21.1",
1011
"react-scripts": "^5.0.1"
1112
},
1213
"scripts": {
@@ -15,6 +16,9 @@
1516
"start": "PORT=8000 react-scripts start",
1617
"build": "react-scripts build",
1718
"test": "./node_modules/.bin/react-scripts test --watchAll=false --verbose --env=jsdom --testResultsProcessor ./node_modules/jest-junit",
19+
"test:task1": "(react-scripts test test/task1/App.test.js --watchAll=false --env=jsdom --verbose --colors --testResultsProcessor=jest-junit); (mv junit.xml ./output/task1.xml)",
20+
"test:task2": "(react-scripts test test/task2/App.test.js --watchAll=false --env=jsdom --verbose --colors --testResultsProcessor=jest-junit); (mv junit.xml ./output/task2.xml)",
21+
"test:task3": "(react-scripts test test/task3/App.test.js --watchAll=false --env=jsdom --verbose --colors --testResultsProcessor=jest-junit); (mv junit.xml ./output/task3.xml)",
1822
"eject": "react-scripts eject"
1923
},
2024
"devDependencies": {

src/App.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import React from "react";
2+
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
23
import "./App.css";
34
import "h8k-components";
5+
import HomePage from "./components/HomePage";
6+
import PropertyDetails from "./components/PropertyDetails";
47

5-
const title = "React App";
8+
const title = "Rental Property";
69

710
const App = () => {
811
return (
912
<div className="App">
1013
<h8k-navbar header={title}></h8k-navbar>
14+
<Router>
15+
<Routes>
16+
<Route path="/" element={<HomePage />} />
17+
<Route path="/property-details/:id" element={<PropertyDetails />} />
18+
</Routes>
19+
</Router>
1120
</div>
1221
);
1322
};

src/assests/placeholder.jpeg

4.25 KB
Loading

src/components/FilterButton.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.filters {
2+
display: flex;
3+
gap: 80px;
4+
align-items: flex-start;
5+
justify-content: center;
6+
}
7+
8+
.filters label {
9+
display: block;
10+
margin-bottom: 12px;
11+
}
12+
13+
.filters input {
14+
margin-right: 8px;
15+
}
16+
17+
.filters input[type="number"] {
18+
width: 75px;
19+
padding: 5px;
20+
margin-top: 5px;
21+
margin-bottom: 10px;
22+
margin-left: 8px;
23+
border: 1px solid #ccc;
24+
border-radius: 3px;
25+
}
26+
27+
.filters input[type="radio"],
28+
.filters input[type="checkbox"] {
29+
accent-color: #1ba94c;
30+
}
31+
32+
.filters input[type="radio"]:hover,
33+
.filters input[type="checkbox"]:hover {
34+
background-color: transparent;
35+
}
36+
37+
.amenitites-filter,
38+
.rating-filter {
39+
display: flex;
40+
flex-direction: column;
41+
align-items: flex-start;
42+
justify-content: flex-start;
43+
}

src/components/FilterButton.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, { useState } from "react";
2+
import "./FilterButton.css";
3+
4+
const FilterButton = ({
5+
setPriceOrder,
6+
setAmenitiesFilter,
7+
setRatingFilter,
8+
setPriceFilter,
9+
priceOrder,
10+
amenitiesFilter,
11+
ratingFilter,
12+
uniqueAmenities,
13+
priceFilter,
14+
onClear,
15+
}) => {
16+
const [showFilters, setShowFilters] = useState(false);
17+
const handleAmenityChange = (amenity) => {
18+
if (amenitiesFilter.includes(amenity)) {
19+
setAmenitiesFilter(amenitiesFilter.filter((a) => a !== amenity));
20+
} else {
21+
setAmenitiesFilter([...amenitiesFilter, amenity]);
22+
}
23+
};
24+
25+
const isAscSortSelected = priceOrder === "asc";
26+
27+
return (
28+
<div className="filter">
29+
<button data-testid="price-button" onClick={() => setPriceOrder(isAscSortSelected ? "desc" : "asc")}>
30+
{isAscSortSelected ? "Price: High to Low" : "Price: Low to High"}
31+
</button>
32+
<button data-testid="filter-button" onClick={() => setShowFilters(!showFilters)}>
33+
{showFilters ? "Hide Filters" : "Show Filters"}
34+
</button>
35+
36+
{showFilters && (
37+
<div>
38+
<div className="filters">
39+
{/* Price Filter */}
40+
<div className="price-filter">
41+
<h4>Price</h4>
42+
<label>
43+
Min Price:
44+
<input
45+
type="number"
46+
data-testid="price-input-min"
47+
min="1"
48+
max={priceFilter[1]}
49+
value={priceFilter[0]}
50+
onChange={(e) => {
51+
setPriceFilter([Number(e.target.value), priceFilter[1]]);
52+
}}
53+
/>
54+
</label>
55+
<label>
56+
Max Price:
57+
<input
58+
type="number"
59+
data-testid="price-input-max"
60+
min={priceFilter[0]}
61+
max="1000"
62+
value={priceFilter[1]}
63+
onChange={(e) => {
64+
setPriceFilter([priceFilter[0], Number(e.target.value)]);
65+
}}
66+
/>
67+
</label>
68+
</div>
69+
{/* Rating Filter */}
70+
<div className="rating-filter">
71+
<h4>Rating</h4>
72+
{["0-1", "1-2", "2-3", "3-4", "4-"].map((range, index) => (
73+
<label key={range} className="rating-label">
74+
<input
75+
type="radio"
76+
name="rating"
77+
data-testid="rating-filter"
78+
value={range}
79+
checked={ratingFilter === range}
80+
onChange={(e) => setRatingFilter(e.target.value)}
81+
/>
82+
{index < 4
83+
? `${range.replace("-", " to ")} Stars`
84+
: "4+ Stars"}
85+
</label>
86+
))}
87+
</div>
88+
89+
{/* Amenities Filter */}
90+
<div className="amenitites-filter">
91+
<h4>Amenities</h4>
92+
{uniqueAmenities.map((amenity) => (
93+
<label key={amenity}>
94+
<input
95+
type="checkbox"
96+
data-testid="amenities-filter"
97+
checked={amenitiesFilter.includes(amenity)}
98+
onChange={() => handleAmenityChange(amenity)}
99+
/>
100+
{amenity}
101+
</label>
102+
))}
103+
</div>
104+
</div>
105+
<button data-testid="clear-button" onClick={onClear}>Clear</button>
106+
</div>
107+
)}
108+
</div>
109+
);
110+
};
111+
112+
export default FilterButton;

src/components/HomePage.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.home-page {
2+
text-align: center;
3+
}
4+
5+
.home-page h3 {
6+
margin: 3% auto 1% auto;
7+
}
8+
9+
.property-cards {
10+
display: flex;
11+
flex-wrap: wrap;
12+
justify-content: center;
13+
margin-top: 24px;
14+
}
15+
16+
.property-card {
17+
border: 1px solid #ccc;
18+
border-radius: 0px;
19+
margin: 10px;
20+
padding: 10px;
21+
width: 250px;
22+
display: flex;
23+
flex-direction: column;
24+
justify-content: space-between;
25+
}
26+
27+
.property-card img {
28+
width: 100%;
29+
height: 175px;
30+
border-radius: 5px;
31+
}
32+
33+
.filter {
34+
display: inline;
35+
margin-left: 1.5%;
36+
}

src/components/HomePage.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React, { useState } from "react";
2+
import PropertyCard from "./PropertyCard";
3+
import SearchBar from "./SearchBar";
4+
import FilterButton from "./FilterButton";
5+
import propertiesData from "../data.json";
6+
import "./HomePage.css";
7+
8+
const HomePage = () => {
9+
const [searchTerm, setSearchTerm] = useState("");
10+
const [priceOrder, setPriceOrder] = useState("");
11+
12+
const [amenitiesFilter, setAmenitiesFilter] = useState([]);
13+
const [ratingFilter, setRatingFilter] = useState("");
14+
15+
const properties = propertiesData.properties;
16+
17+
const [minPrice, maxPrice] = properties.reduce(
18+
([min, max], property) => {
19+
const price = parseInt(
20+
property.price.replace("$", "").replace("/night", "")
21+
);
22+
return [Math.min(min, price), Math.max(max, price)];
23+
},
24+
[Infinity, -Infinity]
25+
);
26+
27+
const [priceFilter, setPriceFilter] = useState([minPrice, maxPrice]);
28+
29+
const uniqueAmenities = Array.from(
30+
new Set(properties.flatMap((property) => property.amenities))
31+
);
32+
33+
let filteredProperties = properties.filter((property) => {
34+
const matchesAmenities = amenitiesFilter.length
35+
? amenitiesFilter.every((amenity) => property.amenities.includes(amenity))
36+
: true;
37+
38+
const matchesRating = ratingFilter
39+
? (() => {
40+
const [min, max] = ratingFilter.split("-").map(Number);
41+
return property.rating >= min && (max ? property.rating < max : true);
42+
})()
43+
: true;
44+
45+
const matchesPrice = (() => {
46+
const [min, max] = priceFilter;
47+
const price = parseInt(
48+
property.price.replace("$", "").replace("/night", "")
49+
);
50+
return price >= min && price <= max;
51+
})();
52+
53+
const matchesSearch = property.name
54+
.toLowerCase()
55+
.includes(searchTerm.toLowerCase());
56+
57+
return matchesAmenities && matchesRating && matchesSearch && matchesPrice;
58+
});
59+
60+
if (priceOrder) {
61+
filteredProperties = filteredProperties.sort((a, b) => {
62+
const priceA = parseInt(a.price.replace("$", "").replace("/night", ""));
63+
const priceB = parseInt(b.price.replace("$", "").replace("/night", ""));
64+
return priceOrder === "asc" ? priceA - priceB : priceB - priceA;
65+
});
66+
}
67+
68+
const handleClearFilters = () => {
69+
setPriceOrder("");
70+
setAmenitiesFilter([]);
71+
setRatingFilter("");
72+
setPriceFilter([minPrice, maxPrice]);
73+
};
74+
75+
return (
76+
<div className="home-page">
77+
<h2>Find Your Perfect Home Away From Home!</h2>
78+
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
79+
<FilterButton
80+
setPriceOrder={setPriceOrder}
81+
setAmenitiesFilter={setAmenitiesFilter}
82+
setRatingFilter={setRatingFilter}
83+
setPriceFilter={setPriceFilter}
84+
priceOrder={priceOrder}
85+
priceFilter={priceFilter}
86+
amenitiesFilter={amenitiesFilter}
87+
ratingFilter={ratingFilter}
88+
uniqueAmenities={uniqueAmenities}
89+
onClear={handleClearFilters}
90+
/>
91+
<div className="property-cards">
92+
{filteredProperties.length > 0 ? (
93+
filteredProperties.map((property) => (
94+
<PropertyCard key={property.id} property={property} />
95+
))
96+
) : (
97+
<h3 data-testid="conditional-message">No rental property available</h3>
98+
)}
99+
</div>
100+
</div>
101+
);
102+
};
103+
104+
export default HomePage;

src/components/PropertyCard.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react";
2+
import { Link } from "react-router-dom";
3+
import placeholderImage from "../assests/placeholder.jpeg";
4+
5+
const PropertyCard = ({ property }) => {
6+
return (
7+
<div className="property-card">
8+
<div>
9+
<img src={placeholderImage} alt="property" />
10+
<h3 data-testid="property-name">{property.name}</h3>
11+
<p>{property.description}</p>
12+
<p>Location: {property.location}</p>
13+
<p>Price: {property.price}</p>
14+
</div>
15+
<Link to={`/property-details/${property.id}`} data-testid="view-details">View Details</Link>
16+
</div>
17+
);
18+
};
19+
20+
export default PropertyCard;

0 commit comments

Comments
 (0)