Reusing react components to build awesome components. You’ll create a star rating component that will handle collecting users’ ratings.
import React, { Component } from 'react';
import './star-rating.css';
class StarRating extends Component {
constructor(props) {
super(props);
this.state = {
currentRating: this.props.currentRating
};
}
componentDidMount() {
this.setRating();
}
hoverHandler = ev => {
const stars = ev.target.parentElement.getElementsByClassName('star');
const hoverValue = ev.target.dataset.value;
Array.from(stars).forEach(star => {
star.style.color = hoverValue >= star.dataset.value ? 'yellow' : 'gray';
});
};
setRating = ev => {
const stars = this.refs.rating.getElementsByClassName('star');
Array.from(stars).forEach(star => {
star.style.color =
this.state.currentRating >= star.dataset.value ? 'yellow' : 'gray';
});
};
starClickHandler = ev => {
let rating = ev.target.dataset.value;
this.setState({ currentRating: rating }); // set state so the rating stays highlighted
if(this.props.onClick){
this.props.onClick(rating); // emit the event up to the parent
}
};
render() {
return (
<div
className="rating"
ref="rating"
data-rating={this.state.currentRating}
onMouseOut={this.setRating}
>
{[...Array(+this.props.numberOfStars).keys()].map(n => {
return (
<span
className="star"
key={n+1}
data-value={n+1}
onMouseOver={this.hoverHandler}
onClick={this.starClickHandler}
>
★
</span>
);
})}
</div>
);
}
}
export default StarRating;
Now that you’ve got a rating component, you’ll want to put it on a page. Create a folder in src called pages and inside that add a new rating folder with a rating-page.jsx and rating-page.css file.
The contents of the rating-page.jsx should be:
import React, { Component } from 'react';
import { withAuth } from '@okta/okta-react';
import { BrewstrRef } from '../../firebase';
import StarRating from '../../components/rater/star-rating';
import './rating-page.css';
class RatingPage extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
description: '',
rating: 0,
user: ''
};
}
async componentDidMount(){
const user = await this.props.auth.getUser();
this.setState({user:user.email});
}
handleChange = ev => {
this.setState({
[ev.target.name]: ev.target.value
});
};
setRating = rating => {
this.setState({ rating: rating });
};
saveRating = () => {
BrewstrRef.push()
.set(this.state)
.then(() => {
this.props.history.push('/ratinglist');
});
};
render() {
return (
<div className="rating-form">
<div className="heading">Rate A Beer</div>
<div className="form-input">
<label htmlFor="name">Beer:</label>
<input
type="text"
name="name"
id="name"
onChange={this.handleChange}
/>
</div>
<div className="form-input">
<label htmlFor="description">Description:</label>
<textarea
name="description"
id="description"
onChange={this.handleChange}
/>
</div>
<div className="form-input rating">
<label htmlFor="rating">Rating:</label>
<StarRating
numberOfStars="5"
currentRating="0"
onClick={this.setRating}
/>
</div>
<div className="actions">
<button type="submit" onClick={this.saveRating}>
Submit Rating
</button>
</div>
</div>
);
}
}
export default withAuth(RatingPage);
The import statements bring in the withAuth higher-order component from the @okta/okta-react package. This allows you to get the currently logged in user when saving ratings for that user. This also brings in the Firebase set up and the StarRating component.
At the bottom of the file, you wrap the RatingPage component with the withAuth higher-order component. This allows you to get the currently logged in user in the componentDidMount() function and add the user’s email address to the state. This will be saved with their ratings so that when they go to the RatingList page, they will only see their ratings.
The handleChange() function handles the changing of the text values for the beer name and description in the component’s form. The setRating() handler is what is passed to the rating component so that when a user clicks on a rating, the value is propagated back to the parent and, in this case, is added to the state.
The saveRating() function gets the reference to the Firebase store and pushes a new rating into the collection then the application is routed to the RatingList page.
The render() method is pretty standard except where you add the StarRating component. You set the numberOfStars to five for this rating system, then set the currentRating to zero. You could set it to two or three if you think that looks better. Finally, the reference to the click handler is passed to the StarRating component, so that when a user chooses a rating, the value is bubbled back up to the click handler on this page component.
import React, { Component } from 'react';
import { withAuth } from '@okta/okta-react';
import {BrewstrRef} from '../../firebase';
import './rating-list.css';
class RatingsListPage extends Component {
constructor(props){
super(props);
this.state = {
ratings: [],
user:''
};
}
async componentDidMount(){
const user = await this.props.auth.getUser();
BrewstrRef.orderByChild('user').equalTo(user.email).on('value', snap => {
const response = snap.val();
const ratings = [];
for(let rating in response){
ratings.push({id: rating, ...response[rating]});
}
this.setState({
ratings: ratings
});
});
}
render(){
return (
<table className="ratings-list">
<thead>
<tr>
<th>Beer</th>
<th>Description</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
{this.state.ratings.map((rating) => {
return (
<tr className="rating" key={rating.id}>
<td>{rating.name}</td>
<td>{rating.description}</td>
<td className="rating-value">{rating.rating}</td>
</tr>
)
})}
</tbody>
</table>
)
}
}
export default withAuth(RatingsListPage);
This componentDidMount() is getting the currently logged in user and passing it to Firebase to get a list of the ratings that this user has entered. All queries to Firebase return a “snapshot” represented here by the variable snap and it is pushed onto an array and then the state is set with that array. If you push each “record” onto the array in the state object, the component will redraw each time one is pushed. That’s the reason you push onto another array and then only update the state once. The render() function merely lists the ratings in a table.
All the routing is going to “fake” components that just spit out text right now. You’ll need to go back to the App.js file and make sure the routes are hooked to the components you just created. so that the final file contents are.
import React from 'react';
import { Link, Route } from 'react-router-dom';
import { SecureRoute, ImplicitCallback } from '@okta/okta-react';
import RatingPage from './pages/rating/rating-page';
import RatingsListPage from './pages/rating-list/rating-list';
import './App.css';
function App() {
return (
<div className="App">
<nav>
<Link to="/">Home</Link>
<Link to="/rating">Rate</Link>
<Link to="/ratinglist">My Ratings</Link>
</nav>
<main>
<Route exact path="/" component={()=> 'Home Page')} />
<SecureRoute exact path="/rating" component={RatingPage} />
<SecureRoute exact path="/ratinglist" component={RatingsListPage} />
<Route path="/implicit/callback" component={ImplicitCallback} />
</main>
</div>
);
}
export default App;
Here, you just added the imports for the component pages you just created, then updated or added routes to those components.
I also added some styling to my menu in App.css in the src folder:
nav {
background-color: #333;
font-size: 1.5rem;
}
nav a {
display: inline-block;
color: white;
padding: 1rem;
text-decoration: none;
}
nav a:hover {
background-color: black;
}