I am trying to make a 3D terminal for my portfolio. Currently developing the project in Next.js.
The idea is to use an existing package (https://www.npmjs.com/package/crt-terminal) and put inside the tag of the Drei library for react-three-fiber.
This is the code for model component:
import React, { useState, useEffect } from "react";
import { Canvas } from "#react-three/fiber";
import { OrbitControls, Stats } from "#react-three/drei";
import { useTheme } from "next-themes";
import TestComponent from "./TestComponent";
import { motion } from "framer-motion";
function Model() {
const { theme, setTheme } = useTheme();
const [initialZoom, setInitialZoom] = useState(null);
useEffect(() => {
if (window.innerWidth < 1000) {
setInitialZoom(0.5);
} else {
setInitialZoom(1);
}
}, []);
return (
<motion.div
id="canvas-container"
initial={{ y: 50 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
>
{initialZoom && (
<Canvas
camera={{ zoom: initialZoom }}
style={{ width: "100%", height: 700 }}
>
<OrbitControls
enablePan={true}
enableZoom={true}
enableRotate={true}
minPolarAngle={Math.PI / 2.2}
maxPolarAngle={Math.PI / 1.8}
minAzimuthAngle={-Math.PI / 12}
maxAzimuthAngle={Math.PI / 12}
/>
<TestComponent />
<Stats />
</Canvas>
)}
</motion.div>
);
}
export default Model;
And this is the Html component to render.
import React, { useEffect, useState, useRef } from "react";
import { Terminal, useEventQueue, textLine, textWord } from "crt-terminal";
import { IoTerminal } from "react-icons/io5";
import { VscChromeMaximize, VscChromeMinimize } from "react-icons/vsc";
import { CgClose } from "react-icons/cg";
import { OrbitControls, Html, Icosahedron } from "#react-three/drei";
import { Canvas, useFrame } from "react-three-fiber";
import { useThree } from "#react-three/fiber";
const bannerText = `
Welcome to my CLI. Kind of
`;
export default function TestComponent(props) {
const eventQueue = useEventQueue();
const [screenDimensions, setScreenDimensions] = useState([]);
const { print } = eventQueue.handlers;
const { size } = useThree();
useEffect(() => {
const currentDim = [size.width, size.height];
setScreenDimensions(currentDim);
}, []);
return (
<group>
<mesh position={[0, 0, 0]}>
<Html transform position={[0, 0, 0]}>
<div
style={{
width: screenDimensions[0] * 0.5,
height: screenDimensions[1] * 0.3,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
className="html-transform-div"
>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<IoTerminal style={{ fontSize: 8, color: "black" }} />
<h1 style={{ fontSize: 6, marginLeft: 3, color: "black" }}>
Command Line
</h1>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<div className="canvas-minimize-div">
<VscChromeMinimize
style={{ fontSize: 8, marginTop: 4, color: "black" }}
/>
</div>
<div className="canvas-maximize-div">
<VscChromeMaximize style={{ fontSize: 8, color: "black" }} />
</div>
<div className="canvas-close-div">
<CgClose
style={{
fontSize: 8,
color: "#010308",
}}
/>
</div>
</div>
<Terminal
queue={eventQueue}
banner={[
textLine({ words: [textWord({ characters: bannerText })] }),
]}
className="Terminal"
onCommand={(command) =>
print([
textLine({
words: [
textWord({ characters: "You entered command: " }),
commandWord({ characters: command, prompt: ">" }),
],
}),
])
}
/>
</div>
</Html>
</mesh>
</group>
);
}
The component renders fine, appearing sharp, but then when I use the controls to move the characters all get blurred. This problem only happens for desktop computers, everything works perfectly for mobile devices.
Before move
After move
Any ideia how to fix?
Thank you
Related
I want to make a progress bar in image with Material UI in React like this:
I tried with a customProgress bar, something like:
export const CustomLinearProgress = styled(LinearProgress)(({ theme }) => ({
height: 35,
borderRadius: 3,
[`&.${ linearProgressClasses.colorPrimary }`]: {
backgroundColor: '#00b4e559',
},
[`&.${ linearProgressClasses.bar }`]: {
borderRadius: 3,
backgroundColor: "#00B4E5"
},
}));
but it doesn't work.
You can use MUI's CircularProgress component and add a label for the progress percent inside it.
import * as React from "react";
import CircularProgress, {
CircularProgressProps,
} from "#mui/material/CircularProgress";
import Typography from "#mui/material/Typography";
import Box from "#mui/material/Box";
function CircularProgressWithLabel(
props: CircularProgressProps & { value: number }
) {
return (
<Box sx={{ position: "relative", display: "inline-flex" }}>
<CircularProgress
variant="determinate"
size={150}
thickness={5}
{...props}
/>
<Box
sx={{
top: 0,
left: 0,
bottom: 0,
right: 0,
position: "absolute",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography
variant="caption"
component="div"
color="text.secondary"
>{`${Math.round(props.value)}%`}</Typography>
</Box>
</Box>
);
}
export default function CircularStatic() {
const [progress, setProgress] = React.useState(10);
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((prevProgress) =>
prevProgress >= 100 ? 0 : prevProgress + 10
);
}, 800);
return () => {
clearInterval(timer);
};
}, []);
return <CircularProgressWithLabel value={progress} />;
}
Change size and thickness props to achieve the style you want.
Learn more ways to control your circular progress in it's API page.
I am working on a food app that adds items to a cart then calculates the total amount. I am trying to implement a reducer as opposed to having multiple instances of state hooks and I am running into this error when trying to click open my cart modal.
I know the items are being added successfully to the cart because when I add them, then inspect in the console and remove the "display: none " property from my modal and see the added food items there. For some reason its specifically when I click my cart to see the modal is when I receive the error.
The error message is saying the error is located in my app.js file where I assign define the cartFoods prop to my Cartmodal component
App.js Component
import logo from "./logo.svg";
import "./App.css";
import NavHeader from "./NavHeader";
import { useState, useEffect, useReducer } from "react";
import FoodList from "./FoodList";
import CartModal from "./CartModal";
import "animate.css";
function App() {
const [foods, setFoods] = useState([
{
food: "sushi roll",
description: "fresh tuna topped with eel sauce and avocado",
price: "14.99",
quantity: 1,
},
{
food: "pizza",
description: "baked to a crisp with mozarrella, basil and tomato sauce",
price: "9.99",
quantity: 1,
},
{
food: "special fried rice",
description: "cooked with onions , shrimp , chicken",
price: "19.99",
quantity: 1,
},
{
food: "tacos",
description:
"choice of either ,shrimp , chicken, or steak, topped with pico de gallo",
price: "15.99",
quantity: 1,
},
]);
const [totalFoodAmount, setTotalFoodAmount] = useState(0);
// const [modalDisplay, setModalDisplay] = useState("none");
const [totalPrice, setTotalPrice] = useState(0);
const [cartFoods, setCartFoods] = useState([]);
const [cartItem, setCartItem] = useState({});
const [modalDisplay, setModalDisplay] = useState("none");
const [amountValue, setAmountValue] = useState(0);
function foodReducer(state, action) {
if (action.type === "ADD_CART_ITEMS") {
return { cartFoodItems: state.cartFoodItems.concat(action.val) };
}
}
const [foodState, dispatch] = useReducer(foodReducer, {
modalDisplay: "none",
totalFoodPrice: 0,
cartFoodItems: [],
});
function setDisplay() {
dispatch({ type: "ADD_MODAL_DISPLAY" });
setModalDisplay("flex");
}
function removeModal() {
dispatch({ type: "REMOVE_MODAL_DISPLAY" });
setModalDisplay("none");
}
function setQuantity(e) {
console.log(e.target.value);
setAmountValue(e.target.value);
}
function addFood(food) {
console.log(food);
let newFoodItem = { ...food, quantity: amountValue };
// setCartFoods((prevState) => cartFoods.concat(newFoodItem));
dispatch({ type: "ADD_CART_ITEMS", val: newFoodItem });
// ask slavo specific question on why state doesnt update right away when you console.log(cartFoods) inside the function
}
useEffect(() => {
let total = foodState.cartFoodItems.reduce(function (a, b) {
return a + Number(b.quantity);
}, 0);
setTotalFoodAmount(total);
foodState.cartFoodItems.map((food) =>
setTotalPrice(totalPrice + food.price * food.quantity)
);
}, [foodState]);
return (
<div className="App">
<NavHeader cartQuantity={totalFoodAmount} setDisplay={setDisplay} />
<FoodList foodList={foods} addFood={addFood} setAmount={setQuantity} />
<CartModal
cartFoods={foodState.cartFoodItems}
display={modalDisplay}
closeModal={removeModal}
totalPrice={totalPrice}
/>
</div>
);
}
export default App;
Cart.js component
import React from "react";
export default function Cart(props) {
return (
<div
style={{
display: "flex",
marginRight: "35px",
border: "1px solid lightgray",
padding: "5px",
borderRadius: "5px",
margin: "10px",
}}
className="cart-section"
onClick={props.setDisplay}
>
<div style={{ marginRight: "20px" }}>
<h4>Your Cart</h4>
</div>
<div style={{ position: "relative", top: "0vh" }}>
<h4>Quantity: {props.cartQuantity}</h4>
</div>
</div>
);
}
CartModal.js
import React from "react";
import "animate.css";
import { CSSTransition } from "react-transition-group";
export default function CartModal(props) {
return (
<div
style={{
background: "#000000a6",
width: "100vw",
height: "100vh",
position: "fixed",
top: "0",
left: "0",
overflow: "scroll",
display: props.display,
justifyContent: "center",
alignItems: "center",
}}
>
<div
style={{
background: "white",
padding: "30px",
minHeight: "60vh",
minWidth: "75vw",
borderRadius: "10px",
}}
className="cart-modal"
>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<h3 onClick={props.closeModal}>x</h3>
</div>
<h4 style={{ textAlign: "center" }}>Your Cart</h4>
{props.cartFoods.length === 0 ? (
<h1>There are no Items in your cart</h1>
) : (
props.cartFoods.map((food, index) => (
<div
style={{
display: "flex",
justifyContent: "space-between",
borderBottom: "2px solid lightgray",
}}
key={index}
>
<div style={{ width: "10vh" }}>
<h3>{food.food}</h3>
<p>{food.price}</p>
<p>Quantity: {food.quantity}</p>
</div>
<div style={{ display: "flex" }}>
<button style={{ margin: "10px", maxHeight: "20px" }}>-</button>
<button style={{ margin: "10px", maxHeight: "20px" }}>+</button>
</div>
</div>
))
)}
<h2>Price: ${props.totalPrice}</h2>
</div>
</div>
);
}
This model seems to load completely black - but others do not. https://d1iczm3wxxz9zd.cloudfront.net/e4a5c9ee-9fa0-46b2-9ff5-8e52e6286a3b/5ee3baf2-2524-4617-99a7-2a91b4bd20c1/11/ITEM_PREVIEW1.glb
Anyone happen to know why?
Other GLB files have no issues with textures. This one seems to have an issue. But when loaded in other online GLB viewers it loads fine.
Using reactjs, react-three-fibre and three.js.
Here is the three.js code:
import * as THREE from "three";
import React, { useRef, Suspense } from "react";
import { Canvas, useThree, useFrame, extend } from "react-three-fiber";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { Box } from "./Box";
import { Model } from "./Model";
extend({ OrbitControls });
const Controls = (props) => {
const { gl, camera } = useThree();
const ref = useRef();
//#ts-ignore
useFrame(() => ref.current.update());
//#ts-ignore
return <orbitControls ref={ref} args={[camera, gl.domElement]} {...props} />;
};
const ThreeJSComponent = (props) => {
const {
width,
height,
fallback,
modelSuccess,
setBackground,
background,
} = props;
return (
<div
style={{
width: width,
height: height,
position: "relative",
}}
>
<div
onClick={() => setBackground("black")}
style={{
position: "absolute",
top: "0",
right: "0",
width: "25px",
borderRadius: "5px",
height: "25px",
background: "black",
zIndex: 50,
border: "white 1px solid",
}}
></div>
<div
onClick={() => setBackground("white")}
style={{
position: "absolute",
top: "0",
right: "50px",
width: "25px",
height: "25px",
borderRadius: "5px",
background: "white",
border: "black 1px solid",
zIndex: 50,
}}
></div>
<Canvas
style={{
width: "100%",
height: "100%",
background: background,
}}
onCreated={({ gl }) => {
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
//#ts-ignore
gl.outputEncoding = false;
gl.toneMappingExposure = 0;
}}
camera={{
position: [0, 0, 10],
isPerspectiveCamera: true,
}}
//shadowMap
>
{" "}
<ambientLight intensity={0x222222} />
<Suspense fallback={fallback}>
<Model url={props.modelURL} modelSuccess={modelSuccess} />
</Suspense>
<Controls
autoRotate
enablePan={true}
enableZoom={true}
enableDamping
dampingFactor={0.5}
rotateSpeed={1}
/>
</Canvas>
</div>
);
};
export default ThreeJSComponent;
and Model:
import React, { useState, useEffect } from "react";
import { useLoader } from "react-three-fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
export const Model = (props) => {
const { url, modelSuccess } = props;
const [model, setModel] = useState();
const loadedModel = useLoader(GLTFLoader, url, (loader) => {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco-gltf/");
loader.setDRACOLoader(dracoLoader);
});
useEffect(() => {
if (!model) {
setModel(loadedModel);
}
}, []);
if (model) {
modelSuccess();
return (
<primitive
position={[0, 0, 0]}
scale={[1, 1, 1]}
object={model.scene}
dispose={null}
></primitive>
);
} else return null;
};
you dont need setState, useLoader uses suspense, loadedModel will contain the model guaranteed, or the component will throw.
const { scene } = useLoader(GLTFLoader, url, ...)
return <primitive object={scene} dispose={null} />
ambientlight intensity is also wrong, 0 is no light, 1 is the default, then it goes higher. you're giving it an impossibly high value.
other than that it could be that the model is outside the cameras frustrum (too big), it could also be too small. or the path is wrong (it should either be in /public, or you need to it import modelUrl as "./model.glb".
here's a working example: https://codesandbox.io/s/r3f-ibl-envmap-simple-k7q9h?file=/src/App.js
import React, { Suspense } from 'react'
import { Canvas, useLoader } from 'react-three-fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls, Html, draco } from 'drei'
function Model({ url }) {
const { scene } = useLoader(GLTFLoader, url, draco())
return <primitive object={scene} dispose={null} />
}
export default function App() {
return (
<Canvas camera={{ position: [0, 5, 1] }}>
<directionalLight position={[10, 10, 5]} intensity={2} />
<directionalLight position={[-10, -10, -5]} intensity={1} />
<OrbitControls />
<Suspense fallback={<Html>loading..</Html>}>
<Model url="/suzanne-draco.glb" />
</Suspense>
</Canvas>
)
}
So I got the latest versions of jss and material ui.
Im using withStyles HOC and dynamically changing css in styles object, but I cant seem to be able to declare keyframes in css. Im also using nextjs if that makes any difference. Ive looked at how material-ui declares their animations, and I'm following their example https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/ButtonBase/TouchRipple.js.
import React, { useEffect, useState } from "react";
import classnames from "classnames";
import PropTypes from "prop-types";
import { withStyles, makeStyles } from "#material-ui/core/styles";
import { sectionAnchors, maxContentWidth } from "../../../util/constants";
import LearnMoreLink from "./LearnMoreLink";
import SectionContent from "../../common/SectionContent";
import content from "../../../../content";
import useRealHeight from "../../../util/useRealHeight";
import SplashHeading from "./SplashHeading";
import { singleHeight, doubleHeight } from "../../common/Footer";
import GetStartedForm from "./GetStartedForm";
import { withRouter } from "next/router";
import SwipeableTextMobileStepper from "./SwipeableTextMobileStepper";
import { Link } from 'next/router'
import Radio from '#material-ui/core/Radio';
import RadioGroup from '#material-ui/core/RadioGroup';
import FormHelperText from '#material-ui/core/FormHelperText';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import { green } from '#material-ui/core/colors';
import RadioButtonUncheckedIcon from '#material-ui/icons/RadioButtonUnchecked';
import RadioButtonCheckedIcon from '#material-ui/icons/RadioButtonChecked';
import Switch from '#material-ui/core/Switch';
import Button from '#material-ui/core/Button';
const styles = theme => ({
heading: {
marginBottom: theme.spacing.unit * 6
},
headingColored: {
marginBottom: 0,
color: theme.palette.primary.dark
},
container: {
width: "100%",
// '#keyframes fade': {
// '0%': {
// opacity: 1
// },
// '100%': {
// opacity: 0
// }
// },
'#keyframes enter': {
'0%': {
transform: 'scale(0)',
opacity: 0.1,
},
'100%': {
transform: 'scale(1)',
opacity: 0.3,
},
},
// backgroundColor: theme.palette.primary.dark,
// background: 'url(https://images.newscientist.com/wp-content/uploads/2019/04/08111018/screenshot-2019-04-08-10.24.34.jpg)',
backgroundImage: props => props.background,
backgroundSize: "cover",
// animation: '$fadeMe linear 1s infinite',
// animationName: '#fadeMe',
// animdationDuration: '1s',
// animation:
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center"
},
containerWizard: {
background: "none",
marginBottom: -doubleHeight,
paddingBottom: doubleHeight + theme.spacing.unit * 3,
[theme.breakpoints.up("sm")]: {
marginBottom: -singleHeight,
paddingBottom: singleHeight + theme.spacing.unit * 3
}
},
inner: {
maxWidth: maxContentWidth,
width: "100%",
flex: "1 0 auto",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
},
flex: {
flex: "1 0 auto"
},
form: {
width: "100%",
maxWidth: maxContentWidth / 2,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}
});
// http://www.coverbash.com/wp-content/covers/caeli_flowers_facebook_cover.jpg
function SplashLogic() {
const [selectedValue, setSelectedValue] = React.useState(1);
let int
useEffect(() => { int = init(1) }, []);
let background = selectedValue == 1 ? bG('http://www.coverbash.com/wp-content/covers/caeli_flowers_facebook_cover.jpg') :
bG('https://images.newscientist.com/wp-content/uploads/2019/04/08111018/screenshot-2019-04-08-10.24.34.jpg?')
const handleChange = event => {
setSelectedValue(event.target.value);
clearInterval(int)
int = init(event.target.value)
};
function init(num) {
return setInterval(() => {
background = bG('http://www.coverbash.com/wp-content/covers/caeli_flowers_facebook_cover.jpg')
setSelectedValue(2)
}, 4000)
}
return <SplashFull {...{ setSelectedValue, selectedValue, background, handleChange }}></SplashFull>
}
const SplashSection = ({ classes, router, setSelectedValue, selectedValue, background, handleChange }) => {
// const [height] = useRealHeight({ resize: false });
console.log(classes.container)
useEffect(() => {
router.prefetch("/signup");
}, []);
const onSubmit = values => {
router.push(`/signup?destination=${values.destination}`, "/signup");
};
return (
<SectionContent id={sectionAnchors.SPLASH} className={classes.container}>
<div className="bg-white opacity-50 rounded-full">
<Radio
checked={selectedValue === 1}
onChange={handleChange}
value={1}
name="radio-button-demo"
inputProps={{ 'aria-label': 'A' }}
/>
<Radio
checked={selectedValue === 2}
onChange={handleChange}
value={2}
name="radio-button-demo"
inputProps={{ 'aria-label': 'B' }}
/>
<Radio
checked={selectedValue === 3}
onChange={handleChange}
value={3}
name="radio-button-demo"
inputProps={{ 'aria-label': 'D' }}
/>
<Radio
checked={selectedValue === 4}
onChange={handleChange}
value={4}
name="radio-button-demo"
inputProps={{ 'aria-label': 'E' }}
/>
</div>
<div className={classes.inner}>
<SplashHeading
classes={{
container: classes.heading
}}
/>
<GetStartedForm className={classes.form} onSubmit={onSubmit} />
</div>
<Button variant="contained" >
Default
</Button>
<LearnMoreLink />
</SectionContent>
// <SwipeableTextMobileStepper></SwipeableTextMobileStepper>
);
};
SplashSection.propTypes = {
classes: PropTypes.object.isRequired
};
function bG(arg) {
return `url(${arg})`
}
const SplashFull = withStyles(styles)(withRouter(SplashSection));
export default SplashLogic
I get error:
container.addRule(...).addRule is not a function
TypeError: container.addRule(...).addRule is not a function
at Array.onProcessStyle (/workspace/travelcontacts/node_modules/jss-plugin-nested/dist/jss-plugin-nested.cjs.js:96:10)
at PluginsRegistry.onProcessStyle (/workspace/travelcontacts/node_modules/jss/dist/jss.cjs.js:1246:51)
at PluginsRegistry.onProcessRule (/workspace/travelcontacts/node_modules/jss/dist/jss.cjs.js:1235:26)
at Array.forEach (<anonymous>)
at RuleList.process (/workspace/travelcontacts/node_modules/jss/dist/jss.cjs.js:871:25)
at new StyleSheet (/workspace/travelcontacts/node_modules/jss/dist/jss.cjs.js:1041:16)
at Jss.createStyleSheet (/workspace/travelcontacts/node_modules/jss/dist/jss.cjs.js:2007:17)
at attach (/workspace/travelcontacts/node_modules/#material-ui/styles/makeStyles/makeStyles.js:116:39)
at /workspace/travelcontacts/node_modules/#material-ui/styles/makeStyles/makeStyles.js:256:7
at useSynchronousEffect (/workspace/travelcontacts/node_modules/#material-ui/styles/makeStyles/makeStyles.js:210:14)
at /workspace/travelcontacts/node_modules/#material-ui/styles/makeStyles/makeStyles.js:248:5
at Object.WithStyles [as render] (/workspace/travelcontacts/node_modules/#material-ui/styles/withStyles/withStyles.js:70:21)
at ReactDOMServerRenderer.render (/workspace/travelcontacts/node_modules/react-dom/cjs/react-dom-server.node.development.js:3758:44)
at ReactDOMServerRenderer.read (/workspace/travelcontacts/node_modules/react-dom/cjs/react-dom-server.node.development.js:3538:29)
at renderToString (/workspace/travelcontacts/node_modules/react-dom/cjs/react-dom-server.node.development.js:4247:27)
at render (/workspace/travelcontacts/node_modules/next-server/dist/server/render.js:86:16)
The issue appears to be resolved by not nesting #keyframes definitions. The hint was in the second application of the addRule function in the error: container.addRule(...).addRule is not a function.
Solution
Try moving your keyframe animations to the root level. So from this
const styles = theme => ({
container: {
'#keyframes enter': {
'0%': {
transform: 'scale(0)',
opacity: 0.1,
},
'100%': {
transform: 'scale(1)',
opacity: 0.3,
},
}
}
}
to this
const styles = theme => ({
'#keyframes enter': {
'0%': {
transform: 'scale(0)',
opacity: 0.1,
},
'100%': {
transform: 'scale(1)',
opacity: 0.3,
},
}
}
Hope that helps.
Interesting side note: nesting the animation another level removes the error, but does not instantiate the CSS animation.
from
const styles = theme => ({
container: {
'#keyframes enter': {
'0%': {
transform: 'scale(0)',
opacity: 0.1,
},
'100%': {
transform: 'scale(1)',
opacity: 0.3,
},
}
}
}
to
const styles = theme => ({
'#global':{ //need add into global rules
'#keyframes enter': {
'0%': {
transform: 'scale(0)',
opacity: 0.1,
},
'100%': {
transform: 'scale(1)',
opacity: 0.3,
},
}
}
}
I am using a React PDF viewer in my project. I have a react mui dialog component that I use with react draggable to drag it around.
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import makeStyles from "#material-ui/core/styles/makeStyles";
import DialogContent from "#material-ui/core/DialogContent";
import IconButton from "#material-ui/core/IconButton";
import ClearIcon from "#material-ui/icons/Clear";
import Draggable from "react-draggable";
import Paper from "#material-ui/core/Paper";
import Dialog from "#material-ui/core/Dialog";
import PDFViewer from "./PDFViewer";
function PaperComponent({...props}) {
return (
<Draggable
>
<Paper {...props} />
</Draggable>
);
}
const StyledDialog = withStyles({
root: {
pointerEvents: "none"
},
paper: {
pointerEvents: "auto"
},
scrollPaper: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginRight: 20
}
})(props => <Dialog hideBackdrop {...props} />);
const useStyles = makeStyles({
dialog: {
cursor: 'move'
},
dialogContent: {
'&:first-child': {
padding: 10,
background: 'white'
}
},
clearIcon: {
position: 'absolute',
top: -20,
right: -20,
background: 'white',
zIndex: 1,
'&:hover': {
background: 'white'
}
},
paper: {
overflowY: 'visible',
maxWidth: 'none',
maxHeight: 'none',
width: 550,
height: 730
}
});
const PDFModal = (props) => {
const classes = useStyles();
const {open, onClose, pdfURL} = props;
return (
<StyledDialog
open={open}
classes={{root: classes.dialog, paper: classes.paper}}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog"
>
<DialogContent classes={{root: classes.dialogContent}} id="draggable-dialog">
<IconButton className={classes.clearIcon} aria-label="Clear" onClick={onClose}>
<ClearIcon/>
</IconButton>
<PDFViewer
url={pdfURL}
/>
</DialogContent>
</StyledDialog>
);
};
export default PDFModal;
And this is the PDFViewer component:
import React from 'react';
import { Viewer, SpecialZoomLevel, Worker } from '#react-pdf-viewer/core';
import { defaultLayoutPlugin } from '#react-pdf-viewer/default-layout';
import '#react-pdf-viewer/core/lib/styles/index.css';
import '#react-pdf-viewer/default-layout/lib/styles/index.css';
import ArrowForward from "#material-ui/icons/ArrowForward";
import ArrowBack from "#material-ui/icons/ArrowBack";
import Button from "#material-ui/core/Button";
import IconButton from "#material-ui/core/IconButton";
import RemoveCircleOutlineIcon from '#material-ui/icons/RemoveCircleOutline';
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
import './PDFViewer.css';
const PDFViewer = ({url}) => {
const renderToolbar = (Toolbar) => (
<Toolbar>
{
(slots) => {
const {
CurrentPageLabel, CurrentScale, GoToNextPage, GoToPreviousPage, ZoomIn, ZoomOut,
} = slots;
return (
<div
style={{
alignItems: 'center',
display: 'flex',
}}
>
<div style={{ padding: '0px 2px' }}>
<ZoomOut>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<RemoveCircleOutlineIcon />
</IconButton>
)
}
</ZoomOut>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentScale>
{
(props) => (
<span>{`${Math.round(props.scale * 100)}%`}</span>
)
}
</CurrentScale>
</div>
<div style={{ padding: '0px 2px' }}>
<ZoomIn>
{
(props) => (
<IconButton aria-label="delete" onClick={props.onClick}>
<AddCircleOutlineIcon />
</IconButton>
)
}
</ZoomIn>
</div>
<div style={{ padding: '0px 2px', marginLeft: 'auto' }}>
<GoToPreviousPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowBack fontSize="small"/>
</Button>
)
}
</GoToPreviousPage>
</div>
<div style={{ padding: '0px 2px' }}>
<CurrentPageLabel>
{
(props) => (
<span>{`${props.currentPage + 1} av ${props.numberOfPages}`}</span>
)
}
</CurrentPageLabel>
</div>
<div style={{ padding: '0px 2px' }}>
<GoToNextPage>
{
(props) => (
<Button
style={{
cursor: props.isDisabled ? 'not-allowed' : 'pointer',
height: '30px',
width: '30px'
}}
disabled={props.isDisabled}
disableElevation
disableFocusRipple
onClick={props.onClick}
variant="outlined">
<ArrowForward fontSize="small"/>
</Button>
)
}
</GoToNextPage>
</div>
</div>
)
}
}
</Toolbar>
);
const defaultLayoutPluginInstance = defaultLayoutPlugin({
renderToolbar,
sidebarTabs: defaultTabs => [defaultTabs[1]]
});
// constantly called
console.log('entered')
return (
<div
style={{
height: '100%',
}}
>
<Worker workerUrl="https://unpkg.com/pdfjs-dist#2.5.207/build/pdf.worker.min.js">
<Viewer
fileUrl={url}
defaultScale={SpecialZoomLevel.PageFit}
plugins={[
defaultLayoutPluginInstance
]}
/>
</Worker>
</div>
);
};
export default PDFViewer;
I can see in the console that PDFViewer is being constantly called. I am not sure what is causing this rerenders the whole time?
Isn't it make sense to re-render when you have a new fileUrl passed to PDFModal? The following sequence should be how the app is executed.
PDFModal, PDFViewer and other related components init
When a file is dragged into the PaperComponent context, the upper level component handles it and passing pdfURL as props
const PDFModal = (props) => {
const { ......., pdfURL } = props;
//...skipped code
return (
<StyledDialog
PaperComponent={PaperComponent}
>
//...skipped code
<PDFViewer
url={pdfURL}
/>
</StyledDialog>
);
};
PDFViewer updated because there is a new prop.
const PDFViewer = ({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
);
}
I agree what #LindaPaiste said, putting Toolbar maybe an option since it doesn't use the url props passed in. For the re-render problem, I suggest that useCallback can be used to wrap the whole PDFViewer component. Only update the component when the url has changed.
This link provide some insights on when to use useCallback which can be a reference.
const PDFViewer = useCallback(
({ url }) => {
//...skipped code
return (
//...skipped code
<Viewer
fileUrl={url}
/>
)
}, [url])