Related
OK guys, so I'm noob in React, and I know that I'm mistaken something, but I can't see what.
I put loading spinners between my pages and no problems, until this page :
So I have this page that is scrolling on X axe and a logo spinning during scroll, until know, all was working correctly. And there is the code without the loading spinner :
import React, { useEffect, useRef } from "react";
import styled, { ThemeProvider } from "styled-components";
import { DarkTheme } from "./Themes";
import {motion} from 'framer-motion';
import LogoComponent from '../subComponents/LogoComponent';
import SocialIcons from '../subComponents/SocialIcons';
import PowerButton from '../subComponents/PowerButton';
import { Work } from '../data/WorkData';
import Card from "../subComponents/Card";
import { Umbrella } from "./AllSvgs";
import BigTitle from "../subComponents/BigTitle";
const Box = styled.div`
background-color: ${props => props.theme.body};
height: 400vh;
position: relative;
display: flex;
align-items: center;
`
const Main = styled(motion.ul)`
position: fixed;
top: 12rem;
left: calc(10rem + 15vw);
height: 40vh;
display: flex;
color: white;
`
const Rotate = styled.span`
display: block;
position: fixed;
right: 1rem;
bottom: 1rem;
width: 80px;
height: 80px;
z-index:1;
`
// Framer-motion configuration
const container = {
hidden: {opacity:0},
show: {
opacity:1,
transition: {
staggerChildren:0.5,
duration:0.5,
}
}
}
const WorkPage = () => {
const ref = useRef(null);
const umbrella = useRef(null);
useEffect(() => {
let element = ref.current;
const rotate = () => {
element.style.transform = `translateX(${-window.scrollY}px)`
umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
}
window.addEventListener('scroll', rotate)
return () => window.removeEventListener('scroll', rotate)
}, [])
return (
<ThemeProvider theme={DarkTheme}>
<Box>
<LogoComponent theme='dark'/>
<SocialIcons theme='dark'/>
<PowerButton />
<Main ref={ref} variants={container} initial='hidden' animate='show' >
{
Work.map( d =>
<Card key={d.id} data={d} />
)
}
</Main>
<Rotate ref={umbrella}>
<Umbrella width={80} height={80} fill={DarkTheme.theme} />
</Rotate>
<BigTitle text="PROJETS" top="10%" right="20%" />
</Box>
</ThemeProvider>
)
}
export default WorkPage
Then I put the code with the same logic than the other pages that are working :
import React, { useEffect, useRef, useState } from "react";
import styled, { ThemeProvider } from "styled-components";
import { DarkTheme } from "./Themes";
import {motion} from 'framer-motion';
import RingLoader from "react-spinners/RingLoader";
import { css } from "#emotion/react";
import LogoComponent from '../subComponents/LogoComponent';
import SocialIcons from '../subComponents/SocialIcons';
import PowerButton from '../subComponents/PowerButton';
import { Work } from '../data/WorkData';
import Card from "../subComponents/Card";
import { Umbrella } from "./AllSvgs";
import BigTitle from "../subComponents/BigTitle";
const Box = styled.div`
background-color: ${props => props.theme.body};
height: 400vh;
position: relative;
display: flex;
align-items: center;
`
const Main = styled(motion.ul)`
position: fixed;
top: 12rem;
left: calc(10rem + 15vw);
height: 40vh;
display: flex;
color: white;
`
const Rotate = styled.span`
display: block;
position: fixed;
right: 1rem;
bottom: 1rem;
width: 80px;
height: 80px;
z-index:1;
`
const override = css`
position: absolute;
bottom: 10%;
right: 10%;
`
// Framer-motion configuration
const container = {
hidden: {opacity:0},
show: {
opacity:1,
transition: {
staggerChildren:0.5,
duration:0.5,
}
}
}
const WorkPage = () => {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 2000)
}, [])
const ref = useRef(null);
const umbrella = useRef(null);
useEffect(() => {
let element = ref.current;
const rotate = () => {
element.style.transform = `translateX(${-window.scrollY}px)`
umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
}
window.addEventListener('scroll', rotate)
return () => window.removeEventListener('scroll', rotate)
}, [])
return (
<ThemeProvider theme={DarkTheme}>
{
loading ?
<RingLoader
color={'#000'}
loading={loading}
size={60}
css={override}
/>
:
<Box>
<LogoComponent theme='dark'/>
<SocialIcons theme='dark'/>
<PowerButton />
<Main ref={ref} variants={container} initial='hidden' animate='show' >
{
Work.map( d =>
<Card key={d.id} data={d} />
)
}
</Main>
<Rotate ref={umbrella}>
<Umbrella width={80} height={80} fill={DarkTheme.theme} />
</Rotate>
<BigTitle text="PROJETS" top="10%" right="20%" />
</Box>
}
</ThemeProvider>
)
}
export default WorkPage
And the scroll is not working anymore. The logo is still spinning. I try to put in conditions, but nope.
Put the useState on true, but then I have an error on the rotate func :
TypeError: Cannot read properties of null (reading 'style')
rotate
src/components/WorkPage.js:89
86 | let element = ref.current;
87 |
88 | const rotate = () => {
> 89 | element.style.transform = `translateX(${-window.scrollY}px)`
| ^ 90 |
91 | umbrella.current.style.transform = `rotate(` + -window.scrollY + 'deg)'
92 | }
I don't see what I'm missing... Thanks y'all 🙏
In your second example code (where you added loading and setLoading from the useState hook), the <Box> component (which contains the <Main ref={ref}... component) doesn't get rendered when loading is set to true (and you're setting it to true in your first useEffect).
Since the <Main ref={ref}... component isn't being rendered, the line let element = ref.current will initialize element as null. (that's why you're getting that error)
I am currently in the process of replicating some of the core functionalities that exists in Trello. One of the functionalities i am looking into is the drag-and-drop.
I have managed to implement the functionality, albeit with an issue. When you drag a card from one column to another column, there is some form of animation (see the linked YouTube video for reference). To describe it in words, the animation portray a movement back to the starting position of the dragged card, although it is not the case.
https://www.youtube.com/watch?v=wSoaaKLj3r0
My current code:
/* Column.js */
import React, {useState, useRef} from 'react';
import './Column.css';
function Column({data}) {
const [list, setList] = useState(data);
const [dragging, setDragging] = useState(false);
const dragItem = useRef();
const dragNode = useRef();
const handleDragStart = (event, params) => {
console.log('drag starting', params)
dragItem.current = params;
dragNode.current = event.target;
dragNode.current.addEventListener('dragend', handleDragEnd)
setTimeout(() => {
setDragging(true);
}, 0)
}
const handleDragEnter = (event, params) => {
console.log('entering drag', params);
const currentItem = dragItem.current;
if(event.target !== dragNode.current) {
console.log('TARGET IS NOT THE SAME')
setList(oldList => {
let newList = JSON.parse(JSON.stringify(oldList));
newList[params.groupIndex].items.splice(params.itemIndex, 0, newList[currentItem.groupIndex].items.splice(currentItem.itemIndex,1)[0])
dragItem.current = params
return newList;
})
}
}
const handleDragEnd = () => {
console.log('Ending Drag')
setDragging(false);
dragNode.current.removeEventListener('dragend', handleDragEnd)
dragItem.current = null;
dragNode.current = null;
}
const getStyles = (params) => {
const currentItem = dragItem.current;
if (currentItem.groupIndex === params.groupIndex && currentItem.itemIndex === params.itemIndex) {
return 'current drag-and-drop-item';
}
return 'drag-and-drop-item'
};
return (
<section className="drag-and-drop-container">
<div className="drag-and-drop">
{list.map((group, groupIndex) => (
<div
key={group.title}
className="drag-and-drop-group"
onDragEnter={dragging && !group.items.length
? (event) => handleDragEnter(event, {groupIndex, itemIndex:0})
: null
}
>
<div className="drag-and-drop-group-title">{group.title}</div>
{group.items.map((item, itemIndex) => (
<div
draggable
onDragStart={(dragEvent) => {handleDragStart(dragEvent, {groupIndex, itemIndex})}}
onDragEnter={dragging
? (event) => {handleDragEnter(event, {groupIndex, itemIndex})}
: null
}
key={item}
className={dragging
? getStyles({groupIndex, itemIndex})
: "drag-and-drop-item"
}
>
{item}
</div>
))}
</div>
))}
</div>
</section>
)
}
export default Column;
/* Column.css */
#import url(./../../index.css);
.drag-and-drop{
display: grid;
gap: 0.5rem;
width: 100%;
height: 100%;
grid-template-columns: repeat(auto-fill, 300px);
background-color: var(--primary-color);
padding: 0.5rem;
align-items: start;
}
.drag-and-drop-group{
background-color: var(--secondary-color-80);
padding: 0.5rem;
}
.drag-and-drop-group-title{
margin-bottom: 0.5rem;
font-size: 1.2rem;
font-family: 'Poppins';
}
.drag-and-drop-item {
background-color: #FFFFFF;
color: var(--primary-color);
min-height: 150px;
}
.drag-and-drop-item:not(:last-of-type){
margin-bottom: 0.5rem;
}
.current{
background-color: var(--primary-color);
}
Findings:
The problem occurs exclusively on my MacBook, and therefore not on my Windows computer.
I have tried with Event.preventDefault();, however without luck.
Question:
What can I do to prevent the animation on Mac OS?
I am trying to achieve this: https://codesandbox.io/s/framer-motion-2-drag-to-reorder-fc4rt?file=/src/use-position-reorder.js
Basically you can drag a card, place it somewhere in the container and reorder the card based on where you drag/put it from the original place.
The problem is that in my code, you can drag the card but not place it. I tried to change the code so many times but I can't find a way out of it. Below you will find the code where I render the component FavouriteTreeNodeComponent which is a LI(List) nested in FavouriteTreeNodesList a UL(Unordered List) :
// react
import { useRef, useEffect, useCallback, useState } from 'react';
// animation spring
import { animated, Transition, useSpring } from 'react-spring';
import { AnimateSharedLayout } from 'framer-motion';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { usePositionReorder } from './ReorderingModalNodes/use-position-reorder';
// icons
import { MdClose } from 'react-icons/md';
// components
import TemplateItem from './TemplateItem';
import SearchBar from './SearchBar';
import DurationIntervalComponent from './DurationIntervalComponent';
import ContextItem from './ContextItem';
import FavouriteTreeNodeComponent from './FavouriteTreeNodeComponent';
const TimeEntryModal = ({ showModal, setShowModal }) => {
// STATES
const [selected, setSelected] = useState('');
const [reportingTemplates, setReportingTemplates] = useState([]);
const [reportingContexts, setReportingContexts] = useState([]);
// USEREF
const modalRef = useRef();
// ANIMATION MODAL
const animation = useSpring({
config: {
duration: 250,
},
opacity: showModal ? 1 : 0,
transform: showModal ? `translateY(0%)` : `translateY(-100%)`,
});
// CLOSING MODAL FROM BACKGROUND
const closeModal = (e) => {
if (modalRef.current === e.target) {
setShowModal(false);
}
};
// CLOSING MODAL WITH ESCAPE BUTTON
const keyPress = useCallback(
(e) => {
if (e.key === 'Escape' && showModal) {
setShowModal(false);
console.log('I pressed');
}
},
[setShowModal, showModal]
);
useEffect(() => {
document.addEventListener('keydown', keyPress);
return () => document.removeEventListener('keydown', keyPress);
}, [keyPress]);
const variants = {
hidden: {
y: '-100vh',
},
visible: { y: 0, transition: { ease: 'easeInOut', duration: 1 } },
exit: {
y: '-100vh',
},
};
// DATA
const colorsData = [
'#86d8bda1',
'#eb472a83',
'#8fcfe3ad',
'#db8ddfb0',
'#e8e46fc0',
'#e1aa66b2',
];
const List = [
'3000',
'4000',
'5000',
'6000',
'7000',
'8000',
];
// FAVOURITES LIST ANIMATION
const [updatedList, updatePosition, updateOrder] =
usePositionReorder(List);
// FETCHES
useEffect(() => {
getTemplates();
}, []);
const url = 'http://localhost:8080/graphql';
const getTemplates = async () => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `query {
reportingTemplates {
name
_id
colorScheme
reportingContexts {
name
_id
}
}
}`,
}),
});
const data = await res.json();
console.log(data);
const dataWithColors = data.data.reportingTemplates.map((obj, i) => ({
...obj,
color: colorsData[i],
}));
setReportingTemplates(dataWithColors);
setSelected(dataWithColors[0].color);
setReportingContexts(dataWithColors[0].reportingContexts);
console.log('data', dataWithColors);
} catch (err) {
console.log('ERROR', err);
}
};
console.log('data state', reportingTemplates);
return (
<Background ref={modalRef} onClick={closeModal}>
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
>
<ModalWrapper>
<ModalContent>
<Nav>
<h3>New Entry Tirsdag 2. Sep </h3>
</Nav>
<ModalColumns>
<ColumnOne>
<TemplatesContainer>
<AnimateSharedLayout>
<Templates>
{reportingTemplates.map((template, index) => (
<TemplateItem
key={template.name}
color={template.color}
title={template.name}
isSelected={selected === template.color}
onClick={() => {
setReportingContexts(
reportingTemplates[index].reportingContexts
);
setSelected(template.color);
}}
/>
))}
</Templates>
</AnimateSharedLayout>
</TemplatesContainer>
<InformationBlock>
<h4>Information</h4>
{reportingContexts.map((context) => (
<ContextItem
key={context.name}
text={context.name}
// isSelected={selected === context.color}
// onClick={() => setSelected(context.color)}
/>
))}
</InformationBlock>
<DurationIntervalComponent />
</ColumnOne>
<ColumnTwo>
<SearchBar />
<Recent>
<h4>Recent</h4>
</Recent>
<Favourites>
<h4>Favourites</h4>
<FavouriteTreeNodesList>
{updatedList.map((treeNode, index) => (
<FavouriteTreeNodeComponent
key={index}
index={index}
text={treeNode}
updateOrder={updateOrder}
updatePosition={updatePosition}
/>
))}
</FavouriteTreeNodesList>
</Favourites>
</ColumnTwo>
</ModalColumns>
</ModalContent>
<CloseModalButton
aria-label="Close Modal"
onClick={() => setShowModal((prev) => !prev)}
/>
<Footer>
<DeleteBlock>
<p> Delete </p>
</DeleteBlock>
<CancelNSaveBlock>
<Cancel>
<p> Cancel </p>
</Cancel>
<SaveNClose>
<p> Save and close </p>
</SaveNClose>
<SaveNAdd>
<p> Save and add another </p>
</SaveNAdd>
</CancelNSaveBlock>
</Footer>
</ModalWrapper>
</motion.div>
</Background>
);
};
// STYLES
const Background = styled.div`
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
position: fixed;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
`;
const ModalWrapper = styled.div`
width: 80vw;
height: 80vh;
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.2);
background: #fff;
color: #000;
display: flex;
flex-direction: column;
align-items: stretch;
border-radius: 10px;
`;
const CloseModalButton = styled(MdClose)`
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
padding: 0;
z-index: 10;
`;
// MODAL CONTENT
const ModalContent = styled.div`
padding: 20px;
position: relative;
`;
// NAV
const Nav = styled.div``;
const ModalColumns = styled.div`
padding: 10px 0px;
display: flex;
`;
// COLUMN ONE
const ColumnOne = styled.div`
width: 50%;
padding: 20px;
`;
const TemplatesContainer = styled.div``;
const Templates = styled.ul`
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: initial;
justify-content: center;
`;
const InformationBlock = styled.div`
padding-bottom: 20px;
`;
// COLUMN TWO
const ColumnTwo = styled.div`
width: 50%;
padding: 20px;
`;
const Recent = styled.div`
background-color: #f6f6f9;
border: 1px solid #c5c5c5;
border-radius: 20px;
`;
const Favourites = styled.div`
list-style: none;
`;
const FavouriteTreeNodesList = styled.ul`
position: relative;
width: 100%;
`;
// FOOTER
const Footer = styled.div`
display: flex;
position: fixed;
padding: 0 20px;
bottom: 10px;
/* text-align: center; */
width: 100%;
`;
const DeleteBlock = styled.div`
color: #fb423a;
`;
const CancelNSaveBlock = styled.div`
display: flex;
margin-left: auto;
`;
// const [{name:"cancel", color func: ()=>sdasd }, "sada", "asdasd"]
const Cancel = styled.div`
color: #02b396;
margin-right: 40px;
`;
const SaveNClose = styled.div`
font-weight: bolder;
color: #02b396;
margin-right: 40px;
`;
const SaveNAdd = styled.div`
font-weight: bolder;
color: #02b396;
`;
export default TimeEntryModal;
After that the list FavouriteTreeNodeComponent is rendered in the code below:
import { useState } from 'react';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { useMeasurePosition } from './ReorderingModalNodes/use-measure-position';
const FavouriteTreeNodeComponent = ({
text,
updateOrder,
updatePosition,
index,
}) => {
const [isdragged, setIsDragged] = useState(false);
const itemRef = useMeasurePosition((pos) => updatePosition(index, pos));
return (
<ListContainer>
<motion.div
style={{
zIndex: isdragged ? 2 : 1,
height: text.length * 10,
padding: "5px",
}}
dragConstraints={{
top: 0,
bottom: 0,
}}
dragElastic={1}
layout
ref={itemRef}
onDragStart={() => setIsDragged(true)}
onDragEnd={() => setIsDragged(false)}
animate={{
scale: isdragged ? 1.05 : 1,
}}
onViewportBoxUpdate={(_, delta) => {
isdragged && updateOrder(index, delta.y.translate);
}}
drag="y">{text}
</motion.div>
</ListContainer>
);
};
// STYLES
const ListContainer = styled.li`
height: auto;
gap: 9px 0px;
position: relative;
padding: 10px 0px;
list-style: none;
div {
background-color: white;
border-radius: 5px;
border: 1px solid #c5c5c5;
}
`;
export default FavouriteTreeNodeComponent;
The above code is connected to the file: useMeasurePosition same as the original project:
import { useEffect, useRef } from 'react';
export function useMeasurePosition(update) {
// We'll use a `ref` to access the DOM element that the `motion.li` produces.
// This will allow us to measure its height and position, which will be useful to
// decide when a dragging element should switch places with its siblings.
const ref = useRef(null);
// Update the measured position of the item so we can calculate when we should rearrange.
useEffect(() => {
update({
height: ref.current.offsetHeight,
top: ref.current.offsetTop,
});
});
return ref;
}
and to the file: usePositionReorder:
import { useState, useRef } from 'react';
import { clamp, distance } from 'popmotion';
import { arrayMoveImmutable as move} from 'array-move';
export function usePositionReorder(initialState) {
const [order, setOrder] = useState(initialState);
// We need to collect an array of height and position data for all of this component's
// `Item` children, so we can later us that in calculations to decide when a dragging
// `Item` should swap places with its siblings.
const positions = useRef([]).current;
const updatePosition = (i, offset) => (positions[i] = offset);
// Find the ideal index for a dragging item based on its position in the array, and its
// current drag offset. If it's different to its current index, we swap this item with that
// sibling.
const updateOrder = (i, dragOffset) => {
const targetIndex = findIndex(i, dragOffset, positions);
if (targetIndex !== i) setOrder(move(order, i, targetIndex));
};
return [order, updatePosition, updateOrder];
}
const buffer = 30;
export const findIndex = (i, yOffset, positions) => {
let target = i;
const { top, height } = positions[i];
const bottom = top + height;
// If moving down
if (yOffset > 0) {
const nextItem = positions[i + 1];
if (nextItem === undefined) return i;
const swapOffset =
distance(bottom, nextItem.top + nextItem.height / 2) + buffer;
if (yOffset > swapOffset) target = i + 1;
// If moving up
} else if (yOffset < 0) {
const prevItem = positions[i - 1];
if (prevItem === undefined) return i;
const prevBottom = prevItem.top + prevItem.height;
const swapOffset = distance(top, prevBottom - prevItem.height / 2) + buffer;
if (yOffset < -swapOffset) target = i - 1;
}
return clamp(0, positions.length, target);
};
I can't get my head around why I'm getting this error. Does anyone know what is wrong with the code? The code worked when there was only the "class image" and the error started appearing after I added back "function App()".
App.js =>
import React, {useEffect, useState} from "react";
import "./App.css";
import Navbar from './components/Navbar/Navbar'
import "bootstrap/dist/css/bootstrap.min.css";
import UploadImages from "./components/image-upload.component";
import styled from "styled-components";
function App() {
const [url, setURL] = useState("");
const Button = styled.button`
background-color: black;
color: white;
font-size: 20px;
padding: 10px 60px;
border-radius: 5px;
margin: 10px 0px;
cursor: pointer;
`;
const [urls, setURLs] = useState([]);
useEffect(() => {
const urls = [
250940,
20622,
436625,
436444,
436509,
359245,
459090,
333933,
333916,
466350,
44831,
383010,
202660,
406317,
337349,
503448,
12617,
248662,
435805,
438545
].map(
(itemId) =>
`https://collectionapi.metmuseum.org/public/collection/v1/objects/${itemId}`
);
Promise.all(
urls.map((currUrl) =>
fetch(currUrl)
.then((response) => response.json())
.then((data) => data.primaryImage)
.catch((error) => console.log("There was a problem!", error))
)
).then((fetchedUrls) => setURLs(fetchedUrls));
}, []);
class image extends React.Component {
constructor(props) {
super(props);
this.onClickNext = this.onClickNext.bind(this)
this.onClickPrevious = this.onClickPrevious.bind(this)
const img0 = 'https://images.metmuseum.org/CRDImages/gr/original/DP146615.jpg'
const img1 = 'https://images.metmuseum.org/CRDImages/ad/original/DP253076.jpg'
const img2 = 'https://images.metmuseum.org/CRDImages/ep/original/DP146499.jpg'
const img3 = 'https://images.metmuseum.org/CRDImages/ep/original/EP1455.jpg'
const img4 = 'https://images.metmuseum.org/CRDImages/ep/original/DT2580.jpg'
const img5 = 'https://images.metmuseum.org/CRDImages/dp/original/DP821059.jpg'
const img6 = 'https://images.metmuseum.org/CRDImages/rl/original/DP295708.jpg'
const img7 = 'https://images.metmuseum.org/CRDImages/dp/original/DP805154.jpg'
this.state = {
index: 0,
imgList: [img0 ,img1, img2, img3, img4, img5, img6, img7]
}
}
// imgList: [urls[1], urls[2], urls[3], urls[4], urls[5], urls[6], urls[7], urls[8], urls[9], urls[10], urls[11], urls[12], urls[13], urls[14], urls[15], urls[16], urls[17], urls[18], urls[19], urls[20]]
onClickNext() {
if (this.state.index + 1 === this.state.imgList.length) {
this.setState({
index: 0
})
} else {
this.setState({
index: this.state.index + 1
})
}
}
onClickPrevious() {
if (this.state.index - 1 === -1) {
this.setState({
index: this.state.imgList.length -1
})
} else {
this.setState({
index: this.state.index -1
})
}
}
render() {
return (
<div>
<div className="productsContainer">
<div className="images">
<div className="images__item">
<img className="photo" src={this.state.imgList[this.state.index]}/>
</div>
<div className="images__item">
<img className="photo" src={this.state.imgList[this.state.index + 1]}/>
</div>
<div className="images__item">
<img className="photo" src={this.state.imgList[this.state.index + 2]}/>
</div>
</div>
</div>
<button onClick={this.onClickPrevious}>Previous</button>
<button onClick={this.onClickNext}>Next</button>
</div>
)
}
}
}
export default App;
Full error: App(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
You are not rendering anything in your <App>. You need to return your component. I renamed it from <Image> to <ImageCarousel>.
I also cleaned-up the modular math.
const { useEffect, useState } = React;
const images = [
'https://images.metmuseum.org/CRDImages/gr/original/DP146615.jpg',
'https://images.metmuseum.org/CRDImages/ad/original/DP253076.jpg',
'https://images.metmuseum.org/CRDImages/ep/original/DP146499.jpg',
'https://images.metmuseum.org/CRDImages/ep/original/EP1455.jpg',
'https://images.metmuseum.org/CRDImages/ep/original/DT2580.jpg',
'https://images.metmuseum.org/CRDImages/dp/original/DP821059.jpg',
'https://images.metmuseum.org/CRDImages/rl/original/DP295708.jpg',
'https://images.metmuseum.org/CRDImages/dp/original/DP805154.jpg'
];
class ImageCarousel extends React.Component {
constructor(props) {
super(props);
const { images } = props;
this.onClickNext = this.onClickNext.bind(this)
this.onClickPrevious = this.onClickPrevious.bind(this)
this.state = {
index: 0,
imgList: images
}
}
onClickNext() {
this.rotateIndex(1);
}
onClickPrevious() {
this.rotateIndex(-1);
}
rotateIndex(direction) {
const { index, imgList: { length }} = this.state;
this.setState({
index: this.calculateIndexOffset(index, direction, length)
});
}
calculateIndexOffset(start, offset, size) {
return (start + size + offset) % size;
}
indexOffset(offset = 0) {
const { index, imgList: { length }} = this.state;
return this.calculateIndexOffset(index, offset, length);
}
imageOffset(offset = 0) {
return this.state.imgList[this.indexOffset(offset)];
}
render() {
return (
<div className="imageCarousel">
<div className="productsContainer">
<div className="images">
<div className="images__item">
<img className="photo" src={this.imageOffset()}/>
</div>
<div className="images__item">
<img className="photo" src={this.imageOffset(1)}/>
</div>
<div className="images__item">
<img className="photo" src={this.imageOffset(2)}/>
</div>
</div>
</div>
<div className="buttons">
<button onClick={this.onClickPrevious}>Previous</button>
<button onClick={this.onClickNext}>Next</button>
</div>
</div>
)
}
}
const App = () => {
const [url, setURL] = useState("");
const [urls, setURLs] = useState([]);
useEffect(() => {
const urls = [
250940,
20622,
436625,
436444,
436509,
359245,
459090,
333933,
333916,
466350,
44831,
383010,
202660,
406317,
337349,
503448,
12617,
248662,
435805,
438545
].map(
(itemId) =>
`https://collectionapi.metmuseum.org/public/collection/v1/objects/${itemId}`
);
Promise.all(
urls.map((currUrl) =>
fetch(currUrl)
.then((response) => response.json())
.then((data) => data.primaryImage)
.catch((error) => console.log("There was a problem!", error))
)
).then((fetchedUrls) => setURLs(fetchedUrls));
}, []);
return <ImageCarousel images={images} />
}
ReactDOM.render(<App />, document.getElementById("react"));
.imageCarousel {
display: grid;
grid-template-columns: 1fr;
grid-row-gap: 0.5em;
}
.imageCarousel .productsContainer .images {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.images__item {
display: flex;
justify-content: center;
}
.images__item .photo {
max-height: 160px;
flex: 1;
}
.imageCarousel .buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 1em;
justify-content: center;
}
<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"></div>
Im new to React and started working on a memory game where you flip cards and compare two cards. Im having trouble understanding how to change state of individual component. now when I click a component the state of all components change and all my cards turn red instead of one. later I was thinking to add photos but for now just testing with background color. Also I know I have to add some logic/features but cant get past state problem.
App.js
import React, {Component} from 'react';
import './App.css';
import Grid from './grid/grid';
import Header from './Header/header';
import Footer from './Footer/footer';
class App extends Component {
cards = [{id:1, name: 'dog'},{id:2, name: 'dog'},{id:3, name: 'cat'},{id:4, name: 'cat'},{id:5, name: 'mouse'},{id:6, name: 'mouse'},{id:7, name: 'horse'},{id:8, name: 'horse'},
{id:9, name: 'pig'},{id:10, name: 'pig'},{id:11, name: 'chicken'},{id:12, name: 'chicken'},{id:13, name: 'cow'},{id:14, name: 'cow'},{id:15, name: 'fox'},{id:16, name: 'fox'}]
.sort( () => Math.random() - 0.5);
clicks = [];
state = {
current: 0,
}
clickHandler = (click) => {
this.clicks.push(click.name);
this.setState({
current: 1
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
compare = (name) => {
if (name === this.clicks[0]) {
console.log('pair')
} else {
console.log('nope');
}
}
render() {
return (
<div className="App">
<Header />
<div className='Grid-container'>
<div className='wrapper'>
{this.cards.map(child =>
<Grid click={() => this.clickHandler(child)}
active={this.state.current === 0}
id={child.id}
/>)}
</div>
</div>
<Footer />
</div>
);
}
}
export default App;
grid.js
import React from 'react';
import './grid.css';
const Grid = (props) => {
return (
<div className={'Child' + (props.active ? '': ' active')}
onClick={props.click}
>
{props.id}
</div>
);
}
export default Grid;
App.css
.Grid-container {
display: flex;
background-color: black;
justify-content: center;
align-items: center;
}
.wrapper {
display: grid;
width: 700px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-gap: 10px;
background-color: black;
justify-content: center;
align-items: center;
}
**grid.css**
.Child {
width: auto;
height: 120px;
background-color: azure;
border-radius: 10px;
}
.Child.active {
width: auto;
height: 120px;
background-color: red;
border-radius: 10px;
}
You have to use index for this. You have index as second argument in map,
{this.cards.map( ( child, index ) =>
<Grid
click={() => this.clickHandler(child,index)}
active={this.state.current === index}
id={child.id}
key ={child.id} //Provide unique key here
/>
)}
Your click hander should be,
clickHandler = (click,index) => {
this.clicks.push(click.name);
this.setState({
current: index //Set index in a state
})
console.log(this.clicks);
if (this.clicks.length > 1) {
this.compare(click.name);
}
}
You need to initialize current state as empty, otherwise you will get first grid by default active
state = {
current: '',
}
Your Grid component will be this,
const Grid = (props) => {
return (
<div
className={`Child ${props.active ? 'active': ''}`} //Your condition should be this
onClick={props.click}
>
{props.id}
</div>
);
}
Demo
Updating state section
state = {
current: -1, // nothing is selected it contains id of selected card
clicks: [],
}
clickHandler = (selectedId) => {
const { clicks, current } = this.state;
if (current === -1) { // no card selected to check
this.setState({
current: selectedId,
clicks: clicks.includes(selectedId) ? clicks : [...clicks, selectedId],
});
return; // no more in this funtion
}
if (selectedId === current) { // already selected card(totally same card)
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1), // remove last selected click
});
} else { // different card. here check if they have same name
if (this.cards[selectedId].name === this.cards[current].name) {
// couple cards
console.log('Bingo They are Same!!!');
this.setState({
current: -1,
clicks: [...cliks, selectedId], // add just selected card in cliks
});
} else {
// Oh, you failed!
this.setState({
current: -1, // unselect last selected card :(
clicks: clicks.slice(0, clicks.length - 1),
});
}
}
}
Render
...
<Grid
click={() => this.clickHandler(child.id)}
active={this.state.clicks.includes(child.id)} // check if it is included in clicks
id={child.id}
/>
...
Then you can see cool cards game. :)