Creating an animation for a basic React slot machine - javascript

I'm quite new to react and a simple slot machine is my first mini-project. Currently I have completed the logic for displaying random emojis after the button is pressed. The next step for me, before styling and adding logic for winning/losing and a counter for coins etc is adding an animation.
https://codepen.io/fmressel/pen/vRLerN
This codepen is exactly the sort of thing I'm after, but as you can see it is structured quite differently to my code (below), and I'm pulling my hair out trying to figure out how I can get something similar to work for my project.
Any help much appreciated!
import React, { Component } from 'react'
import Contents from './Contents'
import './mainslots.css'
class Slots extends Component {
static defaultProps = {
fruits: ["🍒", "🍉", "🍊", "🍓", "🍇", "🥝"]
};
constructor(props) {
super(props);
this.state = {fruit1: '🍒', fruit2: '🍒', fruit3: '🍒', rolling: false};
this.roll = this.roll.bind(this);
};
roll = () => {
const dFruit1 = this.props.fruits[
Math.floor(Math.random() * this.props.fruits.length)
];
const dFruit2 = this.props.fruits[
Math.floor(Math.random() * this.props.fruits.length)
];
const dFruit3 = this.props.fruits[
Math.floor(Math.random() * this.props.fruits.length)
];
this.setState({fruit1: dFruit1, fruit2: dFruit2, fruit3: dFruit3, rolling: true});
setTimeout(() => {
this.setState({ rolling: false });
}, 700)
}
render(){
return(
<div className="SlotMachine">
<div className="SlotsContainer">
{this.state.fruit1}
{this.state.fruit2}
{this.state.fruit3}
</div>
<button className="spinner" onClick={this.roll} disabled={this.state.rolling}>
{this.state.rolling ? "Spinning..." : "Spin"}
</button>
</div>
);
}
}
export default Slots;
import React, { Component } from 'react'
class Contents extends Component {
Fruits = ["🍒", "🍉", "🍊", "🍓", "🍇", "🥝"];
render() {
return(
<div className="emptys">
{this.props.roll}
</div>
)
}
}
export default Contents

Here you go,
It was fun to develop :), you can run the below code snippet to review and I've added the comments in code, that will make things clear, please have a look,
Hope this will help,
const { createRef , Component } = React;
class Slots extends Component {
static defaultProps = {
fruits: ["🍒", "🍉", "🍊", "🍓", "🍇", "🥝"]
};
constructor(props) {
super(props);
this.state = { fruit1: "🍒", fruit2: "🍒", fruit3: "🍒", rolling: false };
// get ref of dic onn which elements will roll
this.slotRef = [createRef(), createRef(), createRef()];
}
// to trigger roolling and maintain state
roll = () => {
this.setState({
rolling: true
});
setTimeout(() => {
this.setState({ rolling: false });
}, 700);
// looping through all 3 slots to start rolling
this.slotRef.forEach((slot, i) => {
// this will trigger rolling effect
const selected = this.triggerSlotRotation(slot.current);
this.setState({ [`fruit${i + 1}`]: selected });
});
};
// this will create a rolling effect and return random selected option
triggerSlotRotation = ref => {
function setTop(top) {
ref.style.top = `${top}px`;
}
let options = ref.children;
let randomOption = Math.floor(
Math.random() * Slots.defaultProps.fruits.length
);
let choosenOption = options[randomOption];
setTop(-choosenOption.offsetTop + 2);
return Slots.defaultProps.fruits[randomOption];
};
render() {
return (
<div className="SlotMachine">
<div className="slot">
<section>
<div className="container" ref={this.slotRef[0]}>
{Slots.defaultProps.fruits.map((fruit, i) => (
<div key={i}>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div className="slot">
<section>
<div className="container" ref={this.slotRef[1]}>
{Slots.defaultProps.fruits.map(fruit => (
<div>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div className="slot">
<section>
<div className="container" ref={this.slotRef[2]}>
{Slots.defaultProps.fruits.map(fruit => (
<div>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div
className={!this.state.rolling ? "roll rolling" : "roll"}
onClick={!this.state.rolling && this.roll}
disabled={this.state.rolling}
>
{this.state.rolling ? "Rolling..." : "ROLL"}
</div>
</div>
);
}
}
ReactDOM.render(<Slots />, document.getElementById('react-root'));
.App {
font-family: sans-serif;
text-align: center;
}
.slot {
position: relative;
display: inline-block;
height: 100px;
width: 80px;
}
section {
position: absolute;
border-radius: 15px !important;
border: 3px solid black !important;
width: 70px;
height: 70px;
overflow: hidden;
background-color: grey;
border-radius: 2px;
border: 1px solid lightgrey;
color: white;
font-family: sans-serif;
text-align: center;
font-size: 25px;
line-height: 60px;
cursor: default;
}
.container {
position: absolute;
top: 2px;
width: 100%;
transition: top ease-in-out 0.5s;
text-align: center;
}
.roll {
width: 215px;
cursor: pointer;
background-color: yellow;
padding: 10px;
text-align: center;
font-size: 20px;
border-radius: 20px;
border: 3px solid black;
}
.rolling {
animation: blinkingText 1.2s infinite;
}
#keyframes blinkingText {
0% {
color: #000;
}
49% {
color: #000;
}
60% {
color: transparent;
}
99% {
color: transparent;
}
100% {
color: #000;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>

I recreated the #VivekDoshi 's answer as a react FC.
const { useRef, useState} = React;
function Slots(){
const [fruit1,setFruit1] = useState("🍒");
const [fruit2,setFruit2] = useState("🍒");
const [fruit3,setFruit3] = useState("🍒");
const [rolling,setRolling] = useState(false);
let slotRef = [useRef(null), useRef(null), useRef(null)];
const fruits = ["🍒", "🍉", "🍊", "🍓", "🍇", "🥝"]
// to trigger roolling and maintain state
const roll = () => {
setRolling(true);
setTimeout(() => {
setRolling(false);
}, 700);
// looping through all 3 slots to start rolling
slotRef.forEach((slot, i) => {
// this will trigger rolling effect
const selected = triggerSlotRotation(slot.current);
if(i+1 == 1)
setFruit1(selected);
else if(i+1 == 2)
setFruit2(selected);
else
setFruit3(selected);
});
};
// this will create a rolling effect and return random selected option
const triggerSlotRotation = ref => {
function setTop(top) {
ref.style.top = `${top}px`;
}
let options = ref.children;
let randomOption = Math.floor(
Math.random() * fruits.length
);
let choosenOption = options[randomOption];
setTop(-choosenOption.offsetTop + 2);
return fruits[randomOption];
};
return (
<div className="SlotMachine">
<div className="slot">
<section>
<div className="container" ref={slotRef[0]}>
{fruits.map((fruit, i) => (
<div key={i}>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div className="slot">
<section>
<div className="container" ref={slotRef[1]}>
{fruits.map(fruit => (
<div>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div className="slot">
<section>
<div className="container" ref={slotRef[2]}>
{fruits.map(fruit => (
<div>
<span>{fruit}</span>
</div>
))}
</div>
</section>
</div>
<div
className={!rolling ? "roll rolling" : "roll"}
onClick={!rolling && roll}
disabled={rolling}>
{rolling ? "Rolling..." : "ROLL"}
</div>
</div>
);
};
ReactDOM.render(<Slots />, document.getElementById('react-root'));
.App {
font-family: sans-serif;
text-align: center;
}
.slot {
position: relative;
display: inline-block;
height: 100px;
width: 80px;
}
section {
position: absolute;
border-radius: 15px !important;
border: 3px solid black !important;
width: 70px;
height: 70px;
overflow: hidden;
background-color: grey;
border-radius: 2px;
border: 1px solid lightgrey;
color: white;
font-family: sans-serif;
text-align: center;
font-size: 25px;
line-height: 60px;
cursor: default;
}
.container {
position: absolute;
top: 2px;
width: 100%;
transition: top ease-in-out 0.5s;
text-align: center;
}
.roll {
width: 215px;
cursor: pointer;
background-color: yellow;
padding: 10px;
text-align: center;
font-size: 20px;
border-radius: 20px;
border: 3px solid black;
}
.rolling {
animation: blinkingText 1.2s infinite;
}
#keyframes blinkingText {
0% {
color: #000;
}
49% {
color: #000;
}
60% {
color: transparent;
}
99% {
color: transparent;
}
100% {
color: #000;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>

Related

Console.log is telling me my div is null when I try and add a class to it

I'm adding a keydown eventlistener that'll simply make my box div show. However, when I press a key, nothing happens and instead the console tells me, "Uncaught TypeError: box is null".
No idea what the problem is, please help
Here's my App.js:
import "./styles.css"
const box = document.querySelector(".box");
const notification = document.querySelector(".notif");
window.onload = document.addEventListener("keydown", e => {
box.classList.add("active");
notification.classList.add("inactive");
})
function App() {
return (
<>
<div className='notif'>Press Any Key</div>
<div className='box'>
<h2>20</h2>
<h1>Shift</h1>
</div>
</>
);
}
export default App;
and the css:
#import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
:root {
--teal: #00ccff;
--pink: #d400d4;
}
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background: #0e1538;
}
.box {
position: relative;
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0,0,0,.5);
overflow: hidden;
border-radius: 20px;
flex-direction: column;
display: none;
}
.box.active {
display: flex;
}
.box::before {
content: '';
position: absolute;
width: 150px;
height: 140%;
background: linear-gradient(var(--teal), var(--pink));
animation: animate 4s linear infinite;
}
#keyframes animate {
0%{
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.box::after {
content: '';
position: absolute;
background: #0e1538;
inset: 4px;
border-radius: 16px;
}
.box h2 {
position: relative;
color: white;
font-size: 5em;
z-index: 10;
}
.box h1 {
position: relative;
color: white;
z-index: 10;
background: rgba(0,0,0,.5);
width: 97.5%;
text-align: center;
letter-spacing: 5px;
}
.notif {
font-size: 3em;
background: var(--teal);
padding: 0 20px;
border-radius: 20px;
}
.notif.inactive {
display: none;
}
You should be maintaining/updating state with the new class information when you press a key. Add the listener within the useEffect (along with a cleanup function). When a key is pressed the listener calls a function that updates the state (in this case switching the active/inactive classes). The component is re-rendered, and the elements' class names are changed using a little helper function.
const { useEffect, useState } = React;
function Example() {
// Initialise state
const [ active, setActive ] = useState({
box: false,
notification: true
});
// A function to update the state
function toggleActive() {
setActive(prev => ({
box: !prev.box,
notification: !prev.notification
}));
}
// An effect that adds a listener, w/ a cleanup function
useEffect(() => {
window.addEventListener('keydown', toggleActive);
return () => {
window.removeEventListener('keydown', toggleActive);
}
}, []);
// Returns the active status of the element from state
function isActive(type) {
return active[type] ? 'active' : 'inactive';
}
return (
<div>
<div className={isActive('notification')}>
Press Any Key
</div>
<div className={isActive('box')}>
<h2>20</h2>
<h1>Shift</h1>
</div>
</div>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
.active { color: red; }
.inactive { color: black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
import "./styles.css"
import {useEffect} from 'react'
function App() {
useEffect(()=>{
const box = document.querySelector(".box");
const notification = document.querySelector(".notif");
document.addEventListener("keydown", e => {
box.classList.add("active");
notification.classList.add("inactive");
})
},[])
return (
<>
<div className='notif'>Press Any Key</div>
<div className='box'>
<h2>20</h2>
<h1>Shift</h1>
</div>
</>
);
}
export default App;

The way to optimize react and Typescript codes

In this case, I used React + TypeScript and ant-design. The following code works perfectly but I want the codes to be summarized as much as possible. This is about starting a site that has 3 pages. For example, how can I write this part (const { id, title, description, background } = splash;) so that I don't need to define (splashs[index].background , splashs[index].title, splashs[index].description) all the time.
Thank you in advance for your cooperation.
.splash {
height: 100vh;
position: relative;
overflow: hidden;
}
.bg {
background-color: var(--cjp);
}
.BgGradiant {
background: linear-gradient(107.78deg, rgba(80, 21, 100, 0) 1.87%, rgba(80, 21, 100, 0.05) 18.6%, rgba(80, 21, 100, 0.51) 25.79%, #1C3396 99.02%, #1C3396 51.08%);
}
.context{
width: 80%;
}
.content {
text-align: center;
}
.content h1,
.content p {
color: var(--cwh);
}
.backgroundImage>img {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: -1;
-o-object-fit: cover;
object-fit: cover;
}
.logo {
width: 100%;
text-align: center;
}
.btns {
display: flex !important;
align-items: center;
justify-content: space-between;
-webkit-margin-start: auto;
margin-inline-start: auto;
-webkit-margin-end: auto;
margin-inline-end: auto;
-webkit-margin-before: 2rem;
margin-block-start: 7rem;
}
.btns :global(.ant-btn){
background-color: var(--cwh);
border-radius: var(--borderRadius12);
position: relative;
padding: 4px 10px !important;
}
.btns :global(.ant-btn)::after{
content: "";
position: absolute;
width: 125%;
height: 125%;
top: 50%;
left: 50%;
border: 1px solid var(--chb);
border-radius: var(--borderRadius14);
transform: translate(-50%, -50%);
}
.btns :global(.ant-btn > span){
margin-left: 0 !important;
}
.btns :global(.ant-btn > span > svg){
fill: var(--cal);
}
.btnSkip {
background-color: unset;
outline: none;
border: none;
color: var(--cca);
}
.btnLogin{
-webkit-margin-before: 2rem;
margin-block-start: 7rem;
}
.btnLogin :global(.ant-btn){
border-radius: var(--borderRadius10);
background-color: var(--cwh);
color: var(--cjp);
}
.btnLogin :global(.ant-btn > span){
font-family: "Display-Bold";
}
.dots {
position: absolute;
bottom: 17%;
display: flex !important;
align-items: center;
justify-content: center;
left: 50%;
transform: translateX(-50%);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dotActive {
background-color: var(--cwh);
}
.dotDeActive {
background-color: var(--cca);
}
.dot:not(:last-child) {
-webkit-margin-end: 0.5rem;
margin-inline-end: 0.5rem;
}
.contentInner{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
-webkit-padding-before: 2rem;
padding-block-start: 2rem;
-webkit-padding-after: 3rem;
padding-block-end: 3rem;
}
.contentInner1{
justify-content: space-between;
}
.contentInner2{
justify-content: flex-end;
}
import React, { useState } from 'react';
import { useNavigate } from "react-router-dom";
import { Row, Col, Button } from 'antd';
import { ArrowRightOutlined } from '#ant-design/icons';
import Container from '../../Components/UI/Container/Container'
import classes from './Splash.module.css';
import { backgroundSplash1, backgroundSplash2, logoImage } from '../../Assets/index';
const Splash = () => {
let navigate = useNavigate();
const [index, setIndex] = useState<number>(0);
const {
splash,
bg,
BgGradiant,
context,
content,
backgroundImage,
logo,
btns,
btnLogin,
btnSkip,
dots,
dot,
dotActive,
dotDeActive,
contentInner,
contentInner1,
contentInner2,
} = classes
const splashs = [
{
id: 0,
title: 'Page 1 : title 1',
desctiption: '1- Lorem ipsum 1 ',
background: logoImage,
},
{
id: 1,
title: 'Page 2 : title 2',
desctiption: '2- Lorem ipsum 2 ',
background: backgroundSplash1,
},
{
id: 2,
title: 'Page 3 : title3',
desctiption: '3- Lorem ipsum 3',
background: backgroundSplash2,
}
];
const nextBnt = () => {
setIndex(index + 1);
if (index === splashs.length - 1) {
return navigate("/login");
}
}
const skipBtn = () => {
console.log('skip ');
return navigate("/login");
}
const loginBtn = () => {
return navigate("/login");
}
return (
<>
<Row>
<Col xs={24}>
<section
className={`${index === 0 ? bg : BgGradiant} ${splash}`}>
{
splashs.map((splash) => {
const { id, title, desctiption, background } = splash;
console.log(title, "title");
return (
<>
{
index !== 0 && (
<div className={backgroundImage}>
<img src={splashs[index].background} />
</div>
)
}
<Container key={id} className={backgroundImage}>
<div className={`${index === 0 ? contentInner1 : contentInner2} ${contentInner}`}>
{
index === 0 && (
<div className={logo}>
<img src={logoImage} alt="logoImage" />
</div>
)
}
<div className={context}>
<div className={content}>
<h1>{splashs[index].title}</h1>
<p>{splashs[index].desctiption}</p>
</div>
{/* BTNS */}
{
index === splashs.length - 1 ? (
<div className={btnLogin}>
<Button block onClick={loginBtn}>Login</Button>
</div>
) : (
<div className={btns}>
<button className={btnSkip} onClick={skipBtn}>skip</button>
<Button onClick={nextBnt}> <ArrowRightOutlined /></Button>
</div>
)
}
</div>
</div>
</Container>
</>
)
})
}
<div className={dots}>
{
Array.from({ length: 3 }).map((item, idx) => {
return (
<div key={idx} className={`${dot} ${index === idx ? dotActive : dotDeActive}`}></div>
)
})
}
</div>
</section>
</Col>
</Row>
</>
)
}
export default Splash;
Just an advice, your question isn't really well formulated so it's hard to understand what are you trying to accomplish. Try to keep the questions clear and remove any redundant code so the community can better understand it.
If I'm assuming correctly that you don't want to use splash[index] then you should change splash[index].title to title, same for the other props.
Since you already destructured the splash object with const { id, title, description, background } = splash; all these will be available.
Another thing here is, .map method returns the item in the array so I don't see the point in you using the index inside the loop to access the item from the array.

how to make a vertical slide of text in React

I am trying to make a vertical text slide. Having not found help in reactjs, I try to do it myself and I'm pretty happy with the result but a slight bug appears at the end of each word slide.
I imagine that there are several ways to create this animation but it is the only one that comes to me with my knowledge in JS.
Here my code:
import React, { useEffect, useRef, useState } from 'react'
const Version1 = () => {
const [ words, setWords ] = useState(['Victor', 'Alex', 'Lucie'])
const wrapperRef = useRef()
const handleAnim = () => {
setTimeout(() => {
const copyWords = [ ...words ];
const firstElem = copyWords.splice(1)
wrapperRef.current.style.transition = 'none';
wrapperRef.current.style.top = '0px'
setWords([ ...firstElem.concat(copyWords) ])
},1000);
wrapperRef.current.style.transition = '0.5s';
wrapperRef.current.style.top = '-70px'
}
useEffect(() => {
setTimeout(() => {
handleAnim()
}, 2000);
})
return (
<>
<div className="test-container">
<div className='test-title'>Hello</div>
<div className='text-container-word'>
<div ref={wrapperRef} className='text-container-word-wrapper'>
<span className='text-word'>{words[0]}</span>
<span className='text-word'>{words[1]}</span>
</div>
</div>
</div>
<style jsx>
{`
.test-container {
padding: 100px 0;
width: 100%;
display: flex;
}
.test-title {
font-size: 48px;
font-weight: bold;
color: blue;
}
.text-container-word {
position: relative;
width: 200px;
height: 70px;
background-color: green;
display: inline-block;
overflow: hidden;
margin-top: -7px;
}
.text-container-word-wrapper {
height: auto;
position: relative;
top: 0px;
}
.test-container h1 {
position: relative;
display: inline;
padding: 10px;
}
.text-word {
height: 70px;
font-size: 48px;
font-weight: bold;
display: block;
transition: 0.5s;
line-height: 70px;
}
`}
</style>
</>
)
}
export default Version1
Here is a pure css based solution that uses words from state without the useEffect hacks.
const {useState} = React;
function App() {
const [words, setWords] = useState(["Victor", "Alex", "Lucie", "Michael"]);
return (
<div className="App">
<div className="scroller">
<span>
{words[0]}
<br />
{words[1]}
<br />
{words[2]}
<br />
{words[3]}
</span>
</div>
</div>
);
}
ReactDOM.render(
<App/>,
document.getElementById("react")
);
.App {
font-family: sans-serif;
text-align: center;
}
.scroller {
height: 1.2em;
line-height: 1.2em;
position: relative;
overflow: hidden;
font-size: 40px;
text-align: center;
}
.scroller > span {
position: absolute;
top: 0;
animation: slide 6s infinite;
font-weight: bold;
background-color: green;
}
#keyframes slide {
0% {
top: 0;
}
25% {
top: -1.2em;
}
50% {
top: -2.4em;
}
75% {
top: -3.6em;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react">
set height and width with overflow-f:scroll
overflow-y: scroll;
you can also see this: https://www.w3schools.com/cssref/tryit.asp?filename=trycss3_overflow-y

How to display data from clicked container to other con

Can someone help me to display specific song while clicking on that in Your Playlist container on the left??
I am trying to list data on the left in Your Playlist container When I click on one of the music it should show it in Your Playlist container. It has to save it to browser history as well and it has to remove it from Search because it is already gonna be in Your Playlist container. I will deploy it later to Firebase but now I need help.
It should be added to the left while clicking on one of the listed songs after a search.
Please support me on that.
I am adding my codes here as well for my project:
I have App.js
import "./App.css";
import MySongs from "./MySongs.js";
import Search from "./Search.jsx";
function App() {
return (
<div className="App">
<div className="body">
<MySongs />
<Search />
</div>
</div>
);
}
export default App;
.App {
background-color: #303030;
width: 100%;
height: 100vh;
}
.body {
display: flex;
}
I have Search.jsx
import React, { useState, useEffect, useRef } from "react";
import "./Search.css";
import styled from 'styled-components';
import { IoSearch,IoClose } from "react-icons/io5";
import {motion, AnimatePresence} from "framer-motion";
import {useClickOutside} from "react-click-outside-hook";
import MoonLoader from 'react-spinners/MoonLoader';
import { useDebounce } from "./hooks/debounceHook";
import axios from "axios";
import { TvShow } from "./tvShow";
const SearchBarContainer = styled(motion.div)`
margin-left: 10px;
margin-top: 20px;
display: flex;
flex-direction: column;
width: 96%;
height: 2.5em;
background-color: #424242;
border-radius: 3px;
`;
const SearchInputContainer = styled.div`
width: 98%;
min-height: 2.5em;
display: flex;
align-items: center;
position: relative;
padding: 2px 15px;
`;
const SearchInput = styled.input`
width: 100%;
height: 100%;
outline: none;
border: none;
font-size: 15px;
color: white;
font-weight: 300;
border-radius: 6px;
background-color: transparent;
&:focus {
outline: none;
&::placeholder {
opacity: 0;
}
}
&::placeholder {
color: #white;
transition: all 250ms ease-in-out;
}
`;
const SearchIcon = styled.span`
color: #bebebe;
font-size: 14px;
margin-right: 10px;
margin-top: 6px;
vertical-align: middle;
`;
const CloseIcon = styled(motion.span)`
color: #bebebe;
font-size: 15px;
vertical-align: middle;
transition: all 200ms ease-in-out;
cursor: pointer;
&:hover {
color: #dfdfdf;
}
`;
const LineSeperator = styled.span`
display: flex;
min-width: 100%;
min-height: 2px;
background-color: #d8d8d878;
`;
const SearchContent = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 1em;
overflow-y: auto;
`;
const LoadingWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
const WarningMessage = styled.span`
color: #a1a1a1;
font-size: 14px;
display: flex;
align-self: center;
justify-self: center;
`;
const containerVariants = {
expanded: {
height: "26em",
},
collapsed: {
height: "2.5em",
},
};
const containerTransition = { type: "spring", damping: 22, stiffness: 150 };
export function SearchBar(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [tvShows, setTvShows] = useState([]);
const [noTvShows, setNoTvShows] = useState(false);
const isEmpty = !tvShows || tvShows.length === 0;
const changeHandler = (e) => {
e.preventDefault();
if (e.target.value.trim() === "") setNoTvShows(false);
setSearchQuery(e.target.value);
};
const expandContainer = () => {
setExpanded(true);
};
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoTvShows(false);
setTvShows([]);
if (inputRef.current) inputRef.current.value = "";
};
useEffect(() => {
if (isClickedOutside) collapseContainer();
}, [isClickedOutside]);
const searchTvShow = async () => {
if (!searchQuery || searchQuery.trim() === "") return;
setLoading(true);
setNoTvShows(false);
const options = {
method: 'GET',
url: 'https://deezerdevs-deezer.p.rapidapi.com/search',
params: {q: searchQuery},
headers: {
'x-rapidapi-host': 'deezerdevs-deezer.p.rapidapi.com',
'x-rapidapi-key': '6a99d5e101msh1e9f2b2f948746fp1ae1f3jsn6b458fe8b4e4'
}
};
axios.request(options).then(function (response) {
if (response) {
if (response.data && response.data.length === 0) setNoTvShows(true);
setTvShows(response.data.data);
}
}).catch(function (error) {
console.error(error);
});
setLoading(false);
};
useDebounce(searchQuery, 500, searchTvShow);
// console.log(tvShows);
return (
<div className="my__search">
<SearchBarContainer
animate={isExpanded ? "expanded" : "collapsed"}
variants={containerVariants}
transition={containerTransition}
ref={parentRef}
>
<SearchInputContainer>
<SearchIcon>
<IoSearch />
</SearchIcon>
<SearchInput
placeholder="Search for Series/Shows"
onFocus={expandContainer}
ref={inputRef}
value={searchQuery}
onChange={changeHandler}
/>
<AnimatePresence>
{isExpanded && (
<CloseIcon
key="close-icon"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={collapseContainer}
transition={{ duration: 0.2 }}
>
<IoClose />
</CloseIcon>
)}
</AnimatePresence>
</SearchInputContainer>
{isExpanded && <LineSeperator />}
{isExpanded && (
<SearchContent>
{isLoading && (
<LoadingWrapper>
<MoonLoader loading color="#000" size={20} />
</LoadingWrapper>
)}
{!isLoading && isEmpty && !noTvShows && (
<LoadingWrapper>
<WarningMessage>Start typing to Search</WarningMessage>
</LoadingWrapper>
)}
{!isLoading && noTvShows && (
<LoadingWrapper>
<WarningMessage>No Tv Shows or Series found!</WarningMessage>
</LoadingWrapper>
)}
{!isLoading && !isEmpty && (
<>
{tvShows.map((show) => (
<TvShow
key={show.id}
thumbnailSrc={show.album.cover_medium}
name={show.title_short}
artist={show.artist.name}
/>
))}
</>
)}
</SearchContent>
)}
</SearchBarContainer>
</div>
);
}
export default SearchBar;
.my__search {
margin-top: 20px;
flex: 0.6;
height: 450px;
border-radius: 5px;
border: 1px solid black;
margin-left: 80px;
background-color: #424242;
}
I have tvShow.jsx
import React, { useState } from "react";
import styled from "styled-components";
import {ImDownload} from "react-icons/im";
const TvShowContainer = styled.div`
width: 96%%;
min-height: 3em;
display: flex;
border-bottom: 2px solid #555555;
align-items: center;
`;
const Thumbnail = styled.div`
width: auto;
height: 80%;
display: flex;
flex: 0.4;
img {
border-radius: 20px;
width: auto;
height: 100%;
}
`;
const Name = styled.h3`
font-size: 12px;
color: white;
flex: 2;
display: flex;
flex-direction: column;
`;
const Artist = styled.span`
margin-top: 10px;
font-size: 8px;
color: white;
display: flex;
align-items: center;
`;
const Rating = styled.span`
color: #a1a1a1;
font-size: 16px;
display: flex;
flex: 0.2;
`;
export function TvShow(props) {
const { thumbnailSrc, name, artist,clickedMusic } = props;
const [wantedMusic, setWantedMusic] = useState("");
// const [clickedShow, setClickedShow] = useState("");
// function clickedContainer(e){
// const element = e.currentTarget();
// setClickedShow(element);
// console.log("I am clickedShow " +clickedShow);
// }
return (
<TvShowContainer onclick="location.href='#';" >
<Thumbnail>
<img src={thumbnailSrc} />
</Thumbnail>
<Name>{name}
<Artist>
{artist}
</Artist>
</Name>
</TvShowContainer>
);
}
I have mySongs.js
import React from "react";
import "./MySongs.css";
function MySongs() {
return (
<div className="my__songs">
<p>Your Playlist</p>
</div>
);
}
export default MySongs;
.my__songs {
margin-left: 10px;
margin-top: 20px;
flex: 0.3;
height: 300px;
height: 450px;
border: 1px solid black;
border-radius: 5px;
background-color: #424242;
}
.my__songs > p {
color: white;
opacity: 90%;
margin-left: 10px;
font-size: 13px;
}
Only partly answering the question: move an item from one list to another (and back) on mouse click.
The basic situation can be solved if you use the parent component to hold the state that the children components display. Then you only need to implement a function that toggles a "flag" (like selected), and the components can be rendered based on that flag.
const {useState} = React
const tracklist = [
{
id: 1,
title: 'Track 1',
selected: false,
},
{
id: 2,
title: 'Track 2',
selected: false,
},
{
id: 3,
title: 'Track 3',
selected: false,
},
{
id: 4,
title: 'Track 4',
selected: false,
},
{
id: 5,
title: 'Track 5',
selected: false,
},
]
const ListItem = ({ title, onToggleSelect }) => <div className="list-item" onClick={onToggleSelect}>{title}</div>
const App = ({ tracklist }) => {
const [tracks, setTracks] = useState(tracklist)
const toggleSelect = (id) => {
setTracks((prevState) => prevState.map(item => item.id === id ? {...item, selected: !item.selected} : item))
}
const listItem = (track) => <ListItem key={track.id} {...track} onToggleSelect={() => toggleSelect(track.id)}/>
return (
<div className="container" >
<div className="tracklist">
{
tracks
.filter(({ selected }) => selected)
.map(listItem)
}
</div>
<div className="tracklist">
{
tracks
.filter(({ selected }) => !selected)
.map(listItem)
}
</div>
</div>
)
}
ReactDOM.render(
<App tracklist={tracklist} />,
document.getElementById('root')
);
html, body {
margin: 0;
padding: 5px 10px;
}
.container {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 20px;
}
.list-item {
cursor: pointer;
}
.list-item:hover {
background: lightgray;
}
.tracklist {
border: 1px solid gray;
}
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

create an expand content or show more content button in react app

So i am trying to basically add a expand button to my react app that will reveal more information and i would prefer that the app expand the div size to reveal the additional content, so the its basically an expand button, i understand i need to utilize the usestate property and a function however i am having a hard time figuring out how do i update the css of the student div to expand and reveal the added information. The added information is the grades portion (FYI).
UPDATE: I have found a way to display the changes the problem i am facing is to get the state to start as isExpanded false so when a user clicks the plus button it expands to reveal the class hidden information again
here is my App.js file
import React, { useState } from "react";
import "./App.css";
export default function App() {
const [students, setStudents] = useState(null);
const [filterData, setFilterData ] = useState(null);
const [isExpanded, setIsExpanded] = useState(false);
const studentdata = 'https://www.hatchways.io/api/assessment/students';
function getStudents(){
fetch(studentdata)
.then(resp => resp.json())
.then(data => {
setFilterData(data.students);
setStudents(data.students);
setIsExpanded(false);
})
}
const searchByName = (event) => {
event.persist();
// Get the search term
const searchItem = event.target.value.toLowerCase().trim();
// If search term is empty fill with full students data
if(!searchItem.trim()) {
setFilterData(students);
}
// Search the name and if it found retun the same array
const serachIn = (firstName, lastName) => {
if(firstName.indexOf(searchItem) !== -1 || lastName.indexOf(searchItem) !== -1) {
return true;
}
let fullName = firstName.toLowerCase()+" "+lastName.toLowerCase();
if(fullName.indexOf(searchItem) !== -1) {
return true;
}
return false;
};
// Filter the array
const filteredData = students.filter((item) => {
return serachIn(item.firstName, item.lastName);
});
// Set the state with filtered data
setFilterData(filteredData);
}
function exp() {
if(isExpanded){
setIsExpanded(true);
}
}
return (
<div className="App">
<h1>Students</h1>
<div>
<button className="fetch-button" onClick={getStudents}>
Get Students
</button>
<br />
</div>
<div className="search" id="search">
<input type="text" name="serachByName" id="searchbar" placeholder="Search by name" onChange={(e) => searchByName(e)} ></input>
</div>
{filterData && filterData.map((student, index) => {
var total = 0;
for(var i = 0; i < student.grades.length; i++) {
var grade = parseInt(student.grades[i]);
total += grade;
}
const avg = total / student.grades.length;
const average = avg.toString();
const grade1 = student.grades[0];
const grade2 = student.grades[1];
const grade3 = student.grades[2];
const grade4 = student.grades[3];
const grade5 = student.grades[4];
const grade6 = student.grades[5];
const grade7 = student.grades[6];
const grade8 = student.grades[7];
return(
<div className={'student' + isExpanded ? 'expanded' : '' } key={index}>
<div className="image">
<img src={student.pic} id="icon"></img>
</div>
<div className="text">
<h3 id="name">{student.firstName} {student.lastName}</h3>
<p id="detail"><strong>EMAIL:</strong> {student.email}</p>
<p id="detail"><strong>COMPANY:</strong> {student.company}</p>
<p id="detail"><strong>SKILL:</strong> {student.skill}</p>
<p id="detail"><strong>AVERAGE:</strong>: {average}%</p>
<p id="detail" className="hidden">
<br></br>Test 1 :{grade1}
<br></br>Test 2 :{grade2}
<br></br>Test 3 :{grade3}
<br></br>Test 4 :{grade4}
<br></br>Test 5 :{grade5}
<br></br>Test 6 :{grade6}
<br></br>Test 7 :{grade7}
<br></br>Test 8 :{grade8}
</p>
</div>
<div className="expand">
<button className="expand_btn" onClick={exp()} id="expand_btn">+</button>
</div>
</div>
)}
)}
</div>
);
}
and my css file
#import url('https://fonts.googleapis.com/css?family=Bebas+Neue&display=swap');
#import url('https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap');
.root{
width: 100vw;
height: 100vh;
background-color: black;
}
.App {
text-align: center;
width: 1000px;
height: 750px;
background-color: aliceblue;
border: 4px solid black;
border-radius: 5%;
margin-top: 75px;
margin-left: auto;
margin-right: auto;
overflow: scroll;
}
.student{
width: 80%;
height: 200px;
background-color: white;
display: flex;
align-items: center;
padding-top: 3%;
padding-bottom: 3%;
border: 2px solid lightblue;
margin-left: auto;
margin-right: auto;
}
.text{
text-align: left;
padding-left: 7%;
width: 300px;
}
.image{
padding-left: 15%;
}
#icon{
border-radius: 50%;
width: 150px;
height: 150px;
border: 2px solid black;
}
#name{
text-transform: capitalize;
font-family: 'Bebas Neue';
letter-spacing: 4px;
font-size: 40px;
margin-bottom: 10px;
margin-top: 10px;
}
#detail {
font-family: 'Roboto';
font-weight: 300;
line-height: normal;
margin: 0;
}
.search {
width: 80%;
height: 20px;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
margin-bottom: 20px;
}
#searchbar {
width: 100%;
height: 30px;
font-family: 'Roboto';
font-size: 18px;
font-weight: 300;
}
.expand {
width: 100px;
height: 100px;
padding-left: 3%;
margin-bottom: 5%;
}
#expand_btn {
font-family: 'Bebas Neue';
font-size: 50px;
color: lightskyblue;
background-color: transparent;
border: none;
}
.hidden {
display: none;
}
.expanded{
width: 80%;
height: 300px;
background-color: white;
display: flex;
align-items: center;
padding-top: 3%;
padding-bottom: 3%;
border: 2px solid lightblue;
margin-left: auto;
margin-right: auto;
}
I have fixed the issue in this codesandbox - https://codesandbox.io/s/purple-bird-j5vrm
Check and let me know if this helps.
There are many ways to solve your problem. One way is adding isExpanded flag to each object in the students array so that each student object would know if that is expanded or not. And I have used the flag like this
className={"student " + student.isExpanded ? "expanded" : " "}
As per your implementation, isExpanded was being set globally so every student item would be set as Expanded and there was no way to know which item was expanded.
Note: I have implemented only for getStudents and not filterStudents.

Categories

Resources