Reactjs app doesn't show image preview in safari - reactjs

I am trying to make an app where a user can upload an image and send it off to an email, it's working fine on all browsers except Safari. For both mobile and web browsers, when I choose an image to upload nothing seems to be previewed nor is it even loaded (ready to be sent). Is there anything I can do to fix this? My code as it stands is really simple:
const EnterDetailPage = props => {
const [imageUrl, setImageUrl] = useState("");
const [imageFile, setImageFile] = useState();
const [upload, setUpload] = useState(null);
const handleUploadChange = async e => {
setLoading(true);
const file = e.target.files[0];
if (!file) {
return;
}
setUpload(URL.createObjectURL(file));
setImageFile(file);
const ref = firebase
.storage()
.ref()
.child(uuid.v4());
const snapshot = await ref.put(file);
let getImageUrl = await snapshot.ref.getDownloadURL();
setImageUrl(getImageUrl);
setLoading(false);
console.log(getImageUrl);
};
let imgPreview = null;
if (upload) {
imgPreview = (
<Avatar
variant="square"
src={upload}
alt="Avatar"
className={classes.bigAvatar}
/>
);
}
return(
<div className="m-auto p-16 sm:px-24 sm:mx-auto max-w-xl">
<input
accept="image/jpeg,image/gif,image/png"
className="hidden"
id="button-file"
type="file"
// onChange={handleUploadChange}
onInput={handleUploadChange}
onClick={event => {
event.target.value = null;
}}
/>
<label
htmlFor="button-file"
className={`${classes.bigAvatar} mt-8 bg-gray-300 m-auto flex items-center justify-center relative w-128 h-128 rounded-4 a-mr-16 a-mb-16 overflow-hidden cursor-pointer shadow-1 hover:shadow-xl`}
>
<div className="absolute flex items-center justify-center w-full h-full z-50">
{imageUrl ? null :
<Icon fontSize="large" color="primary" className="cloud-icon">
cloud_upload
</Icon>}
</div>
{imgPreview}
</label>
);
}:
I compared my code to this article here: https://w3path.com/react-image-upload-or-file-upload-with-preview/
and it seems like I've done exactly the same thing...how come I'm not getting the same results?

There's quite a bit going on your codesandbox example, but by stripping it down its bare bones, I was able to track down the issue...
Safari doesn't seem to support input elements that try to use the onInput event listener -- the callback is never executed. Instead, you can use the onChange event listener.
For the example below, I faked an API call by setting a Promise with a timeout, but this not needed and is only for demonstration purposes. In addition, I like using objects over multiple individual states, especially when the state needs to be synchronous -- it also is cleaner, easier to read, and functions more like a class based component.
Demo: https://jd13t.csb.app/
Source:
components/DetailPage.js
import React, { useRef, useState } from "react";
import { CircularProgress, Icon, Fab } from "#material-ui/core";
const initialState = {
isLoading: false,
imageName: "",
imagePreview: null,
imageSize: 0
};
const EnterDetailPage = () => {
const [state, setState] = useState(initialState);
const uploadInputEl = useRef(null);
const handleUploadChange = async ({ target: { files } }) => {
setState(prevState => ({ ...prevState, isLoading: true }));
const file = files[0];
await new Promise(res => {
setTimeout(() => {
res(
setState(prevState => ({
...prevState,
imageName: file.name,
imagePreview: URL.createObjectURL(file),
imageSize: file.size,
isLoading: false
}))
);
}, 2000);
});
};
const resetUpload = () => {
setState(initialState);
uploadInputEl.current.value = null;
};
const uploadImage = async () => {
if (state.imagePreview)
setState(prevState => ({ ...prevState, isLoading: true }));
await new Promise(res => {
setTimeout(() => {
res(alert(JSON.stringify(state, null, 4)));
resetUpload();
}, 2000);
});
};
const { imagePreview, imageName, imageSize, isLoading } = state;
return (
<div style={{ padding: 20 }}>
<div style={{ textAlign: "center" }}>
<div>
<input
accept="image/jpeg,image/gif,image/png"
className="hidden"
id="button-file"
type="file"
ref={uploadInputEl}
onChange={handleUploadChange}
/>
<label htmlFor="button-file">
<div>
{imagePreview ? (
<>
<img
src={imagePreview}
alt="Avatar"
style={{ margin: "0 auto", maxHeight: 150 }}
/>
<p style={{ margin: "10px 0" }}>
({imageName} - {(imageSize / 1024000).toFixed(2)}MB)
</p>
</>
) : (
<Icon fontSize="large" color="primary" className="cloud-icon">
cloud_upload
</Icon>
)}
</div>
</label>
<Fab
variant="extended"
size="large"
color="primary"
aria-label="add"
className=""
type="button"
onClick={uploadImage}
>
{isLoading ? (
<CircularProgress style={{ color: "white" }} />
) : (
"Submit"
)}
</Fab>
{imagePreview && (
<Fab
variant="extended"
size="large"
color="default"
aria-label="add"
className=""
type="button"
onClick={resetUpload}
>
Cancel
</Fab>
)}
</div>
</div>
</div>
);
};
export default EnterDetailPage;

Related

useEffect doesn't re-render on state change or infinite looping issue

I have a component which contains a form and a list. When user adds an item to the list though the form, the item should display immediately in the list. I try to use useEffect to fetch data, useEffect without dependency causes an infinite request loop. I added empty array as dependency to prevent looping but in this case new item which is added doesn't display in the list until refreshing the page. How can I solve this issue? (I use antd and antd-form-builder to create the component)
here is my code:
function FieldSetting() {
const [form] = Form.useForm()
const [typeValue, setTypeValue] = useState()
const meta = {
fields: [{ key: "pathname", onChange: (e) => setTypeValue(e.target.value) }],
}
const [data, setData] = useState([])
async function onFinish() {
try {
await axios.post("api", { typeValue, typeId })
form.resetFields()
} catch (e) {
console.log(e)
}
}
useEffect(() => {
const getData = async () => {
const response = await fetch(`api?id=${typeId}`)
const newData = await response.json()
setData(newData)
}
getData()
}, [])
return (
<Container>
<Form form={form} layout="inline" className="form-field" onFinish={onFinish}>
<FormBuilder form={form} meta={meta} />
<Form.Item>
<Button type="primary" htmlType="submit">
Add
</Button>
</Form.Item>
</Form>
<div
id="scrollableDiv"
style={{
height: 665,
overflow: "auto",
padding: "0 16px",
border: "1px solid rgba(140, 140, 140, 0.35)",
}}
>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item
actions={[
<a key="list-edit">edit</a>,
<a onClick={() => axios.delete(`http://gage.axaneh.com/api/Gages/SettingProduct/RemoveProductSetting/${item.id}`, item)} key="list-delete">
delete
</a>,
]}
>
<List.Item.Meta title={item.typeValue} />
</List.Item>
)}
/>
</div>
</Container>
)
}
export default FieldSetting
Just add a state that will refretch (trigger useEffect) after you have submitted the form. Be aware that it will refetch all the data from the API. This might bring scalability issues when the data grows.
function FieldSetting() {
const [form] = Form.useForm()
const [refetch, setRefetch] = useState(false) // <----- add this state
const [typeValue, setTypeValue] = useState()
const meta = {
fields: [{ key: "pathname", onChange: (e) => setTypeValue(e.target.value) }],
}
const [data, setData] = useState([])
async function onFinish() {
try {
await axios.post("api", { typeValue, typeId })
form.resetFields()
setRefetch(!refetch) // <----- set the refetch to change the state
} catch (e) {
console.log(e)
}
}
useEffect(() => {
const getData = async () => {
const response = await fetch(`api?id=${typeId}`)
const newData = await response.json()
setData(newData)
}
getData()
}, [refetch]) // <----- add the refetch here to trigger the effect
return (
<Container>
<Form form={form} layout="inline" className="form-field" onFinish={onFinish}>
<FormBuilder form={form} meta={meta}
/>
<Form.Item>
<Button type="primary" htmlType="submit">
Add
</Button>
</Form.Item>
</Form>
<div
id="scrollableDiv"
style={{
height: 665,
overflow: "auto",
padding: "0 16px",
border: "1px solid rgba(140, 140, 140, 0.35)",
}}
>
<List
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (
<List.Item
actions={[
<a key="list-edit">edit</a>,
<a onClick={() => axios.delete(`http://gage.axaneh.com/api/Gages/SettingProduct/RemoveProductSetting/${item.id}`, item)} key="list-delete">
delete
</a>,
]}
>
<List.Item.Meta title={item.typeValue} />
</List.Item>
)}
/>
</div>
</Container>
)
}
export default FieldSetting```
Whenever you manipulate your array just add a dummy state and change it
add this state
const [extra, setExtra] = useState(0)
when you change the state of your array like add or remove just add this line below
setExtra(extra+1)
what happens is that adding or removing data in an array don't count as a state change in react as per my understanding it need to be something different like true to false or in this case 0 to 1

Create custom Search bar in react to search through Firebase document

I want to create a custom search bar to query my Firestore document retrieve collection based on user input.
I know there are better options to do this like Algolia, Typesense etc.
But I have issues with Firebase upgrading my account, and I have contacted the Firebase team.
DrinkSearch.tsx
const DrinkSearch: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [drinkSnap, setDrinkSnap] = useState<
QueryDocumentSnapshot<DocumentData>[]
>([]);
const [isLoading, setIsLoading] = useState(false);
const drinkRef = collection(firebaseFirestore, "products");
const drinkQuery = query(drinkRef, where("drinkName", "==", searchTerm));
const snapshots = getDocs(drinkQuery);
let docsIsEmpty!: boolean;
const getProductOnChange = () => {
setIsLoading(true);
snapshots
.then((docsSnapshot) => {
setIsLoading(false);
setDrinkSnap(docsSnapshot?.docs);
docsIsEmpty = docsSnapshot?.empty;
console.log(docsSnapshot?.docs);
})
.catch((error: FirestoreError) => {
setIsLoading(false);
console.log(error.message);
});
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.currentTarget.value);
getProductOnChange();
};
useEffect(() => {
console.log(searchTerm);
}, [searchTerm]);
return (
<Box>
<InputGroup size="lg">
<InputLeftElement pointerEvents="none">
<RiSearch2Line color="#CBD5E0" size="20px" />
</InputLeftElement>
<Input
onChange={handleChange}
type="text"
_focus={{
boxShadow: shadowSm,
}}
fontSize="14px"
placeholder="Search for drinks"
/>
</InputGroup>
<Box
padding={5}
bgColor="white"
height="40px"
borderBottomRadius={"8px"}
border={"1px solid #EDF2F7"}
>
{docsIsEmpty && <Text>Drink not found.</Text>}
{isLoading && (
<Flex height="100%">
<Spinner size={"sm"} colorScheme={"primary.500"} />
</Flex>
)}
{drinkSnap &&
drinkSnap?.map((drinkSnap) => {
const drinks = drinkSnap?.data();
return (
<HStack
cursor={"pointer"}
justify={"space-between"}
padding={"5px"}
_hover={{
bgColor: "#EDF2F7",
}}
key={drinkSnap?.id}
>
<Text fontWeight={"semibold"}>{drinks?.drinkName}</Text>
<Badge fontSize={"12px"}>{drinks?.category}</Badge>
</HStack>
);
})}
</Box>
</Box>
);
};
export default DrinkSearch;
Result: When I start typing for example black label is the name of a drink, nothing happens i.e the [] is empty. When I remove 'l'. it remains black labe, it returns the array with the collection.
What I want: On typing, return all collections that match what is typed.

React-webcam enable/disable flash

I'm using react JS to create a camera application
I want to be able to save a photo snapshot, change the facingMode, to be able to use the font and back camera of the phone. Also, I would like to enable and disable the flash when the facingMode is set to the environment.
I used react-webcam from npm to start a video stream and create a snapshot.
At this point, I can flip the camera (to use the front and back camera), but can I enable/disable the flash when the back camera is active?
In here https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API/Constraints i found a constant property called torch that is used for the mobile flash, but i don't know how to dynamycally set the vale to true/false
import React from 'react';
import Webcam from 'react-webcam';
import { useState, useRef } from 'react';
const WebcamComponent = () => <Webcam />;
const WebcamCapture = () => {
const webcamRef = React.useRef(null);
const [hasEnvCamera, setEvnCamera] = useState(false);
const [image, setImage] = useState('');
const [flash, setFlash] = useState(false);
const capture = React.useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
setImage(imageSrc);
}, [webcamRef]);
const videoConstraints = {
width: 220,
height: 200,
facingMode: 'user',
};
const videoFlipConstraints = {
width: 220,
height: 200,
facingMode: { exact: 'environment' },
};
const changeCamera = (e) => {
e.preventDefault();
if (hasEnvCamera) {
console.log(' back camera');
setEvnCamera(false);
} else {
console.log(' front camera');
setEvnCamera(true);
}
};
const changeFlash = (e) => {
e.preventDefault();
console.log('change flash');
if (flash) {
setFlash(false);
console.log('flash off');
} else {
setFlash(true);
console.log('flash on');
}
};
return (
<div>
{hasEnvCamera ? (
<div>
<p> camera flip</p>
<Webcam
audio={false}
height={200}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={220}
videoConstraints={videoFlipConstraints}
/>
<button onClick={changeFlash}>
{' '}
{flash ? 'Disable flesh' : 'Enable flash'}{' '}
</button>
</div>
) : (
<div>
<p>selfie camera</p>
<Webcam
audio={false}
height={200}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={220}
videoConstraints={videoConstraints}
/>
</div>
)}
<button
onClick={(e) => {
e.preventDefault();
capture();
}}
>
Photo
</button>
<button
onClick={(e) => {
e.preventDefault();
const imageSrc = null;
setImage(imageSrc);
}}
>
Take another photo{' '}
</button>
<button onClick={changeCamera}> Change camera </button>
<img src={image} />
</div>
);
};
export default WebcamCapture;

Why the wrong element is being updated only when uploading files?

I have built a component CreatePost which is used for creating or editing posts,
the problem is if I render this component twice even if I upload a file from the second component they are changed in the first one, why? Here is the code:
import FileUpload from "#components/form/FileUpload";
import { Attachment, Camera, Video, Writing } from "public/static/icons";
import styles from "#styles/components/Post/CreatePost.module.scss";
import { useSelector } from "react-redux";
import { useInput, useToggle } from "hooks";
import { useRef, useState } from "react";
import StyledButton from "#components/buttons/StyledButton";
import Modal from "#components/Modal";
import { post as postType } from "types/Post";
import Removeable from "#components/Removeable";
interface createPostProps {
submitHandler: (...args) => void;
post?: postType;
isEdit?: boolean;
}
const CreatePost: React.FC<createPostProps> = ({ submitHandler, post = null, isEdit = false }) => {
console.log(post);
const maxFiles = 10;
const [showModal, setShowModal, ref] = useToggle();
const [description, setDescription] = useInput(post?.description || "");
const user = useSelector((state) => state.user);
const [files, setFiles] = useState<any[]>(post?.files || []);
const handleFileUpload = (e) => {
const fileList = Array.from(e.target.files);
if (fileList.length > maxFiles || files.length + fileList.length > maxFiles) {
setShowModal(true);
} else {
const clonedFiles = [...files, ...fileList];
setFiles(clonedFiles);
}
e.target.value = "";
};
const removeHandler = (id) => {
const filtered = files.filter((file) => file.name !== id);
setFiles(filtered);
};
return (
<div className={styles.createPost}>
<div className={styles.top}>
<span>
<img src="/static/images/person1.jpg" />
</span>
<textarea
onChange={setDescription}
className="primaryScrollbar"
aria-multiline={true}
value={description}
placeholder={`What's on your mind ${user?.name?.split(" ")[0]}`}
></textarea>
{description || files.length ? (
<StyledButton
background="bgPrimary"
size="md"
className={styles.submitButton}
onClick={() => {
if (!isEdit)
submitHandler({
files: files,
author: { name: user.name, username: user.username },
postedTime: 52345,
id: Math.random() * Math.random() * 123456789101112,
comments: [],
likes: [],
description,
});
else {
submitHandler({
...post,
description,
files,
});
}
setDescription("");
setFiles([]);
}}
>
{isEdit ? "Edit" : "Post"}
</StyledButton>
) : null}
</div>
<div className={styles.middle}>
<div className={styles.row}>
{files.map((file) => {
return (
<Removeable
key={file.name + Math.random() * 100000}
removeHandler={() => {
removeHandler(file.name);
}}
>
{file.type.includes("image") ? (
<img src={URL.createObjectURL(file)} width={150} height={150} />
) : (
<video>
<source src={URL.createObjectURL(file)} type={file.type} />
</video>
)}
</Removeable>
);
})}
</div>
</div>
<div className={styles.bottom}>
<FileUpload
id="uploadPhoto"
label="upload photo"
icon={
<span>
<Camera /> Photo
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="image/*"
/>
<FileUpload
id="uploadVideo"
label="upload video"
icon={
<span>
<Video /> Video
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="video/*"
/>
<FileUpload
id="writeArticle"
label="write article"
icon={
<span>
<Writing /> Article
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
/>
</div>
{showModal && (
<Modal size="sm" backdrop="transparent" ref={ref} closeModal={setShowModal.bind(null, false)} yPosition="top">
<p>Please choose a maximum of {maxFiles} files</p>
<StyledButton size="md" background="bgPrimary" onClick={setShowModal.bind(null, false)}>
Ok
</StyledButton>
</Modal>
)}
</div>
);
};
export default CreatePost;
Now on my main file I have:
const Main = () => {
const [posts, setPosts] = useState<postType[]>([]);
const addPost = (post: postType) => {
setPosts([post, ...posts]);
};
const editPost = (post: postType) => {
const updated = posts.map((p) => {
if (post.id === post.id) {
p = post;
}
return p;
});
setPosts(updated);
};
const deletePost = (id) => {
const filtered = posts.filter((post) => post.id !== id);
setPosts(filtered);
};
return (
<>
<CreatePost submitHandler={addPost} key="0" />
<CreatePost submitHandler={addPost} key="1"/>
{posts.map((post) => {
return <PostItem {...post} editHandler={editPost} key={post.id} deleteHandler={deletePost.bind(null, post.id)} />;
})}
</>
);
};
export default Main;
I tried to add/remove the key but doesn't change anything, also tried to recreate this problem in a simpler way in sandbox but I can't it works fine there. And the problem is only when I upload files not when I write text inside the <textarea/>
Note: The second in reality is shown dynamically inside a modal when clicked edit in a post, but I just showed it here for simplicity because the same problem occurs in both cases.
Okay after some hours of debugging I finally found the problem.
Because my <FileUpload/> uses id to target the input inside the <CreatePost/> the <FileUpload/> always had same it, so when I used <CreatePost/> more than 1 time it would target the first element that found with that id that's why the first component was being updated

trying to delete from an API using axios and React hooks

Hello im trying to delete a Booking from an api using an input with the ID . obviously something is wrong. I tried to convert my class to a HOC and i just cant get it to work. Right now i cant even type in the textbox .
I know i have severals errors but i dont know how to solve them would appreciate some help. the only relevant parts in the HTML is the form.
const DeleteBooking = () => {
const [ModalIsOpen, SetModalIsOpen] = useState(false); // set false if closen when open
const [booking, setDelete] = useState([]);
const handleChange = (e) => {
setDelete({ [e.target.name]: e.target.value });
};
useEffect((UserIdInput) => {
const bookingId = UserIdInput.target.elements.bookingId.value;
Axios.delete(`https://localhost:44366/api/Products/${bookingId}`) // change api key
.then((response) => {
console.log(response);
setDelete(response.data);
});
}, []);
return (
<>
<div className="App-header">
<button onClick={() => SetModalIsOpen(true)}>Radera bokning</button>
</div>
<Modal
isOpen={ModalIsOpen}
onRequestClose={() => SetModalIsOpen(false)}
style={{
overlay: {
background:
"linear-gradient(-500deg, #ee7752, #6e1b3b, #0c495f, #000000)",
},
content: {
color: "black",
textAlign: "center",
},
}}
>
<div>
<h1>Radera Bokning</h1>
<p style={{ marginTop: "20px" }}>
Vänligen ange ditt bokningsNummer för att radera din bokning
</p>
<form onSubmit={() => setDelete}>
<input
onChange={handleChange}
type="text"
name=" bookingId"
placeholder="BokningsID"
value="bookingId"
></input>
<button type="submit"></button>
</form>
<button
style={{ marginTop: "100px" }}
onClick={() => SetModalIsOpen(false)}
>
Tillbaka
</button>
</div>
</Modal>
</>
);
};
export default DeleteBooking;
Here is an incredibly simple example (working sandbox) that you can build upon:
import Axios from "axios";
import React, { useState } from "react";
// => Component Code
// -> This component will be used to delete posts
export default function App() {
// => State
const [readPostId, writePostId] = useState("");
const [readStatus, writeStatus] = useState("");
// => Handlers
const updatePostId = (e) => writePostId(e.target.value);
const deletePost = async (e) => {
e.preventDefault();
try {
await Axios.delete(`${API_ENDPOINT}/${readPostId}`);
writeStatus("Post successfully deleted");
setTimeout(() => writeStatus(""), 3000);
} catch (err) {
writeStatus("Post deletion failed");
}
};
return (
<div>
<h1>Delete Posts Page</h1>
<h2>Enter your Post ID:</h2>
<em>Press 'Delete' without entering a number to cause an error</em>
<form onSubmit={deletePost}>
<input onChange={updatePostId} value={readPostId} />
<input type="submit" value="Delete" />
</form>
{readStatus && <p>{readStatus}</p>}
</div>
);
}
// => Support Code
const API_ENDPOINT = "https://jsonplaceholder.typicode.com/posts";

Resources