I am trying to build a simple single select upload component. On click of a button that has a hidden input field, I open the file dialog then I select a file. The placeholder changes to the file name and then a change and clear button. Everything works fine, but on click of clear the file dialog, I dont want it to open on clear. It should open when "Choose file to upload" and "Change" is clicked. Can someone help?.
I am using material UI for the same
Sandbox: https://codesandbox.io/s/react-hook-upload-oxqdp2?file=/src/Upload.tsx:0-1784
import * as React from "react";
import { Button } from "#material-ui/core";
import { useState } from "react";
interface UploaderProps {
fileType?: string | AcceptedFileType[];
}
enum AcceptedFileType {
Text = ".txt",
Gif = ".gif",
Jpeg = ".jpg",
Png = ".png",
Doc = ".doc",
Pdf = ".pdf",
AllImages = "image/*",
AllVideos = "video/*",
AllAudios = "audio/*"
}
export const Upload = (props: UploaderProps) => {
const { fileType } = props;
const acceptedFormats: string | AcceptedFileType[] =
typeof fileType === "string"
? fileType
: Array.isArray(fileType)
? fileType?.join(",")
: AcceptedFileType.Text;
const [selectedFiles, setSelectedFiles] = useState<File | undefined>(
undefined
);
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedFiles(event?.target?.files?.[0]);
};
const onUpload = () => {
console.log(selectedFiles);
};
return (
<>
<Button
variant="contained"
component="label"
style={{ textTransform: "none" }}
>
<input
hidden
type="file"
accept={acceptedFormats}
onChange={handleFileSelect}
/>
{!selectedFiles?.name && <span> Choose file to upload</span>}
{selectedFiles?.name && (
<>
<span style={{ float: "left" }}> {selectedFiles?.name}</span>
<span style={{ padding: "10px" }}> Change</span>
<span onClick={() => setSelectedFiles(undefined)}>Clear</span>
</>
)}
</Button>
<Button
color="primary"
disabled={!selectedFiles}
style={{ textTransform: "none" }}
onClick={onUpload}
>
Upload
</Button>
</>
);
};
You should prevent default behavior of event. It worked for me like this:
<span onClick={(e) => { e.preventDefault(); setSelectedFiles(undefined); }}>Clear</span>
I would use useRef hook to refer to the hidden input field, something like this for example:
import * as React from 'react';
import Button from '#mui/material/Button';
const AcceptedFileType = {
Text: '.txt',
Gif: '.gif',
Jpeg: '.jpg',
Png: '.png',
Doc: '.doc',
Pdf: '.pdf',
AllImages: 'image/*',
AllVideos: 'video/*',
AllAudios: 'audio/*',
};
export default function Upload({ fileType }) {
const fileRef = React.useRef();
const acceptedFormats =
typeof fileType === 'string'
? fileType
: Array.isArray(fileType)
? fileType?.join(',')
: AcceptedFileType.Text;
const [selectedFiles, setSelectedFiles] = React.useState();
const handleFileSelect = (event) => {
setSelectedFiles(event?.target?.files?.[0]);
};
const onUpload = () => {
console.log(selectedFiles);
};
const onClear = () => {
setSelectedFiles(undefined);
};
const onUpdate = (event) => {
if (event.target.textContent.trim().toLowerCase() === 'change') {
onClear();
fileRef.current.click();
return;
}
if (event.target.textContent.trim().toLowerCase() === 'clear') {
onClear();
return;
}
};
return (
<>
<input
ref={fileRef}
hidden
type="file"
accept={acceptedFormats}
onChange={handleFileSelect}
/>
{!selectedFiles?.name && (
<Button
variant="contained"
component="label"
style={{ textTransform: 'none' }}
onClick={() => fileRef.current?.click()}
>
Choose file to upload
</Button>
)}
{selectedFiles?.name && (
<Button
variant="contained"
component="label"
style={{ textTransform: 'none' }}
onClick={onUpdate}
>
<span style={{ float: 'left' }}> {selectedFiles?.name}</span>
<span style={{ padding: '10px' }}> Change</span>
<span>Clear</span>
</Button>
)}
<Button
color="primary"
disabled={!selectedFiles}
style={{ textTransform: 'none' }}
onClick={onUpload}
>
Upload
</Button>
</>
);
}
See working demo: https://stackblitz.com/edit/react-dmzlsq?file=demo.js
Related
I am facing the difficulty that I want to rewrite the component to use react hooks, yet suddenly the DropZone component stopped working. I need to change the Component to hooks due to the JWT authentication I use.
I have this (former) implementation of the component:
import React from "react";
import Dropzone from "react-dropzone";
import UploadFileService from "../../../services/upload-files.service";
import './FileUploadForm.css'
import {withTranslation} from "react-i18next";
import Button from "#mui/material/Button";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import Typography from "#mui/material/Typography";
import {Alert} from "#mui/material";
import AuthContext from "../../../context/AuthContext";
class FileUploadForm extends React.Component {
constructor(props) {
super(props);
this.upload = this.upload.bind(this);
this.removeFiles = this.removeFiles.bind(this);
this.onDrop = this.onDrop.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.state = {
selectedFiles: undefined,
progressInfos: [],
message: [],
fileInfos: [],
dropZoneVisible: true,
url: props.url,
multipleFiles: props.multipleFiles
};
}
onDrop(files) {
if (files.length > 0) {
this.setState({selectedFiles: files});
}
}
uploadFiles() {
const selectedFiles = this.state.selectedFiles;
let _progressInfos = [];
for (let i = 0; i < selectedFiles.length; i++) {
_progressInfos.push({percentage: 0, fileName: selectedFiles[i].name});
}
this.setState(
{
progressInfos: _progressInfos,
message: [],
},
() => {
for (let i = 0; i < selectedFiles.length; i++) {
this.upload(i, selectedFiles[i]);
}
}
);
}
upload(idx, file) {
let _progressInfos = [...this.state.progressInfos];
UploadFileService.upload(file, this.state.url, (event) => {
_progressInfos[idx].percentage = Math.round(
(100 * event.loaded) / event.total
);
this.setState({
_progressInfos,
});
})
.then((response) => {
this.setState((prev) => {
let nextMessage = [
...prev.message,
"Uploaded the file successfully: " + file.name,
];
return {
message: nextMessage,
dropZoneVisible: false
};
});
return UploadFileService.getFiles(this.state.url);
})
.then((files) => {
this.setState({
fileInfos: files.data,
});
})
.catch(() => {
_progressInfos[idx].percentage = 0;
this.setState((prev) => {
let nextMessage = [
...prev.message,
"Could not upload the file: " + file.name,
];
return {
progressInfos: _progressInfos,
message: nextMessage,
};
});
});
}
render() {
const {t} = this.props;
const {selectedFiles, progressInfos, message, fileInfos} = this.state;
return (
<div style={{marginTop: '1em'}}>
{progressInfos &&
progressInfos.map((progressInfo, index) => (
<div className="mb-2" key={index} style={{
textAlign: "center",
fontWeight: "bold"
}}>
<Typography component="span" variant="string" align="center">
{progressInfo.fileName}
</Typography>
<LinearProgressWithLabel variant="determinate" value={progressInfo.percentage}/>
</div>
))}
<Dropzone onDrop={this.onDrop} multiple={this.state.multipleFiles}>
{({getRootProps, getInputProps}) => (
<section>
<div {...getRootProps({className: "dropzone"})}>
<input {...getInputProps()} />
{selectedFiles &&
Array.isArray(selectedFiles) &&
selectedFiles.length ? (
<div className="selected-file">
{selectedFiles.length > 3
? `${selectedFiles.length} files`
: selectedFiles.map((file) => file.name).join(", ")}
</div>
) : (
t("label", "Drag and drop file here, or click to select file", {ns: "fileUploadForm"})
)}
</div>
<aside className="selected-file-wrapper"
style={{
display: "flex",
justifyContent: "space-evenly"
}}
>
<Button
className="btn btn-success"
disabled={!selectedFiles}
variant="contained"
onClick={this.uploadFiles}
>
{t("button", "Submit", {ns: "fileUploadForm"})}
</Button>
<Button
className="btn"
color="error"
disabled={!selectedFiles}
variant="contained"
onClick={this.removeFiles}
>
{/*{t("button", "Submit", {ns: "fileUploadForm"})}*/}
Beheben
</Button>
</aside>
</section>
)}
</Dropzone>
{message.length > 0 && (
<div style={{
marginTop: "1em"
}}>
{message.map((item, i) => {
return (
<Alert severity={
item.includes("success") ? "success" : "warning"}>
{item}
</Alert>);
})}
</div>
)}
{fileInfos.length > 0 && (
<div className="card">
<div className="card-header">List of Files</div>
<ul className="list-group list-group-flush">
{fileInfos && fileInfos.map((file, index) => (
<li className="list-group-item" key={index}>
<a href={file.url}>{file.name}</a>
</li>
))}
</ul>
</div>
)}
</div>
);
}
}
export default withTranslation()(FileUploadForm);
This component works perfectly, meaning that the selectedFiles get propagated so the users can see what they submit. However, when changed to the functional implementation using hooks:
import React, {useCallback, useEffect, useRef, useState} from "react";
import Dropzone from "react-dropzone";
import './FileUploadForm.css'
import {useTranslation, withTranslation} from "react-i18next";
import Button from "#mui/material/Button";
import LinearProgressWithLabel from "./LinearProgressWithLabel";
import Typography from "#mui/material/Typography";
import {Alert} from "#mui/material";
import useAxios from "../../../utils/useAxios";
function FileUploadForm(url, multipleFiles, handleUploadReload) {
const api = useAxios();
const {t, i18n} = useTranslation()
const {selectedFiles, setSelectedFiles} = useState(undefined);
const {progressInfos, setProgressInfos} = useState({val: []});
const {message, setMessage} = useState([]);
const {targetUrl, setTargetUrl} = useState(url);
const {multipleFilesEnabled, setMultipleFilesEnabled} = useState(multipleFiles);
const {formUploaded, setFormUploaded} = useState(false);
const progressInfosRef = useRef(null)
const onDrop = (files) => {
if (files && files.length > 0) {
setSelectedFiles(files);
setProgressInfos({ val: [] });
}
}
const resetFormState = () => {
setFormUploaded(false);
setSelectedFiles(undefined);
setProgressInfos([]);
setMessage([]);
}
const removeFiles = () => {
setSelectedFiles(undefined);
setProgressInfos([]);
setMessage([]);
}
const uploadFiles = () => {
const files = Array.from(selectedFiles);
let _progressInfos = files.map(file => ({percentage: 0, fileName: file.name}));
progressInfosRef.current = {
val: _progressInfos,
}
files.map((file, i) => upload(i, file));
setMessage([]);
};
const upload = (idx, file) => {
let _progressInfos = [...progressInfosRef.current.val];
let formData = new FormData();
formData.append("file", file)
const onUploadProgress = (event) => {
_progressInfos[idx].percentage = Math.round(
(100 * event.loaded) / event.total
);
setProgressInfos({val: _progressInfos});
}
return api.post(targetUrl, formData, {
headers: {
"Content-Type": "multipart/form-data",
}, onUploadProgress
}).then((response) => {
handleUploadReload(response.data["files"]);
setMessage((prev) => ([
...prev.message,
"Uploaded the file successfully: " + file.name,
]
));
setFormUploaded(true);
})
.catch(() => {
_progressInfos[idx].percentage = 0;
setProgressInfos({val: _progressInfos});
setMessage((prev) => ([
...prev.message,
"Could not upload the file: " + file.name,
]));
});
}
return (
<div style={{marginTop: '1em'}}>
{progressInfos &&
progressInfos.val.length > 0 &&
progressInfos.val.map((progressInfo, index) => (
<div className="mb-2" key={index} style={{
textAlign: "center",
fontWeight: "bold"
}}>
<Typography component="span" variant="string" align="center">
{progressInfo.fileName}
</Typography>
<LinearProgressWithLabel variant="determinate" value={progressInfo.percentage}/>
</div>
))}
<Dropzone onDrop={onDrop} multiple={multipleFilesEnabled}>
{({getRootProps, getInputProps}) => (
<section>
<div {...getRootProps({className: "dropzone"})}>
<input {...getInputProps()} />
{selectedFiles &&
Array.isArray(selectedFiles) &&
selectedFiles.length ? (
<div className="selected-file">
{selectedFiles.length > 3
? `${selectedFiles.length} files`
: selectedFiles.map((file) => file.name).join(", ")}
</div>
) : (
t("label", "Drag and drop file here, or click to select file", {ns: "fileUploadForm"})
)}
</div>
<aside className="selected-file-wrapper"
style={{
display: "flex",
justifyContent: "space-evenly"
}}
>
{!formUploaded ? [
<Button
className="btn btn-success"
disabled={!selectedFiles}
variant="contained"
onClick={uploadFiles}
>
{t("button", "Submit", {ns: "fileUploadForm"})}
</Button>,
<Button
className="btn"
color="error"
disabled={!selectedFiles}
variant="contained"
onClick={removeFiles}
>
{/* TODO: {t("button", "Submit", {ns: "fileUploadForm"})}*/}
Beheben
</Button>] : (
<Button
className="btn"
color="error"
variant="contained"
onClick={resetFormState}
>
{/*TODO: {t("button", "Submit", {ns: "fileUploadForm"})}*/}
Neu Hochladen
</Button>
)}
</aside>
</section>
)}
</Dropzone>
{message && message.length > 0 && (
<div style={{
marginTop: "1em"
}}>
{message.map((item, i) => {
return (
<Alert severity={
item.includes("success") ? "success" : "warning"}>
{item}
</Alert>);
})}
</div>
)}
</div>
);
}
export default withTranslation()(FileUploadForm);
where the utilization of useAxios is essential. Nevertheless, users cannot see what they uploaded (preload view before push request). Any clue what could be wrong?
TLDR
DropZone shows uploaded files in the browser (before post request) when FileUploadForm is class and does not in case of a function.
So I have built a movie search app.
On the 4th page we have the ability to search for a specific movie or TV show.
Now I have built a logic that will display "Movies(Tv Shows) not found" when there are no search results.
Here is the code of the entire "Search" Component:
const Search = () => {
const [type, setType] = useState(0);
const [page, setPage] = useState(1);
const [searchText, setSearchText] = useState("");
const [content, setContent] = useState([]);
const [numOfPages, setNumOfPages] = useState();
const [noSearchResults, setNoSearchResults] = useState(false);
const fetchSearch = async () => {
try {
const { data } = await axios.get(`https://api.themoviedb.org/3/search/${type ? "tv" : "movie"}?api_key=${process.env.REACT_APP_API_KEY}&language=en-US&query=${searchText}&page=${page}&include_adult=false`);
setContent(data.results);
setNumOfPages(data.total_pages);
} catch (error) {
console.error(error);
}
};
const buttonClick = () => {
fetchSearch().then(() => {
if (searchText && content.length < 1) {
setNoSearchResults(true);
} else {
setNoSearchResults(false);
}
});
};
useEffect(() => {
window.scroll(0, 0);
fetchSearch();
// eslint-disable-next-line
}, [page, type]);
return (
<div>
<div style={{ display: "flex", margin: "25px 0" }}>
<TextField className="textBox" label="Search" variant="outlined" style={{ flex: 1 }} color="secondary" onChange={e => setSearchText(e.target.value)} />
<Button variant="contained" style={{ marginLeft: "10px" }} size="large" onClick={buttonClick}>
<SearchIcon color="secondary" fontSize="large" />
</Button>
</div>
<Tabs
value={type}
indicatorColor="secondary"
onChange={(event, newValue) => {
setPage(1);
setType(newValue);
}}
style={{
marginBottom: "20px",
}}
>
<Tab style={{ width: "50%" }} label="Search Movies" />
<Tab style={{ width: "50%" }} label="Search TV Shows" />
</Tabs>
<div className="trending">
{content && content.map(c => <SingleContent key={c.id} id={c.id} poster={c.poster_path} title={c.title || c.name} date={c.first_air_date || c.release_date} media_type={type ? "tv" : "movie"} vote_average={c.vote_average} />)}
{noSearchResults && (type ? <h2>Tv Shows not found</h2> : <h2>Movies not found</h2>)}
</div>
{numOfPages > 1 && <CustomPagination setpage={setPage} numOfPages={numOfPages} />}
</div>
);
};
You can see this in action here.
The problem that happens is that even when I have something in my search results, it still shows the Movies(Tv Shows) not found message.
And then if you click the search button again it will disappear.
A similar thing happens when there are no search results.
Then the Movies(Tv Shows) not found message will not appear the first time, only when you press search again.
I don't understand what is going on. I have used .then after my async function and still it does not execute in that order.
Try adding noSearchResults to your useEffect hook. That hook is what tells React when to re-render, and right now it's essentially not listening to noSearchResult whenever it changes because it's not included in the array.
I am using URL.createObjectURL to create image and pass it to an other component. In my NotificationStepper URL.createObjectURL is working just fine but in another Createwebsite component is i am getting this error TypeError: Failed to execute 'createObjectURL' on 'URL': No function was found that matched the signature provided..
following is my NotificationStepper component.
const styles = {
transparentBar: {
backgroundColor: 'transparent !important',
boxShadow: 'none',
paddingTop: '25px',
color: '#FFFFFF'
},
dialogPaper: {
minHeight: '400px',
maxHeight: '400px',
},
};
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const useStyles = makeStyles((theme) =>
createStyles({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}),
);
function getSteps() {
return ['Create', 'Audience', 'Timing'];
}
function getStepContent(step, $this) {
return (
<div className="row">
<CardBox styleName="col-lg-12"
heading="">
<form className="row" noValidate autoComplete="off" style={{ "flex-wrap": "no-wrap", "flex-direction": "column" }}>
<div className="col-md-12 col-12" style={{ "margin-bottom": "15px", "display": "flex", "align-items": "center" }}>
<Tooltip title="Use featured image of the landing page here. Works on Chrome, Opera, Edge. Recommended aspect ratio 2:1. Jpg, Jpeg, PNG file types only.">
<IconButton aria-label="">
<HelpOutlineIcon />
</IconButton>
</Tooltip>
<span style={{ "width": "20%", "margin-right": "20px" }}>Banner: </span>
<div className="inputfile">
Upload
<input className='hide_file' type="file" onChange={$this.onBannerChange} />
</div>
{$this.state.banner ?
<span>{$this.state.icon.banner}</span>
: null
}
</div>
</form>
</div >
);
}
class NotificationStepper extends React.Component {
state = {
banner: '',
crop: { x: 0, y: 0 },
zoom: 1,
aspect: 1,
croppedAreaPixels: null,
croppedImage: null,
showDialog: false,
};
onBannerChange = event => {
this.setState({ banner: event.target.files[0] });
this.setState({showDialog: true})
};
onIconChange = event => {
this.setState({ icon: event.target.files[0] });
this.setState({showDialogIcon: true})
};
onCropChange = crop => {
this.setState({ crop })
};
onCropComplete = (croppedArea, croppedAreaPixels) => {
this.setState({croppedAreaPixels: croppedAreaPixels});
};
CropImageBanner = async ()=> {
const croppedImage = await getCroppedImg(
URL.createObjectURL(this.state.banner),
this.state.croppedAreaPixels,
0
);
this.setState({ banner: croppedImage });
this.closeDialog();
};
closeDialog = () => {
this.setState({showDialog: false});
}
onZoomChange = zoom => {
this.setState({ zoom })
};
render() {
const steps = getSteps();
const { activeStep } = this.state;
if (this.props.showSuccessMessage) {
console.log('dispatch');
setTimeout(() => {
this.props.hideMessage();
}, 100);
}
if (this.props.showMessage) {
console.log('dispatch');
setTimeout(() => {
this.props.hideCompaignAlert();
}, 100);
}
const { classes } = this.props;
return (
<div>
{this.state.banner ?
// <span>{$this.state.banner.name}</span>
<Dialog
classes={{paper: classes.dialogPaper }}
fullWidth={true}
maxWidth='sm'
open={this.state.showDialog}
onClose={this.closeDialog}
>
<DialogTitle>
</DialogTitle>
<DialogContent>
<div className="App-crop" >
<div className="crop-container-div">
<Cropper
image={URL.createObjectURL(this.state.banner)}
crop={this.state.crop}
zoom={this.state.zoom}
aspect={this.state.aspect}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
/>
</div>
<div className="controls">
<Slider
value={this.state.zoom}
min={1}
max={3}
step={0.1}
aria-labelledby="Zoom"
onChange={(e, zoom) => this.onZoomChange(zoom)}
/>
</div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={() => this.closeDialog()} color="secondary">
Cancel
</Button>
<Button onClick={() =>
this.CropImageBanner()
} color="primary">
Crop
</Button>
</DialogActions>
</Dialog>
:
null
}
<Stepper className="MuiPaper-root-custom" activeStep={activeStep} orientation="vertical">
{steps.map((label, index) => {
return (
<Step key={label}>
<StepLabel>{label}</StepLabel>
<StepContent className="pb-3">
<Typography>{getStepContent(index, this)}</Typography>
<div className="mt-2">
<div>
<Button
disabled={activeStep === 0}
onClick={this.handleBack}
className="jr-btn"
>
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={this.handleNext}
className="jr-btn"
>
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
</StepContent>
</Step>
);
})}
</Stepper>
{activeStep === steps.length && (
// <Paper square elevation={0} className="p-2">
/*<Typography>All steps completed - you"re finished</Typography>*/
<div>
<Button onClick={this.sendNotification} disabled={!this.isFormValid} color="primary" style={{ "align-self": "flex-end", "border": "1px solid", "margin-left": "10px", "margin-bottom": "40px" }} size="small" className="col-md-2 col-2">Create</Button>
<Button onClick={this.handleReset} color="primary" style={{ "align-self": "flex-end", "border": "1px solid", "margin-left": "10px", "margin-bottom": "40px" }} size="small" className="col-md-2 col-2" color="primary">Reset</Button>
)}
{this.props.showMessage && NotificationManager.error(this.props.alertMessage) ?
<NotificationContainer /> : null
}
{this.props.showSuccessMessage && NotificationManager.success(this.props.successMessage)}
<NotificationContainer />
</div>
);
}
}
const mapDispatchToProps = dispatch => ({
showCompaignAlert: (message) => dispatch(showCompaignAlert(message)),
CreateCampaign: (data) => dispatch(CreateCampaign(data)),
hideMessage: () => dispatch(hideMessage()),
hideCompaignAlert: () => dispatch(hideCompaignAlert()),
showCampaignLoader: () => dispatch(showCampaignLoader())
});
const mapStateToProps = state => ({
loader: state.Campaigns.loader,
allWebsites: state.Websites.allWebsites,
alertMessage: state.Campaigns.alertMessage,
showMessage: state.Campaigns.showMessage,
successMessage: state.Campaigns.successMessage,
showSuccessMessage: state.Campaigns.showSuccessMessage,
});
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles) (NotificationStepper)));
and following is my CreateWebsite
import React from "react";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import Slide from "#material-ui/core/Slide";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogContent from "#material-ui/core/DialogContent";
import DialogActions from "#material-ui/core/DialogActions";
import FormControl from "#material-ui/core/FormControl";
import TextField from "#material-ui/core/TextField";
import {connect} from "react-redux";
import {createWebsite, hideSuccessMessage, hideAlertCreateWebsite} from "../../../actions/Websites";
import {NotificationContainer, NotificationManager} from "react-notifications";
import InputAdornment from "#material-ui/core/InputAdornment";
import IconButton from "#material-ui/core/IconButton";
import getCroppedImg from "../Send Notification/cropImage";
import Cropper from "react-easy-crop";
import Slider from "#material-ui/core/Slider";
import {withStyles} from "#material-ui/core/styles";
const styles = {
transparentBar: {
backgroundColor: 'transparent !important',
boxShadow: 'none',
paddingTop: '25px',
color: '#FFFFFF'
},
dialogPaper: {
minHeight: '400px',
maxHeight: '400px',
},
};
class CreateWebsiteComponent extends React.Component {
state = {
title: '',
url: '',
logo: '',
showDialog: false,
crop: { x: 0, y: 0 },
zoom: 1,
aspect: 1,
croppedAreaPixels: null,
croppedImage: null,
};
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
onFileChange = event => {
this.setState({ logo: event.target.files[0] });
this.setState({ showDialog: true})
};
createWebsite = (title, url, logo) => {
let temp = 'https://' + url;
this.props.createWebsite({title, url , logo})
};
onCropChange = crop => {
this.setState({ crop })
};
onCropComplete = (croppedArea, croppedAreaPixels) => {
this.setState({croppedAreaPixels: croppedAreaPixels});
};
closeDialog = () => {
this.setState({showDialog: false});
};
onZoomChange = zoom => {
this.setState({ zoom })
};
CropImageBanner = async ()=> {
const croppedImage = await getCroppedImg(
URL.createObjectURL(this.state.logo),
this.state.croppedAreaPixels,
0
);
this.setState({ logo: croppedImage });
this.closeDialog();
};
render() {
if (this.props.showSuccessMessage) {
setTimeout(() => {
this.props.hideSuccessMessage();
}, 100);
}
if (this.props.showAlertCreateWebsite_) {
setTimeout(() => {
this.props.hideAlertCreateWebsite();
}, 100);
}
const { title, url, logo} = this.state;
const openDialog = this.props.openDialog;
const { classes } = this.props;
return (
<div>
<Dialog
classes={{paper: classes.dialogPaper }}
fullWidth={true}
maxWidth='md'
open={this.state.showDialog}
onClose={this.closeDialog}
>
<DialogTitle>
</DialogTitle>
<DialogContent>
<div className="App-crop" >
<div className="crop-container-div">
<Cropper
image={this.state.log}
crop={this.state.crop}
zoom={this.state.zoom}
aspect={this.state.aspect}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
/>
</div>
<div className="controls">
<Slider
value={this.state.zoom}
min={1}
max={3}
step={0.1}
aria-labelledby="Zoom"
onChange={(e, zoom) => this.onZoomChange(zoom)}
/>
</div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={() => this.closeDialog()} color="secondary">
Cancel
</Button>
<Button onClick={() =>
this.CropImageBanner()
} color="primary">
Crop
</Button>
</DialogActions>
</Dialog>
<Dialog
open={openDialog}
onClose={this.props.closeDialog}
>
<DialogTitle>
{"Create Website"}
</DialogTitle>
<DialogContent>
<FormControl className="w-100">
<TextField
style={{'margin-right': "10px"}}
className="col-md-11 col-11"
id="name"
label="Title"
value={this.state.title}
onChange={this.handleChange('title')}
margin="normal"
fullWidth
/>
<TextField
style={{'margin-right': "10px"}}
className="col-md-11 col-11"
id="name"
label="URL"
value={this.state.url}
onChange={this.handleChange('url')}
margin="normal"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment className='positionEnd' position="start">
https://
</InputAdornment>
),
}}
/>
<div style={{"margin-top": "20px"}} className='col-md-11 col-sm-12 col-12 flex-class-custom'>
<span style={{"margin-right": "20px"}}>Icon: </span>
<div className="inputfile">
Upload
<input className='hide_file' style={{"width": "80%"}} type="file" onChange={this.onFileChange} />
</div>
{this.state.logo ?
<span>{this.state.logo.name}</span>
: null
}
</div>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={this.props.closeDialog} color="secondary">
Cancel
</Button>
<Button onClick={() => this.createWebsite(title, url , logo)
} color="primary">
Create
</Button>
</DialogActions>
</Dialog>
{this.props.showAlertCreateWebsite_ && NotificationManager.error(this.props.alertMessage)}
<NotificationContainer/>
{this.props.showSuccessMessage && NotificationManager.success(this.props.successMessage)}
<NotificationContainer/>
</div>
)
}
}
const mapDispatchToProps = dispatch => ({
createWebsite: (payload) => dispatch(createWebsite(payload)),
hideSuccessMessage: () => dispatch(hideSuccessMessage()),
hideAlertCreateWebsite: () => dispatch(hideAlertCreateWebsite())
});
const mapStateToProps = state => ({
loader : state.Websites.loader ,
alertMessage : state.Websites.alertMessage ,
showAlertCreateWebsite_ : state.Websites.showAlertCreateWebsite_,
showSuccessMessage : state.Websites.showSuccessMessage,
successMessage : state.Websites.successMessage
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(CreateWebsiteComponent));
// export default CreateWebsiteComponent;
I'm trying to use S3 service to upload an image and it's telling me that certain variables aren't defined when I have defined them. I have imported axios and all the other stuff that are required
import React, { useState } from "react";
import ReactDOM from "react-dom";
import axios from "axios";
import Grid from "#material-ui/core/Grid";
import Button from "#material-ui/core/Button";
import Card from "#material-ui/core/Card";
import TextField from "#material-ui/core/TextField";
import CreateIcon from "#material-ui/icons/Create";
import Box from "#material-ui/core/Box";
import CardMedia from "#material-ui/core/CardMedia";
import MuiAlert from "#material-ui/lab/Alert";
import Snackbar from "#material-ui/core/Snackbar";
import { withStyles } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
import Chip from "#material-ui/core/Chip";
import Avatar from "#material-ui/core/Avatar";
import Slider from "#material-ui/core/Slider";
import Typography from "#material-ui/core/Typography";
import InputAdornment from "#material-ui/core/InputAdornment";
import { connect } from "react-redux";
function mapStateToProps(state) {
return {};
}
const useStyles = makeStyles((theme) => ({
root: {
"& > *": {
margin: theme.spacing(1),
marginLeft: theme.spacing(15),
},
},
input: {
display: "none",
},
}));
const useSliderStyles = makeStyles({
root: {
width: 250,
},
input: {
width: 100,
},
});
const UploadButton = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<input
accept='image/*'
className={classes.input}
id='contained-button-file'
multiple
type='file'
/>
<label htmlFor='contained-button-file'>
<Button variant='contained' color='primary' component='span'>
Upload
</Button>
</label>
</div>
);
};
const StyledCard = withStyles({
root: { height: 600, width: 350 },
})(Card);
const PetitionForm = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [open, setOpen] = useState(false);
const [petition, validPetition] = useState(false);
const [noTitle, titleError] = useState(false);
const [noDescription, descriptionError] = useState(false);
const [hashtag, setHashtag] = useState("");
const [arrayOfHashtags, addHashtag] = useState([]);
const [money, setMoney] = React.useState(500);
const slider = useSliderStyles();
const handleTitleChange = (event) => setTitle(event.target.value);
const handleDescriptionChange = (event) => setDescription(event.target.value);
const handleClose = (event, reason) => {
if (reason === "clickaway") {
return;
}
};
const Alert = (props) => (
<MuiAlert elevation={6} variant='filled' {...props} />
);
const clearField = (event) => {
setOpen(true);
if (title.length > 0 && description.length > 0) {
validPetition(true);
setTitle("");
setDescription("");
addHashtag([]);
setHashtag("");
axios({
url: `call/s3/backend`,
method: "post",
data: {
images: imageArray.toByteArray(),
},
})
.then((res) => {
imageUrlArr = res.data;
axios({
url: `api/petition_posts`,
method: "post",
data: {
petition_post: {
title: title,
description: description,
hashtags: arrayOfHashtags.join(" "),
amount_donated: 0,
media: imageUrlArr,
goal: money,
card_type: "petition",
org_profile_id: 1,
},
},
})
.then((res) => {
console.log(res.data);
})
.catch((error) => console.log(error));
})
.catch((error) => console.log(error));
}
titleError(true ? title.length === 0 : false);
descriptionError(true ? description.length === 0 : false);
};
const handleDelete = (h) => () => {
addHashtag((arrayOfHashtags) =>
arrayOfHashtags.filter((hashtag) => hashtag !== h)
);
};
const handleHashtagChange = (event) => setHashtag(event.target.value);
const handleSliderChange = (event, newValue) => {
setMoney(newValue);
};
const handleInputChange = (event) => {
setMoney(event.target.value === "" ? "" : Number(event.target.value));
};
const newHashtag = () => {
if (arrayOfHashtags.length < 3) {
addHashtag((arrayOfHashtags) => arrayOfHashtags.concat(hashtag));
} else {
console.log("Too many hashtags");
}
};
const Hashtags = arrayOfHashtags.map((h) => (
<Chip
key={h.length}
size='small'
avatar={<Avatar>#</Avatar>}
label={h}
onDelete={handleDelete(h)}
/>
));
return (
<StyledCard>
<Box mt={1}>
<Grid container justify='center'>
<TextField
id='outlined-multiline-static'
multiline
rows={1}
variant='outlined'
placeholder='Title'
value={title}
onChange={handleTitleChange}
helperText={
open // only displays helper text if button has been clicked and fields haven't been filled
? !noTitle || petition
? ""
: "Can't be an empty field"
: ""
}
/>
</Grid>
</Box>
<Box mt={1}>
<Grid container justify='center'>
<CardMedia title='Petition'>
<UploadButton />
</CardMedia>
</Grid>
</Box>
<div className={slider.root}>
<Typography>Amount to raise</Typography>
<Box>
<Grid container justify='center'>
<Slider
min={500}
max={10000}
value={typeof money === "number" ? money : 0}
onChange={handleSliderChange}
aria-labelledby='input-slider'
/>
<TextField
className={slider.input}
value={money}
onChange={handleInputChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>$</InputAdornment>
),
}}
helperText={
money < 500 || money > 10000
? "Please enter a value between 500 and 10000"
: ""
}
/>
</Grid>
</Box>
</div>
<Box mt={1} mb={1}>
<Grid container justify='center'>
<TextField
size='small'
inputProps={{
style: { fontSize: 15 },
}}
id='outlined-multiline-static'
multiline
rows={1}
placeholder='Hashtags'
variant='outlined'
value={hashtag}
onChange={handleHashtagChange}
helperText={
arrayOfHashtags.length === 3
? "You reached the maximum amount of hashtags"
: ""
}
/>
<Button color='primary' onClick={newHashtag}>
Create!
</Button>
{arrayOfHashtags.length > 0 ? Hashtags : ""}
</Grid>
</Box>
<Box mt={1} justify='center'>
<Grid container justify='center'>
<TextField
size='small'
inputProps={{
style: { fontSize: 15 },
}}
id='outlined-multiline-static'
multiline
rows={5}
placeholder='Description'
variant='outlined'
value={description}
onChange={handleDescriptionChange}
helperText={
// only displays helper text if button has been clicked and fields haven't been filled
open
? !noDescription || petition
? ""
: "Can't be an empty field"
: ""
}
/>
</Grid>
</Box>
<Box mt={1}>
<Grid container justify='center'>
<Button onClick={clearField}>
<CreateIcon />
Create Petition!
</Button>
{open && petition && (
<Snackbar open={open} autoHideDuration={2000} onClose={handleClose}>
<Alert severity='success'>
You have successfully create a petition!
</Alert>
</Snackbar>
)}
{open && !petition && (
<Snackbar open={open} autoHideDuration={2000} onClose={handleClose}>
<Alert severity='error'>You're missing one or more fields</Alert>
</Snackbar>
)}
</Grid>
</Box>
</StyledCard>
);
};
export default connect(mapStateToProps)(PetitionForm);
This is the error I'm getting, someone mentioned something about the scope and I think that shouldn't matter when I'm trying to assign a value to a variable as far I as know
Line 109:19: 'imageArray' is not defined no-undef
Line 113:11: 'imageUrlArr' is not defined no-undef
Line 123:24: 'imageUrlArr' is not defined no-undef
Search for the keywords to learn more about each error.
I have 5 buttons in my app which I would like to change the background color based on button states, so now when I click one button it affects all the buttons (toggle-class), not only that I need to change the button color but also I need to hide and show data for each button, so I am using condition rendering, the default tab is social media. so, for example, u click button 1 it changes the background color and it shows div withe information, etc
Here is what I have so far
import React, { useState, useEffect, useRef } from 'react';
function Mata() {
const [isBlack, setIsBlack] = useState(0);
const [tab, setTab] = useState('socialmedia');
const handleBtn1 = (e) =>{
e.preventDefault();
setIsBlack(!isBlack);
setTab('data1);
}
const handleBtn2 = (e) =>{
e.preventDefault();
setIsBlack(!isBlack);
setTab('data2');
}
const handleBtn3 = (e) =>{
e.preventDefault();
setIsBlack(!isBlack);
setTab('data3');
}
const handleBtn4 = (e) =>{
e.preventDefault();
setIsBlack(!isBlack);
setTab('data4');
}
const handleBtn5 = (e) =>{
e.preventDefault();
setIsBlack(!isBlack);
setTab('data5');
}
return (
<div className="container">
<button style={{ backgroundColor: isBlack ? '#262626' : '#F3F3F3'}} className={`btn1 ${isBlack && activeTab}`} onClick={handleBtn1}>btn1</button>
<button style={{ backgroundColor: isBlack ? '#262626' : '#F3F3F3'}} className={`btn2 ${isBlack && activeTab}`} onClick={handleBtn2}>btn2</button>
<button style={{ backgroundColor: isBlack ? '#262626' : '#F3F3F3'}} className={`btn3 ${isBlack && activeTab}`} onClick={handleBtn3}>btn3</button>
<button style={{ backgroundColor: isBlack ? '#262626' : '#F3F3F3'}} className={`btn4 ${isBlack && activeTab}`} onClick={handleBtn4}>btn4</button>
<button style={{ backgroundColor: isBlack ? '#262626' : '#F3F3F3'}} className={`btn5 ${isBlack && activeTab}`} onClick={handleBtn5}>btn5</button>
{tab === 'socialmedia' && <>
....data
</div>
{tab === 'data1' && <>
....data
</div>
............
..........
</div>
)
}
export default Mata
What do I need to change to get this working?
You need individual state for each button. I suggest using a map to store a button id and a boolean value for whether it is "black" or not, i.e. the click handler simply toggles a boolean value. I don't know if it was a typo in copy/pasting code to SO, but the react state needs to be declared in in the functional component body.
const [isBlack, setIsBlack] = useState({});
You can also use a single click handler by converting it to a curried callback, taking and enclosing in scope the button id. This uses a functional state update to shallowly copy existing state and updates the value of the enclosed button id.
const handleBtn = btnId => e => {
e.preventDefault();
setIsBlack(state => ({
...state,
[btnId]: !state[btnId],
}));
};
Complete code
function Mata() {
const [activeTab, setActiveTab] = useState("activeTab");
const [isBlack, setIsBlack] = useState({});
const handleBtn = btnId => e => {
e.preventDefault();
setIsBlack(state => ({
...state,
[btnId]: !state[btnId]
}));
};
return (
<div className="container">
<button
style={{ backgroundColor: isBlack["btn1"] ? "#262626" : "#F3F3F3" }}
className={`btn1 ${isBlack["btn1"] && activeTab}`}
onClick={handleBtn("btn1")}
>
btn1
</button>
<button
style={{ backgroundColor: isBlack["btn2"] ? "#262626" : "#F3F3F3" }}
className={`btn2 ${isBlack["btn2"] && activeTab}`}
onClick={handleBtn("btn2")}
>
btn2
</button>
<button
style={{ backgroundColor: isBlack["btn3"] ? "#262626" : "#F3F3F3" }}
className={`btn3 ${isBlack["btn3"] && activeTab}`}
onClick={handleBtn("btn3")}
>
btn3
</button>
<button
style={{ backgroundColor: isBlack["btn4"] ? "#262626" : "#F3F3F3" }}
className={`btn4 ${isBlack["btn4"] && activeTab}`}
onClick={handleBtn("btn4")}
>
btn4
</button>
<button
style={{ backgroundColor: isBlack["btn5"] ? "#262626" : "#F3F3F3" }}
className={`btn5 ${isBlack["btn5"] && activeTab}`}
onClick={handleBtn("btn5")}
>
btn5
</button>
</div>
);
}
There is a lot of repeated code, so a more DRY version where active tab and buttons are passed as props.
function Mata({ activeTab = '', buttons }) {
const [isBlack, setIsBlack] = useState({});
const handleBtn = btnId => e => {
e.preventDefault();
setIsBlack(state => ({
...state,
[btnId]: !state[btnId]
}));
};
return (
<div className="container">
{buttons.map(btn => (
<button
style={{ backgroundColor: isBlack[btn] ? "#262626" : "#F3F3F3" }}
className={`btn1 ${isBlack[btn] && activeTab}`}
onClick={handleBtn(btn)}
>
{btn}
</button>
))}
</div>
);
}
Used as such
const buttons = ["btn1", "btn2", "btn3", "btn4", "btn5"];
...
<Mata buttons={buttons} />
Edit
Seems you are really creating a "tab manager". I suggest lofting state to the parent and converting Mata to a "dumb" component that simply renders the "tab" buttons. Takes 3 props: an active tab index, array of buttons, and a state update callback.
function Mata({ activeTab = -1, buttons, setActiveTab }) {
return (
<div className="container">
{buttons.map((btn, i) => {
const isActive = i === activeTab;
return (
<button
key={btn.id}
style={{ backgroundColor: isActive ? "#262626" : "#F3F3F3" }}
className={`${btn.id} ${isActive && activeTab}`}
onClick={() => setActiveTab(i)}
>
{btn.id}
</button>
);
})}
</div>
);
}
Example tabs data
const tabs = [
{ id: "btn1", data: "data1" },
{ id: "btn2", data: "data2" },
{ id: "btn3", data: "data3" },
{ id: "btn4", data: "data4" },
{ id: "btn5", data: "data5" }
];
Example usage
<Mata activeTab={activeTab} buttons={tabs} setActiveTab={setActiveTab} />
{activeTab === -1 ? (
<div>Social Media</div>
) : (
<div>{tabs[activeTab].data}</div>
)}
Adding "Icons"
Similar to Choosing the Type at Runtime
If SVG icons are not already react components, wrap them into a simple functional component
const Icon1 = () => <svg>...</svg>;
Add an icon field to the tabs data and set the value to the icon component
const tabs = [
{ id: "btn1", data: "data1", icon: Icon1 },
{ id: "btn2", data: "data2", icon: Icon2 },
{ id: "btn3", data: "data3", icon: Icon3 },
{ id: "btn4", data: "data4", icon: Icon4 },
{ id: "btn5", data: "data5", icon: Icon5 }
];
And destructure and rename to render
function Mata({ activeTab = -1, buttons, setActiveTab }) {
return (
<div className="container">
{buttons.map((btn, i) => {
const isActive = i === activeTab;
const { icon: Icon, id } = btn; // <-- rename icon -> Icon
return (
<button
key={id}
style={{ backgroundColor: isActive ? "#262626" : "#F3F3F3" }}
className={`${id} ${isActive && activeTab}`}
onClick={() => setActiveTab(i)}
>
<Icon /> {id} // <-- render icon component
</button>
);
})}
</div>
);
}
Why are you doing this
const [isBlack, setIsBlack] = useState(0);
instead of doing this ?
const [isBlack, setIsBlack] = useState(false);
Also to make use of useState you have to edit your code like the following, as hooks can only be called inside of the body of a function component.
import React, { useState, useEffect, useRef } from "react";
function Mata() {
const [isBlack, setIsBlack] = useState(false); // correction here
const handleBtn1 = e => {
e.preventDefault();
setIsBlack(!isBlack);
};
const handleBtn2 = e => {
e.preventDefault();
setIsBlack(!isBlack);
};
const handleBtn3 = e => {
e.preventDefault();
setIsBlack(!isBlack);
};
const handleBtn4 = e => {
e.preventDefault();
setIsBlack(!isBlack);
};
const handleBtn5 = e => {
e.preventDefault();
setIsBlack(!isBlack);
};
return (
<div className="container">
<button
style={{ backgroundColor: isBlack ? "#262626" : "#F3F3F3" }}
className={`btn1 ${isBlack && activeTab}`}
onClick={handleBtn1}
>
btn1
</button>
<button
style={{ backgroundColor: isBlack ? "#262626" : "#F3F3F3" }}
className={`btn2 ${isBlack && activeTab}`}
onClick={handleBtn2}
>
btn2
</button>
<button
style={{ backgroundColor: isBlack ? "#262626" : "#F3F3F3" }}
className={`btn3 ${isBlack && activeTab}`}
onClick={handleBtn3}
>
btn3
</button>
<button
style={{ backgroundColor: isBlack ? "#262626" : "#F3F3F3" }}
className={`btn4 ${isBlack && activeTab}`}
onClick={handleBtn4}
>
btn4
</button>
<button
style={{ backgroundColor: isBlack ? "#262626" : "#F3F3F3" }}
className={`btn5 ${isBlack && activeTab}`}
onClick={handleBtn5}
>
btn5
</button>
</div>
);
}
export default Mata;