Animation Problem with Framer Motion and LayoutGroup - reactjs

strong textI'm trying to make a Pokedex App with an animation when we click on a card.
I have a Component PokemonList who render PokemonCard.
When i click on PokemonCard, I navigate to /{pokemonName} and an detail component shows up with an animation.
The Detail component is rendered with of React router
My PokemonCard and my PokemonView (Detail pannel) are in the same Framer Motion LayoutGroup. My cards have the layoutId card-${pokemonInfo.name} and my PokemonView have too. On the opening, i fetch current pokemon with the location.pathname and fill detail pannel with infos.
PokemonView.ts
import { LayoutGroup, motion } from "framer-motion"
import { useLocation, useNavigate } from "react-router-dom"
import styled from "styled-components"
import { colours } from "../../data/typeColors"
import { hexToRgb } from "../../helpers/hexToRgb"
import { useDisableScroll } from "../../hooks/useDisableScroll"
import {
useGetPokemonInfoQuery,
useGetPokemonSpeciesInfoQuery,
} from "../../redux/api/api"
import PokemonInfoPannel from "./PokemonInfoPannel"
import ViewTab from "./Tabs/ViewTab"
const PokemonViewContainer = styled.div`
height: 100%;
width: 100vw;
background-color: ${(props: { background: string }) =>
hexToRgb(props.background, 0.5)};
position: fixed;
top: 0;
display: flex;
justify-content: center;
align-items: center;
`
const PokemonViewStyle = styled(motion.div)`
background-color: ${(props: { background: string }) => props.background};
height: 100%;
width: 100%;
min-height: 680px;
max-width: 600px;
max-height: 800px;
overflow: hidden;
opacity: 1;
#media (min-width: 600px) {
border-radius: 2rem;
box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.5);
}
#media (max-width: 600px) {
max-height: 100vh;
}
`
const PokemonView = () => {
console.log("PokemonView")
const navigate = useNavigate()
useDisableScroll()
const location = useLocation()
const { data: pokemonInfo } = useGetPokemonInfoQuery(
location.pathname.split("/")[1]
)
const { data: pokemonSpeciesInfo } = useGetPokemonSpeciesInfoQuery(
location.pathname.split("/")[1]
)
return (
<>
<PokemonViewContainer
id="pokemon-view-container"
background={colours[pokemonInfo!.type[0]].background}
onClick={(e) => {
navigate("/")
}}
>
<LayoutGroup id="card-swap-animation">
<PokemonViewStyle
layoutId={`card-${pokemonInfo.name}`}
id="pokemon-view"
background={colours[pokemonInfo!.type[0]].background}
onClick={(e) => {
e.stopPropagation()
}}
>
{/* <PokemonInfoPannel
pokemonDetailed={{
info: pokemonInfo!,
speciesInfo: pokemonSpeciesInfo!,
}}
></PokemonInfoPannel>
<ViewTab
pokemonDetailed={{
info: pokemonInfo!,
speciesInfo: pokemonSpeciesInfo!,
}}
></ViewTab> */}
</PokemonViewStyle>
</LayoutGroup>
</PokemonViewContainer>
</>
)
}
export default PokemonView
With this I have the correct behavior. But, I have some typescript errors because my pokemonInfo and pokemonSpeciesInfo can be undefined (i fill them via RTK-Query).
So, I decided to add this before the return
if (!pokemonInfo || !pokemonSpeciesInfo) {
return null
}
But with this line, the first time i click on a card the animation doesn't work. I close detail pannel, open it again and the animation works.
Another think that I don't understand is that when I do that
const { data: pokemonInfo } = useGetPokemonInfoQuery(
location.pathname.split("/")[1]
)
console.log(pokemonInfo)
const { data: pokemonSpeciesInfo } = useGetPokemonSpeciesInfoQuery(
location.pathname.split("/")[1]
)
console.log(pokemonSpeciesInfo)
I see that the first request is never undefined but the second is always, then became fullfilled. I don't know if this is the problem.
Someone can help me ?
Thanks in advance
The source code is here : https://github.com/RomainHoffmann/Pokedex
UPDATE :
I understood why the first console was never null. It's because I do this request on my PokemonCard component, so the response is cached by RTK Query.
To fix my problem, I add the other query in my Card component, it works but I call it "in the void".
I have understand now thats returning <></> in the first render makes the problem but how could I do to avoid Typescript errors and render the component only when datas are fullfiled ?

Related

Passing a direction prop into styled-components keyframe component?

I'm trying to implement a reusable text animation component where a direction prop can be passed in. I probably close but doesn't seem to be working. Also open to better ways of implementing it a better way.
import React from "react"
import styled from "styled-components"
import { GlobalStyles } from "./global"
import TextAnimations from "./Components/TextAnimations"
const Container = styled.div`
display: flex;
justify-content: space-between;
flex-wrap: wrap;
`
const NavBar = styled.nav`
background: #3a3a55;
padding: 0.25rem;
width: 100%;
height: 10vh;
`
const Main = styled.main`
background: #3a3a55;
color: white;
padding: 0.25rem;
flex: 10 1 auto;
height: 100vh;
`
const Break = styled.div`
flex-basis: 100%;
width: 0;
`
function App() {
return (
<>
<GlobalStyles />
<Container>
<NavBar>NavBar</NavBar>
<Break />
<Main>
<TextAnimations text='Sample text from the left' direction='left' />
<TextAnimations text='Sample text from the right' direction='right' />
</Main>
</Container>
</>
)
}
export default App
and then the animation component:
import { motion } from "framer-motion"
import styled, { keyframes } from "styled-components"
type TextAnimationProps = {
text: string
direction: string
}
const Left = keyframes`
0% { left: -3.125em; }
100% { left: 3em;}
`
const Right = keyframes`
0% { Right: -3.125em; }
100% { Right: 3em;}
`
const HomeHeader = styled.div`
h1 {
font-weight: lighter;
}
position: relative;
top: 0;
animation: ${(props) => (props.defaultValue === "left" ? Left : Right)} // is this right?
animation-duration: 3s;
animation-fill-mode: forwards;
`
const TextAnimations = ({ text, direction }: TextAnimationProps) => {
return (
<HomeHeader defaultValue={direction}>
<h1>{text}</h1>
</HomeHeader>
)
}
export default TextAnimations
I'd also like to make it more flexible so that I can add 'top', 'bottom', etc.
Is this the best way to handle text animations?
You can create a separate function to set the animation. The function will receive the props of the styled component from which the function will access the direction prop.
const setHomeHeaderAnimation = ({ direction = "left" }) => {
const animation = keyframes`
0% {
${direction}: -3.125em;
}
100% {
${direction}: 3em;
}
`;
return css`
position: relative;
animation: ${animation};
animation-duration: 3s;
animation-fill-mode: forwards;
`;
};
const HomeHeader = styled.div`
${setHomeHeaderAnimation}
h1 {
font-weight: lighter;
}
`;
const App = () => (
<div>
<HomeHeader>
<div>Some text</div>
</HomeHeader>
<HomeHeader direction="right">
<div>Some text</div>
</HomeHeader>
<HomeHeader direction="top">
<div>Some text</div>
</HomeHeader>
<HomeHeader direction="bottom">
<div>Some text</div>
</HomeHeader>
</div>
);

Use Navigate function not working (React)

I'm following a tutorial and everything was going great until I tried to implement Navigation through a search input. For instance, If I am on http://localhost:3000/searched/profile. Typing out an input of 'names' in the search bar should take me to http://localhost:3000/searched/names. In the tutorial it worked that way and I believe I did the same thing but it doesn't work for me
First below is the Search component for the search bar and its input
And then the Pages where my routing is done. My Browser Router is in the App.js.
import styled from "styled-components"
import { FaSearch } from 'react-icons/fa'
import { useState } from 'react'
import {useNavigate} from 'react-router-dom'
function Search() {
const [input, setInput] = useState('');
const navigate = useNavigate();
const submitHandler = (e) => {
e.preventDefault();
navigate('/searched/' + input) (I GUESS THIS IS WHAT IS NOT WORKING)
};
return (
<FormStyle onSubmit={submitHandler}>
<div>
<FaSearch></FaSearch>
<input onChange={(e) => setInput(e.target.value)} type="text" value={input}/>
</div>
<h1>{input}</h1>
</FormStyle>
)
}
const FormStyle = styled.div`
margin: 0 20rem;
div{
width: 100%;
position: relative;
}
input{
border: none;
background: linear-gradient(35deg, #494949, #313131);
border-radius: 1rem;
outline: none;
font-size: 1.5rem;
padding: 1rem 3rem;
color: white;
width: 100%;
}
svg{
position: absolute;
top: 50%;
left: 0%;
transform: translate(100%, -50%);
color: white;
}
`
export default Search
Pages
import Home from "./Home"
import { Route, Routes } from 'react-router-dom'
import Cuisine from "./Cuisine"
import Searched from "./Searched"
function Pages() {
return (
<Routes>
<Route path='/' element={<Home/>} />
<Route path='/cuisine/:type' element={<Cuisine/>} />
<Route path='/searched/:search' element={<Searched/>} />
</Routes>
)
}
export default Pages
The FormStyle component is a styled div element instead of a form element, so the onSubmit handler is meaningless on the div. To resolve you should use the form element so the form submission works as you are expecting.
Search.js Example:
import styled from "styled-components";
import { FaSearch } from "react-icons/fa";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
function Search() {
const [input, setInput] = useState("");
const navigate = useNavigate();
const submitHandler = (e) => {
e.preventDefault();
navigate("/searched/" + input);
};
return (
<FormStyle onSubmit={submitHandler}> // <-- (2) onSubmit works now
<div>
<FaSearch></FaSearch>
<input
onChange={(e) => setInput(e.target.value)}
type="text"
value={input}
/>
</div>
<h1>{input}</h1>
</FormStyle>
);
}
const FormStyle = styled.form` // <-- (1) switch to form element
margin: 0 20rem;
div {
width: 100%;
position: relative;
}
input {
border: none;
background: linear-gradient(35deg, #494949, #313131);
border-radius: 1rem;
outline: none;
font-size: 1.5rem;
padding: 1rem 3rem;
color: white;
width: 100%;
}
svg {
position: absolute;
top: 50%;
left: 0%;
transform: translate(100%, -50%);
color: white;
}
`;
export default Search;

how to make react webcam responsive

[`
import React, { useCallback, useRef, useState } from 'react';
import Webcam from 'react-webcam';
import RadioButtonUncheckedIcon from '#material-ui/icons/RadioButtonUnchecked';
import { useDispatch } from 'react-redux';
import { setCameraImage } from './features/cameraSlice';
import { useHistory } from 'react-router-dom';
import './WebcamCapture.css';
var elem =
document.compatMode === 'CSS1Compat'
? document.documentElement
: document.body;
const videoConstraints = {
width: elem.clientWidth,
height: elem.clientHeight,
facingMode: 'environment',
};
function WebcamCapture() {
const webcamRef = useRef(null);
const dispatch = useDispatch();
const history = useHistory();
const capture = useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
dispatch(setCameraImage(imageSrc));
history.push('/preview');
}, [webcamRef]);
return (
<div className="webcamCapture">
<Webcam
audio={false}
height={videoConstraints.height}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={videoConstraints.width}
videoConstraints={videoConstraints}
/>
<RadioButtonUncheckedIcon
className="webcamCapture__button"
onClick={capture}
fontSize="large"
/>
</div>
);
}
export default WebcamCapture;
.webcamCapture {
position: relative;
width: 100%;
}
.webcamCapture__button {
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
color: white;
}
`]1
`I want the video element to take after the screen's dimensions and take up 100% of the screen view at all times, including when browser is resized or when done on mobile but there seems to be a certain ratio restriction.
for mobile devices its not taking up the entire screen
please help me with the entire code so that it's responsive on all devices
To apply a custom style for mobile screens, you will have to detect mobile screens first using
#media then apply custom style.
I have added a solution below which I expect to work, detecting 640px & 768px max width devices and I have added height:100%;, width:100%;.
.webcamCapture {
position: relative;
width: 100%;
}
#media only screen and (max-device-width : 640px) {
.webcamCapture {
position:fixed;
height:100%;
width:100%;
}
}
#media only screen and (max-device-width: 768px) {
.webcamCapture {
position:fixed;
height:100%;
width:100%;
}
}

How to make #emotion/style styles change when a class is changed on a div

SandBox: https://codesandbox.io/s/infallible-nash-x91zz?file=/src/App.js
In the sandbox I have an emotion style defined. It has two classes it in — open and closed.
I am using a state to toggle the classes and the classes are toggling correctly according to the inspector.
Problem: styles not updating on state change.
Expected behavior: background color on div will change when class changes between open and closed
Actual Behavior: The classes are being updated but the stiles are not.
Code:
import React, { useState } from "react";
import "./styles.css";
import styled from "#emotion/styled";
const MenuContainer = styled.div`
.open {
background-color: blue;
width: 600px;
height: 600px;
}
.closed {
background-color: red;
width: 600px;
height: 600px;
}
`;
export default function App() {
const [openState, setOpenState] = useState(false);
return (
<MenuContainer className={openState ? "closed" : "open"}>
<button value="click" onClick={() => setOpenState(!openState)}>
Click Me
</button>
</MenuContainer>
);
}
You should do this:
const MenuContainer = styled.div`
width: 600px;
height: 600px;
&.open {
background-color: blue;
}
&.closed {
background-color: red;
}
`;

Creating a simple animation in React-Pose

I'm having trouble creating a simple animation in React-Pose. The two problems are
1) I can't get the animation to revert to the initial condition. The hovering variable is changing to false when the mouse leaves, but it the animation doesn't change back.
2) I can't manipulate the animation, I wanted to have a longer duration and maybe an ease out or something, but its just an instant snap to the hovered status.
import React, { useState } from 'react';
import styled from 'styled-components';
import posed from 'react-pose';
import { render } from 'react-dom';
const UpFor = () => {
const [hovering, setHovering] = useState(false);
const HoverContainer = posed.div({
hoverable: true
})
const Container = styled(HoverContainer)`
font-family: 'Baumans';
font-size: 220px;
display: flex;
cursor: pointer;
`
const Up = styled.div`
color: #81D6E3;`
const Four = styled.div`
color: #FF101F
`
const Fours = styled.div`
display: flex;
`
const MirroredFour = posed.div({
unhovered: {transform: 'rotatey(0deg)'},
hovered: {transform: 'rotateY(180deg)',
transition: {
type: 'tween',
duration: '2s'
}}
})
const SecondFour = styled(MirroredFour)`
color: #FF101F
position: absolute;
transform-origin: 67%;
`
return (
<Container onMouseEnter={() => {setHovering({ hovering: true }), console.log(hovering)}}
onMouseLeave={() => {setHovering({ hovering: false }), console.log(hovering)}}>
<Up>Up</Up><Fours><Four>4</Four>
<SecondFour pose={hovering ? "hovered" : "unhovered"}
>4</SecondFour></Fours>
</Container>)
}
export default UpFor
There were two main issues with your code:
duration does not appear to support string values like '2s'. I changed this to 2000.
You were defining your components (e.g. using styled.div, posed.div) inside of your render function. This caused these components to be treated by React as unique component types with each re-render. This results in those components being unmounted and re-mounted each render which prevents transitions from working since the element isn't changing -- instead it is being replaced by a new component of a different type.
Below is a working version of your code which moves the component definitions outside of the render (UpFor) function. You can play around with it in the sandbox provided.
import React, { useState } from "react";
import styled from "styled-components";
import posed from "react-pose";
const Container = styled.div`
font-family: "Baumans";
font-size: 220px;
display: flex;
cursor: pointer;
`;
const Up = styled.div`
color: #81d6e3;
`;
const Four = styled.div`
color: #ff101f;
`;
const Fours = styled.div`
display: flex;
`;
const MirroredFour = posed.div({
unhovered: { transform: "rotateY(0deg)" },
hovered: {
transform: "rotateY(180deg)",
transition: {
type: "tween",
duration: 2000
}
}
});
const SecondFour = styled(MirroredFour)`
color: #FF101F
position: absolute;
transform-origin: 67%;
`;
const UpFor = () => {
const [hovering, setHovering] = useState(false);
console.log("hovering", hovering);
return (
<Container
onMouseEnter={() => {
setHovering(true);
}}
onMouseLeave={() => {
setHovering(false);
}}
>
<Up>Up</Up>
<Fours>
<Four>4</Four>
<SecondFour pose={hovering ? "hovered" : "unhovered"}>4</SecondFour>
</Fours>
</Container>
);
};
export default UpFor;

Resources