I'm trying to create a header component that changes based on which screen you're viewing. However, the child component is not working if I try to create some dynamic buttons.
I have split each part of the header into smaller components, the problem is that the right side of the header HeaderRightSide is not rendering unless I add each button myself.
const screenIcon = {
Home: 'home',
Settings: 'tools'
}
export class AppHeader extends Component {
static propTypes = {
screenName: PropTypes.string.isRequired,
navigation: PropTypes.object.isRequired,
googleUser: PropTypes.object.isRequired,
UserSignIn_logOut: PropTypes.func.isRequired
}
signOut = async () => {
try {
await GoogleSignin.revokeAccess();
await GoogleSignin.signOut();
} catch (error) {
// console.error(error);
} finally {
this.props.UserSignIn_logOut({});
this.props.navigation.navigate('SignIn');
}
}
componentDidMount() {
console.log(this.props.navigation.state)
}
render() {
return (
<Header>
<HeaderLeftSide screenName={this.props.screenName} />
<HeaderBody screenName={this.props.screenName} />
<HeaderRightSide screenName={this.props.screenName} navigation={this.props.navigation} />
</Header>
)
}
}
HeaderLeftSide = (props) => (
<Left>
<Button transparent>
<Icon name={screenIcon[props.screenName]} />
</Button>
</Left>
)
HeaderBody = (props) => (
<Body>
<Title>{props.screenName}</Title>
</Body>
)
HeaderRightSide = (props) => (
<Right>
{Object.keys(screenIcon).forEach(screen =>
( <Button transparent>
<Icon name={screenIcon[screen]} />
</Button>)
)}
</Right>
)
const mapStateToProps = (state) => ({
googleUser: state.UserSignInReducer.googleUser
})
const mapDispatchToProps = {
UserSignIn_logOut: () => {
dispatch(UserSignIn_logOut());
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AppHeader)
I just had to change forEach to map. Correct code:
HeaderRightSide = (props) => (
<Right>
{Object.keys(screenIcon).map(screen => (
<Button transparent>
<Icon name={screenIcon[screen]} />
</Button>
)
)}
</Right>
)
Related
I want to integrate a search functionality in my Next.js app which should look like this:
User types a keyword in searchBar which is inside of Header
After clicking on the search button the keyword should be passed on the SearchResult page, in which I load it dynamically into the youtube-search-api link
The structure looks like this:
Header.js (contains SearchBar component)
_app.js (contains Header and components)
SearchResults.js (inside pages folder, should fetch typed keyword from Header)
I tried it with zustand and it looks like this currently:
Header.js:
import SearchBar from "./SearchBar";
const Header = () => {
return (
<div className={styles.header}>
...
<SearchBar />
...
</div>
);
};
export default Header;
SearchBar.js:
import create from 'zustand';
import { useRouter } from 'next/router';
import Image from 'next/image';
import styles from '../styles/Header.module.scss';
import loupe from '../public/images/loupe.png';
const useStore = create((set) => ({
keyword: '',
setKeyword: (keyword) =>
set((state) => ({
...state,
keyword,
})),
}));
const SearchBar = () => {
const router = useRouter();
const keyword = useStore((state) => state.keyword);
const setKeyword = useStore((state) => state.setKeyword);
const handleClick = (e) => {
e.preventDefault();
router.push('/searchResults');
};
return (
<div className={styles.searchBar}>
<input
type='text'
value={keyword}
placeholder='Suche...'
onChange={(e) => setKeyword(e.target.value)}
/>
<button className={styles.searchBtn} type='submit' onClick={handleClick}>
<Image src={loupe} alt='' />
</button>
</div>
);
};
export default SearchBar;
_app.js:
import Header from '../components/Header';
function MyApp({ Component, pageProps }) {
return (
<>
<Header />
<Component {...pageProps} />
</>
);
}
export default MyApp;
and SearchResults.js:
import { fetchData } from '../lib/utils';
import Moment from 'react-moment';
import { Modal } from 'react-responsive-modal';
import ReactPlayer from 'react-player/youtube';
export default function SearchResults({ videos }) {
console.log(videos);
const [modalIsOpen, setModalIsOpen] = useState(false);
const [modalData, setModalData] = useState(null);
const videoURL = 'https://www.youtube.com/watch?v=' + modalData;
const sortedVids = videos
.sort((a, b) =>
Number(
new Date(b.snippet.videoPublishedAt) -
Number(new Date(a.snippet.videoPublishedAt))
)
)
.filter((vid) => vid.snippet.title.toLowerCase());
return (
<>
<div className={`${styles.playlist_container} ${styles.search}`}>
<div className={styles.main_container}>
<h1>Search results</h1>
{sortedVids
.filter((v) => v.snippet.title !== 'Private video')
.map((vid, id) => {
return (
<div className={styles.item_container}
key={id}>
<div className={styles.clip_container}>
<Image
className={styles.thumbnails}
src={vid.snippet.thumbnails.medium.url}
layout='fill'
objectFit='cover'
alt={vid.snippet.title}
/>
<button
className={styles.playBtn}
onClick={() => {
setModalData(vid.snippet.resourceId.videoId);
console.log(modalData);
setModalIsOpen(true);
}}
>
<Image src='/images/play.svg' width='60' height='60' />
</button>
</div>
<div className={styles.details_container}>
<h3>{vid.snippet.title}</h3>
</div>
</div>
);
})}
</div>
</div>
<div>
<Modal
open={modalIsOpen}
onClose={() => setModalIsOpen(false)}
center
classNames={{
overlay: 'customOverlay',
modal: 'customModal',
overlayAnimationIn: 'customEnterOverlayAnimation',
overlayAnimationOut: 'customLeaveOverlayAnimation',
modalAnimationIn: 'customEnterModalAnimation',
modalAnimationOut: 'customLeaveModalAnimation',
}}
animationDuration={800}
>
<ReactPlayer
playing={true}
url={videoURL}
width='100%'
height='calc(100vh - 100px)'
config={{
youtube: {
playerVars: {
autoplay: 1,
controls: 1,
},
},
}}
/>
</Modal>
</div>
<Footer />
</>
);
}
export async function getStaticProps() {
const keyword = useStore((state) => state.keyword);
const { YOUTUBE_KEY } = process.env;
const uploadsURL = `https://youtube.googleapis.com/youtube/v3/search?part=snippet&channelId=UCbqKKcML7P4b4BDhaqdh_DA&maxResults=50&key=${YOUTUBE_KEY}&q=${keyword}`;
async function getData() {
const uploadsData = fetchData(uploadsURL);
return {
videos: await uploadsData,
};
}
const { videos } = await getData();
return {
revalidate: 86400,
props: {
videos: videos.items,
},
};
}
Would someone please help me out by telling me what I did wrong and how it works in the right way? Thank you guys!!
The following code renders a like object when its clicked.
So when console.log(nextProps.myPosts) exectues
it goes from this
Likes(95)
to this
Likes(96)
Currently im getting the like counts like
myLikes={post.Likes.length}
And its not showing the reflected change on the ui, so what should i change so it can reflect the updated prop ?
Posts.js
class Posts extends Component {
state = {
posts: [],
loading: true,
isEditing: false,
// likes:[]
}
componentWillMount(){
this.props.GetPosts();
this.setState({
loading:false
})
}
componentWillReceiveProps(nextProps, prevState) {
let hasNewLike = true ;
if(prevState.posts && prevState.posts.length) {
for(let index=0; index < nextProps.myPosts.length; index++) {
if(nextProps.myPosts[index].Likes.length !=
prevState.posts[index].Likes.length) {
hasNewLike = false;
}
}
this.setState({posts: nextProps.myPosts}); // here we are updating the posts state if redux state has updated value of likes
console.log(nextProps.myPosts)
}
render() {
const {loading} = this.state;
const { myPosts} = this.props
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList posts={this.state.posts}/>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.user.isAuthenticated,
myPosts: state.post.posts,
})
const mapDispatchToProps = (dispatch, state) => ({
GetPosts: () => dispatch( GetPosts())
});
export default withRouter(connect(mapStateToProps,mapDispatchToProps)(Posts));
PostList.js
class PostList extends Component{
constructor(props){
super(props);
this.state ={
title: '',
}
}
// Return a new function. Otherwise the DeletePost action will be dispatch each
// time the Component rerenders.
removePost = (id) => () => {
this.props.DeletePost(id);
}
onChange = (e) => {
e.preventDefault();
this.setState({
title: e.target.value
})
}
formEditing = (id) => ()=> {;
this.props.EditChange(id);
}
render(){
const {posts} = this.props;
return (
<div>
{posts.map(post => (
<Paper key={post.id} style={Styles.myPaper}>
<PostItem
myLikes={post.Likes.length} // right here
myTitle={this.state.title}
editChange={this.onChange}
editForm={this.formEditing}
isEditing={this.props.isEditingId === post.id}
removePost={this.removePost}
{...post}
/>
</Paper>
))}
</div>
);
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
// ourLikes: state.post.likes // reducer likes
})
const mapDispatchToProps = (dispatch) => ({
// pass creds which can be called anything, but i just call it credentials but it should be called something more
// specific.
EditChange: (id) => dispatch(EditChange(id)),
UpdatePost: (creds) => dispatch(UpdatePost(creds)),
postLike: (id) => dispatch( postLike(id)),
// Pass id to the DeletePost functions.
DeletePost: (id) => dispatch(DeletePost(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
reducer
const initialState = {
post: [],
postError: null,
posts:[],
isEditing:false,
isEditingId:null,
likes:[],
someLike:[],
postId:null
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
console.log(action.data)
return {
...state,
posts: action.data, // maps posts fine,
}
case ADD_LIKE:
const newState = {...state}; // here I am trying to shallow copy the existing state;
const existingLikesOfPost = newState.posts.find(post => post.id == action.id).Likes;
newState.posts.find(post => post.id == action.id).Likes = [...existingLikesOfPost, action.newLikeObject]; // using this approach I got some code duplication so I suggested the first approach of using **push** method of array.
return newState
Like component
import React, { Component } from 'react';
import ReactDOM from 'react-dom'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faCoffee, faAdjust } from '#fortawesome/free-solid-svg-icons';
import {connect} from 'react-redux';
import { postLike} from '../actions/';
class Like extends Component{
constructor(props){
super(props);
this.state = {
likes: null,
heart: false
}
}
// passes post id thats stored in PostItem.js
clickLike = (id) => {
this.props.postLike(id);
// toggles between css class
this.setState({
heart: !this.state.heart
})
}
render(){
return(
<div style={{float:'right', fontSize: '1.5em', color:'tomato'}} >
<i style={{ marginRight: '140px'}} className={this.state.heart ? 'fa fa-heart':'fa fa-heart-o' }>
<span style={{ marginLeft: '6px'}}>
<a href="#" onClick={() =>this.clickLike(this.props.like)}>Like</a>
</span>
{/* gets the like counts */}
<span style={{ marginLeft: '7px'}} >{this.props.likes} </span>
</i>
</div>
)
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
// myLikes: state.post.likes
})
const mapDispatchToProps = (dispatch) => ({
postLike: (id) => dispatch( postLike(id))
// Pass id to the DeletePost functions.
});
export default connect(mapStateToProps, mapDispatchToProps)(Like);
PostItem.js
.....
class PostItem extends Component{
constructor(props){
super(props);
this.state = {
disabled: false,
}
}
onUpdate = (id, title) => () => {
// we need the id so expres knows what post to update, and the title being that only editing the title.
if(this.props.myTitle !== null){
const creds = {
id, title
}
this.props.UpdatePost(creds);
}
}
render(){
const {title, id, userId, removePost, createdAt, post_content, username, editForm, isEditing, editChange, myTitle, postUpdate, Likes, clickLike, myLikes} = this.props
return(
<div>
<Typography variant="h6" component="h3">
{/* if else teneray operator */}
{isEditing ? (
<Editable editField={myTitle ? myTitle : title} editChange={editChange}/>
): (
<div>
{title}
</div>
)}
</Typography>
<Typography component={'span'} variant={'body2'}>
{post_content}
<h5>by: {username} </h5>
{/* component span cancels out the cant be a decedent of error */}
<Typography component={'span'} variant={'body2'} color="textSecondary">{moment(createdAt).calendar()}</Typography>
{/* gets like counts */}
<Like like={id} likes={myLikes} />
</Typography>
{!isEditing ? (
<Button variant="outlined" type="submit" onClick={editForm(id)}>
Edit
</Button>
):(
// pass id, and myTitle which as we remember myTitle is the new value when updating the title
<div>
<Button
disabled={myTitle.length <= 3}
variant="outlined"
onClick={this.onUpdate(id, myTitle)}>
Update
</Button>
<Button
variant="outlined"
style={{marginLeft: '0.7%'}}
onClick={editForm(null)}>
Close
</Button>
</div>
)}
{!isEditing && (
<Button
style={{marginLeft: '0.7%'}}
variant="outlined"
color="primary"
type="submit"
onClick={removePost(id)}>
Remove
</Button>
)}
</div>
)
}
}
const mapStateToProps = (state) => ({
isEditingId: state.post.isEditingId,
})
const mapDispatchToProps = (dispatch) => ({
// pass creds which can be called anything, but i just call it credentials but it should be called something more
// specific.
UpdatePost: (creds) => dispatch(UpdatePost(creds)),
postLike: (id) => dispatch( postLike(id)),
// Pass id to the DeletePost functions.
});
export default connect(mapStateToProps, mapDispatchToProps)(PostItem);
Following what you wrote, shouldn't hasNewLike be set to true if the comparison of the two lengths are not equal? Meaning that it has a new like? :)
That way we can update the component state and pass that into your child PostList component.
if(nextProps.myPosts[index].Likes.length !=
prevState.posts[index].Likes.length) {
hasNewLike = true;
}
I have some trouble with my code. I made an app where I use an API last fm and I want to add a rating button, I get few things from Google. Rating is displayed where I want to be, I can console log him, but it's on external file and I have no idea how to modify rate state from my app.js. Here is my code:
App.js
import React, { Component } from 'react';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import { getArtists } from './services/api';
import {
TextField,
Button,
List
} from '#material-ui/core';
import { ArtistCard } from './components/ArtistCard';
import { SearchResult } from './components/SearchResult';
import './App.css';
import { get } from 'https';
const handleChangeRate = (state) => {
this.setState({rate: state})
}
const isEmpty = (str) => str.length === 0;
class App extends Component {
state = {
searchTerm: '',
savedArtists: [],
rate:[]
}
componentDidMount() {
const existing = localStorage.getItem('savedArtists')
if (existing) {
this.setState({ savedArtists: JSON.parse(existing) })
}
}
onTextChange = (event) => {
const value = event.target.value;
this.setState({ searchTerm: value });
}
search = async (terms) => {
const artists = await getArtists(terms);
this.setState({ artists: artists })
}
onSearchClick = () => {
this.search(this.state.searchTerm);
}
clearSearch = () => {
this.setState({
searchTerm: '',
artists: []
})
}
updateArtists = (newArtists) => {
this.setState({ savedArtists: newArtists })
localStorage.setItem('savedArtists', JSON.stringify(newArtists));
}
deleteArtist = (artist) => {
const result = this.state.savedArtists.filter(item => item.name !== artist.name);
this.updateArtists(result);
}
onResultClick = (artist) => {
this.clearSearch();
const savedArtists = this.state.savedArtists;
savedArtists.push(artist);
this.updateArtists(savedArtists);
}
handleChangeRate = (state) => {
this.setState({rate: state})
}
render() {
const results = this.state.artists || [];
return (
<div className="App">
<header className="App-header">
<AppBar position="static" color="primary">
<Toolbar className="search-bar">
<Typography variant="h6" color="inherit">
Photos
</Typography>
<TextField
placeholder="Search on Last.fm"
className="search-input"
onChange={this.onTextChange}
value={this.state.searchTerm}
/>
<Button
onClick={this.onSearchClick}
variant="contained"
color="secondary"
disabled={isEmpty(this.state.searchTerm)}
>
Search
</Button>
{!isEmpty(this.state.searchTerm) && (
<Button
onClick={this.clearSearch}
variant="contained"
>
Clear
</Button>)
}
</Toolbar>
</AppBar>
</header>
<List className="search-results">
{
results.map((artist, index) => {
return <SearchResult key={index} artist={artist} onResultClick={this.onResultClick} />
})
}
</List>
<div className="artist-container">
{
this.state.savedArtists.map((artist, index) => {
return <ArtistCard artist={artist} key={index} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
})
}
</div>
</div>
);
}
}
export default App;
artistCard.js
import React from 'react';
import { Card, CardContent, CardActions, Button } from '#material-ui/core'
import ReactStars from 'react-stars'
export const ratingChanged = (newRating) => {
const { onChangeRating } = this.props;
onChangeRating(newRating);
}
export const ArtistCard = (props) => {
const { artist, deleteArtist } = props;
console.log(artist.cardImage)
return (
<Card className="artist-card">
<div className="image-container">
<img src={artist.cardImage} alt={artist.name} />
</div>
<CardContent>
<h3>{artist.name}</h3>
<p>{artist.listeners} listeners.</p>
<ReactStars
count = {5}
onChange={ratingChanged}
size={27}
color2 ={'#ffd700'}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary">
Share
</Button>
<Button size="small" color="secondary" onClick={() => deleteArtist(artist)}>
Delete
</Button>
</CardActions>
</Card>
)
}
You need to pass the function to change State to artistCard as props
In App.js add the following fucntion
const handleChangeRate = (state) => {
this.setState(rate: state)
}
and Pass the same as props to ArtistCard like specified
<ArtistCard artist={artist} key={index} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
And in artistCard.js change ratingChanged method to
const ratingChanged = (newRating) => {
const { onChangeRating } = this.props;
onChangeRatng(newRating);
}
PS: This answer is based on the understanding i gained after going through this question, If this is not the requirement Please feel free to comment
EDIT
const handleChangeRate = (state) => {
this.setState(rate: state)
}
try adding the value prop to ReactStart like specified below
<ReactStars
value={this.props.rate}
count={5}
onChange={ratingChanged}
size={24}
color2={'#ffd700'}
/>
Pass rate state to artist card component as prop like specified below
<ArtistCard artist={artist} key={index} deleteArtist= {this.deleteArtist} onChangeRating={this.handleChangeRate} rate={this.state.rate} />
EDIT
cardComponent.js
import React from 'react';
import ReactStars from 'react-stars'
export const ArtistCard = (props) => {
const { artist, deleteArtist, onChangeRating } = props;
console.log(props.rating)
return (
<ReactStars
value={props.rating}
count = {5}
onChange={(newRating) => onChangeRating(newRating)}
size={27}
color2 ={'#ffd700'}
/>)}
App.js
handleChangeRate = (state) => {
this.setState({rate: state})
}
<ArtistCard artist={'artist'} key={'index'} rating={this.state.rate} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
FINAL EDIT
Changes made in your code
App.js
modified state object to
state = {
searchTerm: '',
savedArtists: [],
rate: ''
}
Artistcard component line to
<ArtistCard rate={this.state.rate} artist={artist} key={index} onChangeRating={(val) => {this.setState({ rate: val })}} deleteArtist={this.deleteArtist} />
In ArtistCard.js
rendering reactstart component like this
<ReactStars
value={props.rate}
count = {5}
onChange={(newRating) => onChangeRating(newRating)}
size={27}
color2 ={'#ffd700'}
/>
I have the problem that i cannot find a way to get the mapDispatchToProps into my react component. since it is a class.
This is the class where i have the problem, i get the error: 'findPosts' is not defined.
const mapDispatchToProps = {
findPosts: searchPosts
};
class SearchMenu extends React.Component {
state = {
anchorEl: null,
value: ''
};
handleChange = event => {
this.setState({value: event.target.value,});
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { anchorEl } = this.state;
return (
<Fragment>
<IconButton onClick={this.handleClick}>
<SearchIcon color="secondary" />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
>
<MenuItem>
<CategorySelector />
</MenuItem>
<MenuItem>
<TextField
value={this.state.value}
onChange={this.handleChange}
/>
<IconButton onClick={() => { findPosts(this.state.value); }}>
<SearchIcon color="secondary" />
</IconButton>
</MenuItem>
</Menu>
</Fragment>
);
}
}
export default connect(null,mapDispatchToProps)(SearchMenu);
In other components I was able to do this because I could use something like this:
const mapDispatchToProps = {
getPosts: fetchPosts
};
const Reactions = ({ getPosts}) => (
<LinkButton onClick={() => {getPosts("messages");}}>
Back to posts
</LinkButton>
);
export default connect(null, mapDispatchToProps)(Reactions);
But because I need the this.setState in the bit of code, I am not able to change it like in the second example.
In class you have to use this.props.findPosts(this.state.value)
In react-admin, I want to create a toggle button which on the basis of default value from database will allow user to change the state and make corresponding changes both in the Show as well as in backend database.
Currently My code looks like this:
default class Deploy extends React.Component<{ data: any }> {
handleClick = () => {
const status = this.props.data.status;
alert(status);
};
render() {
const status = this.props.data.status;
return (
<Fragment>
<Button color="primary" onClick={this.handleClick}>
<ActionEject />
{status === "DEPLOYED" ? "UNDEPLOY" : "DEPLOY"}
</Button>
</Fragment>
);
}
}
class Actions extends React.Component<{ basePath: any; data: any; resource: any }, any> {
render() {
const basePath = this.props.basePath;
const data = this.props.data;
const resource = this.props.resource;
if (this.props.data) {
const defaultValue = this.props.data.default;
return (
<Deploy data={data} />
);
}
return null;
}
}
export default class ModelShow extends React.Component {
render() {
return (
<Show title={<ModelName />} actions={<Action />} {...this.props}>
<TabbedShowLayout>
<Tab label="Summary">
<TextField source="id" />
<TextField source="status" />
</Tab>
</TabbedShowLayout>
</Show>
);
}
}
P.S: I'm using Typescript.
You'll find multiple examples in the documentation for Actions
In order to correctly update the react-admin state, you should follow either the Using a Data Provider Instead of Fetch example or the Using a Custom Action Creator example.
Here's an example using the dataProvider directly:
// Import the UPDATE verb
import { UPDATE } from 'react-admin';
// Import your dataProvider
import dataProvider from '../dataProvider';
default class Deploy extends React.Component<{ data: any }> {
handleClick = () => {
const status = this.props.data.status;
const { push, record, showNotification } = this.props;
const updatedRecord = { ...record, status };
dataProvider(UPDATE, 'you_resource_name', { id: record.id, data: updatedRecord })
.then(() => {
// Optional notification
showNotification('Resource deployed');
// Optional redirection to the list page
push('/you_resource_name');
})
.catch((e) => {
console.error(e);
showNotification('Error: resource not deployed', 'warning')
});
};
render() {
const status = this.props.data.status;
return (
<Fragment>
<Button color="primary" onClick={this.handleClick}>
<ActionEject />
{status === "DEPLOYED" ? "UNDEPLOY" : "DEPLOY"}
</Button>
</Fragment>
);
}
}