I have a Playlist component that is importing n number of <AudioPlayer /> components dependent on how many tracks are in the playlist.
What I need to do is pause all other <AudioPlayer /> components except the one that is currently playing. Otherwise, you can play multiple players at once which is no bueno.
In my Playlist component, I'm able to grab both the currently playing player as well as the 'not playing' players: when a player is played, it fires the onPlay() handler which grabs the refs of all of the currently mounted players, then I compare that to the playlist and finally, extract the players that are currently not playing.
What I'm stuck in is updating the state of the players so that when I press play on a different player, all of the other players are paused.
The <AudioPlayer /> components have a prop of playing which I should be able to use to pass down the updated playing state when a new player is played.
How can I do this?
Here is my Playlist.js code:
import React, { Component } from 'react'
import AudioPlayer from 'react-cl-audio-player'
const streamUrl = 'https://mystreamurl.com/'
const waveUrl = 'https://mywaveformurl.com/waveforms/'
class Playlist extends Component {
state = {
playlist: this.props.tracks,
playing: false,
currentPlayer: '',
notPlaying: []
}
onPlay = (refName, index) => {
// Get list of AudioPlayer components from refs
const players = Object.keys(this.refs).map((key, i) => {
return this.refs[key]
})
if (players) {
// get AudioPlayer that is playing
const currentPlayer = players[index]
// get playlist items that are not playing
const notPlaying = this.state.playlist.filter(
(track, trackIndex) => {
return trackIndex !== index
}
)
// Compare playlist items that are not playing to AudioPlayer list
// to find AudioPlayer components that are not playing
var notPlayingPlayers = players.filter(function(obj) {
return notPlaying.some(function(obj2) {
return obj.props.alt == obj2.track_audio
})
})
console.log('Not Playing Players', notPlayingPlayers)
// ^^ this gets me the <AudioPlayer /> components that arent playing
const notPlayingPause = notPlayingPlayers.map((item, x) => {
this.setState({
playing: false // I know this isn't correct.
})
})
}
}
render() {
const tracks = this.props.tracks
const sku = this.props.sku
const price = this.props.price
const addToCart = this.props.addToCart
const trackList = tracks.map((track, index) => {
const songs = [
{
url: `${streamUrl}${track.track_audio}_128.mp3`,
cover: '',
artist: {
name: ' ',
song: track.track_title
}
}
]
return (
<li
key={sku + '-' + track.track_audio}
className="playlist-track"
>
<span className="playlist-name">{track.track_title}</span>
<AudioPlayer
key={track.track_audio}
index={index}
songs={songs}
autoplay={false}
src={`${waveUrl}${track.track_audio}.png`}
alt={track.track_audio}
ref={track.track_audio + index}
onPlay={this.onPlay.bind(this, 'player', index)}
playing={this.state.playing}
/>
<span className="playlist-price">$1.99</span>
<button
type="button"
className="button playlist-button"
onClick={() => {
addToCart({
product_id: track.track_product_id,
quantity: 1,
price: price,
cart_item_data: {
name: track.track_title
}
})
}}
>
Add To Cart
</button>
</li>
)
})
return (
<div className="playlist-wrap">
<ul className="playlist">{trackList}</ul>
</div>
)
}
}
export default Playlist
Here's an image if it helps to make it more clear:
Related
I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link
I am building a simple music player but where I fail is at trying to execute one item from the array at a time. I am using React H5 Audio Player package to play the music. I am currently mapping through all the songs but I don't know how to properly play one at a time. I appreciate any help. I've been stuck on this for a few days.
import { SongContext } from '../../SongContext';
import AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';
import './Player.css';
const Player = () => {
const { songs } = useContext(SongContext);
return (
<>
{songs.length > 0 &&
songs.map((song) => (
<div className="player" key={song.id}>
<AudioPlayer
// autoPlay
// src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
/>
</div>
))}
</>
);
};
export default Player;
Don't map all the songs at once, take a song by index (currentSong), and when it's done, use the onEnded event to increment the index, so the next one would play.
Example (codepen):
const Player = () => {
const { songs } = useContext(SongContext);
const [currentSong, setCurrentSong] = useState(0);
const song = songs[currentSong];
if(!song) return null; // don't render the player when no song is available
return (
<div className="player">
<AudioPlayer
autoPlay
src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
onEnded={() => setCurrentSong(i => i + 1)}
/>
</div>
);
};
I'm trying to create an onClick event, that will select the pressed mapped item. Could anyone help?
const renderExample= () => {
return example.map((arrayItem, i) => {
const example = arrayItem.example;
const song =
arrayItem.song ||
"urltosong";
...
then in return
<div key={i}>
<SELECTABLE>
{example}
</SELECTABLE>
<SONG>{song}</SONG>
</div>
render return <div>{renderData()}</div>;
At the moment I have a list of selectable'examples' rendering. But I want to know which example has been pressed by the user specifically.
You can pass the complete item to the handleClick to play around. The code would be
const App = () => {
function handleClick(item){
console.log('item,item);
}
function renderData(){
// assuming you have data in example array
return example && example.map(item=> {
const example = arrayItem.example;
const song =arrayItem.song || "urltosong";
return (
<div key={i} onClick={()=>{handleClick(item)}>
<SELECTABLE>
{example}
</SELECTABLE>
<SONG>{song}</SONG>
</RecentMessages>
</div>
)
})
}
return <div>{renderData()}</div>
}
Maintain state for selected song and update when selection change.
Here is minimal working sample with stackblitz
import React, { Component } from 'react';
import { render } from 'react-dom';
const songs = ["first song", "song 2", "hello song"];
class App extends Component {
constructor() {
super();
this.state = {
song: 'hello song'
};
}
render() {
return (
<div>
<select onChange={(ev) => this.setState({song: ev.target.value})} value={this.state.song}>
{songs.map(x => <option value={x}> {x} </option>)}
</select>
<p>
{this.state.song}
</p>
</div>
);
}
}
render(<App />, document.getElementById('root'));
You can play with it like this:
// Supposing you're using React Functional Component
// Click handler
const handleSelect = (elementIndex) => {
console.log(`You clicked on element with key ${elementIndex}`)
}
// render()
// ... some other to-render stuff
// implying that code below have access to element's index (i in your map)
<Selectable onClick={e => handleSelect(i)}>
{example}
</Selectable>
// ...
I am creating a Tic-Tac-Toe Game and i reached the point i set an array of objects players as a state, each object in the array contains an id represents the id of clicked square and the player which is 'X' or 'O', but i couldn't render them on their corresponding squares, How can i use that array of objects to put the letter in the right place
const Game = () => {
//example [{id: 5, player: 'X'}, {id: 0, player; 'O'}]
const [players, setPlayers] = useState([]);
const [player, setPlayer] = useState('X')
const [picks, setPicks] = useState([])
const handleSquareClick = (id) => {
!picks.includes(id) && setPicks([...picks, id])
player === 'X' ? setPlayer('O') : setPlayer('X');
setPlayers(players => [...players, { id, player }])
}
return (
<div class="game">
<ul class="squares">
{
[...Array(9)].map((_, idx) => {
return (
<Square
players={players}
handleSquareClick={handleSquareClick}
id={idx}
picks={picks}
/>
)
})
}
</ul>
</div>
)
}
const Square = ({ players, handleSquareClick, id, picks }) => {
return (
<li
className="square"
onClick={() => handleSquareClick(id)}
>
{}
</li>
)
}
I would be thankful if someone told me how to make it or what is a better way to do it from the start (a better logic)
codesandbox
So I saw multiple things that could be fixed.
First, I'm not sure what players should do in the code (seems to me like a pick history) so I didnt removed it, but if not needed you can remove it. In this code it is not used (only for updating it).
Now, I changed the picks value to be an array of 9 which every element is a string (equals X or O), and the index to the element is the id of the Square clicked.
So if i clicked on Square 4 and I'm player X, then picks[4]="X".
Now I fixed another thing. You changed the current player (with setPlayer) before checking if the pick was valid. Then you could get into a situation that a player shouldn't pick a square because it was already picked, and you didn't update the square, but the other player has now his turn. So, I put the player === "X" ? setPlayer("O") : setPlayer("X"); inside the validity check of the square (if (picks[id] === null)).
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Game />
</div>
);
}
const Game = () => {
const [players, setPlayers] = useState([]);
const [player, setPlayer] = useState("X");
const [picks, setPicks] = useState(new Array(9).fill(null));
const handleSquareClick = id => {
setPlayers(players => [...players, { id, player }]);
if (picks[id] === null) {
let newArr = picks;
newArr[id] = player;
setPicks(newArr);
player === "X" ? setPlayer("O") : setPlayer("X");
}
};
Finally, I passed to Square a new property called squarePick which should let the specific Sqaure know, what string to render (using the xidxand the array ofpicks`).
return (
<div class="game">
<ul class="squares">
{[...Array(9)].map((_, idx) => {
return (
<Square
handleSquareClick={handleSquareClick}
id={idx}
squarePick={picks[idx]}
/>
);
})}
</ul>
</div>
);
};
const Square = ({ handleSquareClick, id, squarePick }) => {
return (
<li className="square" onClick={() => handleSquareClick(id)}>
{squarePick}
</li>
);
};
Hope that helped you and it was understandable.
I am attempting to have the next video in a channel play right after the other. Currently, the website has the videos showing one after the other, but my goal is to show one video and the second one plays right after the other is done. I have the function set up for the video ending, but right now it just causes an alert. I am using the Youtube Data API to pull in the videos and their information.
Here is a snippet of the code I am using:
constructor() {
super();
this.state = {
videos: [],
};
}
componentDidMount() {
fetch('https://www.googleapis.com/youtube/v3/search?key='APIKey'&channelId=UCXIJgqnII2ZOINSWNOGFThA&part=snippet,id&order=date&maxResults=2')
.then(results => {
return results.json();
}).then(data => {
let videos = data.items.map((videos) => {
return(
<div key={videos.items}>
<YouTube
className="player"
id="video"
videoId={videos.id.videoId}
opts={VIDEO_OPTS}
onEnd={this.playNextVideo}
/>
<h2>{videos.snippet.title}</h2>
<p className="channel">Video by: {videos.snippet.channelTitle}</p>
</div>
);
});
this.setState({videos: videos});
console.log("state", this.state.videos);
})
}
playNextVideo = () => {
alert('The video is done!');
}
I suggest you to do few things a little bit different.
First save the results.json(); to your videos variable in the state and not the whole youtube component, that's bad practice.
Second save another variable in your state that indicates the current playing video id (playingVideoId). Initialize it in the componentDidMount and change it in your playNextVideo function like this:
constructor() {
super();
this.index=0;
}
componentDidMount() {
fetch('https://www.googleapis.com/youtube/v3/search?key='APIKey'&channelId=UCXIJgqnII2ZOINSWNOGFThA&part=snippet,id&order=date&maxResults=2').then(results => {
this.setState({videos: results.json()});
this.setState({playingVideoId: this.state.videos[this.index]});
})}
playNextVideo = () => {
this.setState({playingVideoId: this.state.videos[++this.index]});
}
Now use the render function to render the component
render() {
return(
<YouTube
className="player"
id="video"
videoId={this.state.playingVideoId}
opts={VIDEO_OPTS}
onEnd={this.playNextVideo}
/>
);
}