I'm using Dropzone component of React-Dropzone with Nextjs and TypeScript.
I want to reject the upload if the file size is over 30MB (30000000 Bytes). The reject message could be whatever at this point.
Currently, when dropping a big file into the zone - this error appears:
I saw that there is a property called onDropRejected to use with Dropzone component in this documentation but how can we use this one instead of running into the error like above?
Here's how my UI looks like:
My code:
type Props = {
name?: string;
isError?: boolean;
onChange?: (id: string) => void;
};
export const FileUploader = ({ name, isError, onChange }: Props) => {
const [uploading, setUploading] = useState(false);
const [nameFile, setNameFile] = useState<string>('File format: CSV Maximum upload size: 30MB');
const [currId, setCurrId] = useState('');
useEffect(() => {
name && setNameFile(name);
}, [name]);
const handleDrop = async (acceptedFiles: File[]) => {
setUploading(true);
const file = acceptedFiles[0];
const res = await AssetApis.create(file, AssetResourceType.marketingAction);
if (res.id) {
setNameFile(file.name);
onChange?.(res.id);
currId && (await AssetApis.remove(currId));
setCurrId(res.id);
}
setUploading(false);
};
return (
<div>
<Dropzone
onDrop={handleDrop}
multiple={false}
accept={['image/*', 'text/csv']}
disabled={uploading}
maxSize={30000000}>
{({ getRootProps, getInputProps }) => (
<div
{...getRootProps({ className: 'dropzone' })}
className='flex items-center w-full h-40 text-center'>
<input {...getInputProps()} />
<div
className={classNames(
'rounded flex h-full items-center justify-center flex-col flex-1 border border-dashed text-gray-700 mr-2.5 py-6',
isError ? 'border-danger' : 'border-gray'
)}>
<Icon name='upload' size={20} />
<div className='mb-2.5 text-medium'>Drag and drop to upload</div>
<button
type='button'
className='px-2.5 border-gray-700 border rounded-full text-small px-3 py-0.5'>
Select file
</button>
<div className='mt-2 text-gray-500 text-small'>{nameFile}</div>
</div>
</div>
)}
</Dropzone>
</div>
);
};
you can add a function validator :
const fileValidator = (file) => {
if (file.size > 30000000) {
return {
code: "size-too-large",
message: `file is larger than 30MB`,
};
}
return null;
}
And then, add your function in your useDropZone :
const { getRootProps, getInputProps, isDragAccept, isDragReject } =
useDropzone({
onDrop,
validator: fileValidator,
});
Related
I'm using vitest and react testing library and the situation is the following:
I'm trying to test whether the UI updates after a user interacts with an input checkbox.
Then the question is: how to do it to be sure that when a user clicks the input, the parent component gets a blue border (I'm using tailwind).
The component that I'm testing:
export const AddOn: FC<props> = ({
title,
desc,
price,
type,
handleAdd,
handleRemove,
checked,
}) => {
const [isChecked, toggleCheck] = useState(checked);
useEffect(() => {
isChecked
? handleAdd(title.toLowerCase(), price)
: handleRemove(title.toLowerCase(), price);
}, [isChecked]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div
className={
"relative w-full border border-n-light-gray rounded-md p-3 lg:px-6 lg:py-3 flex gap-4 items-center hover:opacity-70 " +
(**checked ? " border-p-purplish-blue bg-n-alabaster"** : "")
}
*data-testid={`addon-${title}-container`}*
>
<div className="flex gap-4">
<input
autoComplete="off"
className="w-6 h-6 self-center cursor-pointer"
*data-testid={`addon-${title}`}*
defaultChecked={checked}
type="checkbox"
*onClick={() => toggleCheck(!checked)}*
onKeyPress={(e) => {
e.preventDefault();
toggleCheck(!checked);
}}
/>
<div className="w-8/12 sm:w-full text-left">
<h3 className="text-base text-p-marine-blue font-bold">{title}</h3>
<p className="text-n-cool-gray justify-self-start">{desc}</p>
</div>
</div>
<p className="text-p-purplish-blue text-base font-medium absolute right-2 lg:right-6">
{type === "monthly" ? `+$${price}/mo` : `+$${price}/yr`}
</p>
</div>
);
};
And the test I wrote is:
test("UI TEST: should show the container with a blue border after user clicks on", async () => {
render(
<AddOn
checked={false}
desc="test"
handleAdd={() => {}}
handleRemove={() => {}}
price={0}
title="title-test"
type="test"
/>
);
const addOnOnlineService: HTMLInputElement = await screen.findByTestId(
"addon-title-test"
);
await userEvent.click(addOnOnlineService);
const testContainer: HTMLDivElement = await screen.findByTestId(
"addon-title-test-container"
);
await waitFor(() => {
expect(Array.from(testContainer.classList)).toContain(
"border-p-purplish-blue"
);
});
});
I tried running my test but I couldn't see the HTML updated in the test output. I got the same without the class "border-p-purplish-blue bg-n-alabaster" added because of the state change.
My example could help but I did not using your test way.
1.import act
import { act } from "react-dom/test-utils";
2.using dispatch event then check when text changes, testing class name
it("password length", () => {
act(() => {
render(<ChangePasswordState />, container);
});
expect(state).toBeNull();
let buttonChange = document.getElementsByClassName("blue")[2];
let txtPassword = document.getElementsByTagName("input")[0];
let txtConfirmPassword = document.getElementsByTagName("input")[1];
act(() => {
txtPassword.setAttribute("value", "1234567");
txtPassword.dispatchEvent(new CustomEvent("change", { bubbles: true }))
txtConfirmPassword.setAttribute("value", "1234567");
txtConfirmPassword.dispatchEvent(new CustomEvent("change", { bubbles: true }))
});
expect(buttonChange.className).toContain("disabled");
});
I have authentication context provider to store login user info and show user's photo in header navigation.
I have profile edit page to edit user's display name and photo. After I update the user information to firebase and database via api, I set the user's updated info into context value again. But image on header navigation does not change until manually refresh the page or click the other routes. My I know what I have wrong to refresh the context value.
auth_prodiver.jsx
import React, { useState, useEffect, useContext, createContext } from "react";
import { useLocation, Navigate } from "react-router-dom";
import { signIn, signUp, updateCurrentUser } from "../helpers/gql_auth_helpers";
import paths from "../routes/paths";
import { store } from "../store/configure_store";
import { saveUser } from "../store/slice/auth_slice";
import { auth } from "../helpers/init-firebase";
import {
getAuth,
sendEmailVerification,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
sendPasswordResetEmail,
onAuthStateChanged,
signInWithPopup,
GoogleAuthProvider,
FacebookAuthProvider,
signOut,
confirmPasswordReset,
updateProfile,
verifyPasswordResetCode,
applyActionCode,
checkActionCode,
updatePassword,
} from "firebase/auth";
const AuthContext = createContext(null);
let accessToken = "";
export const getAccessToken = () => accessToken;
export const setAccessToken = (token) => {
accessToken = token;
};
export const AuthProvider = ({ user: usr, children }) => {
const [user, setUser] = useState(usr);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user && user.emailVerified) {
setUser(user);
setAccessToken(user.getIdToken(true));
store.dispatch(saveUser(user));
} else {
setUser(null);
setAccessToken(null);
store.dispatch(saveUser(null));
}
});
return () => {
unsubscribe();
};
}, []);
async function login(email, password) {
return signInWithEmailAndPassword(auth, email, password).then(
async function (res) {
const auth = getAuth();
return await auth.currentUser
.getIdToken(true)
.then(async function (token) {
return await signIn(token);
});
}
);
}
async function register(name, email, password) {
return createUserWithEmailAndPassword(auth, email, password).then(
async function (res) {
const auth = getAuth();
return await updateProfile(auth.currentUser, {
displayName: name,
})
.then(async function () {
const auth = getAuth();
if (auth.currentUser != null) {
return await sendEmailVerification(auth.currentUser).then(
async function (res) {
return await auth.currentUser
.getIdToken(true)
.then(async function (token) {
await signUp(token);
});
}
);
}
})
.catch((error) => {
throw error;
});
}
);
}
async function resendEmailVerification() {
return sendEmailVerification(auth.currentUser);
}
async function forgotPassword(email) {
return sendPasswordResetEmail(auth, email, {
url: `https://myanmargita.com/login`,
});
}
async function confirmPassword(oobCode, newPassword) {
return confirmPasswordReset(auth, oobCode, newPassword);
}
async function updateCurrentUserPassword(newPassword) {
const auth = getAuth();
const user = auth.currentUser;
return updatePassword(user, newPassword);
}
async function updateUser(updateUserId, displayName, photoUrl) {
const auth = getAuth();
const currentUser = getAuth().currentUser;
return await updateProfile(auth.currentUser, {
displayName: displayName,
photoUrl: photoUrl,
})
.then(async function () {
const auth = getAuth();
if (auth.currentUser != null) {
updateCurrentUser(updateUserId, displayName, photoUrl)
.then(async(updatedUser) => {
if (updatedUser) {
await currentUser.reload();
setUser(currentUser);
setAccessToken(currentUser.getIdToken(true));
store.dispatch(saveUser(currentUser));
console.log("User Name : ", user.displayName)
} else {
setUser(null);
setAccessToken(null);
store.dispatch(saveUser(null));
}
})
.catch((error) => {
throw error;
});
}
})
.catch((error) => {
throw error;
});
}
async function resetPassword(actionCode) {
return verifyPasswordResetCode(auth, actionCode);
}
async function applyAction(actionCode) {
return applyActionCode(auth, actionCode);
}
async function checkAction(actionCode) {
return checkActionCode(auth, actionCode);
}
async function logout() {
return signOut(auth);
}
async function signInWithGoogle() {
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
display: "popup",
});
return signInWithPopup(auth, provider);
}
async function signInWithFacebook() {
const provider = new FacebookAuthProvider();
provider.setCustomParameters({
display: "popup",
});
return signInWithPopup(auth, provider);
}
const value = {
user,
accessToken,
login,
register,
forgotPassword,
confirmPassword,
logout,
signInWithGoogle,
signInWithFacebook,
resetPassword,
applyAction,
checkAction,
resendEmailVerification,
updateCurrentUserPassword,
updateUser,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
return useContext(AuthContext);
};
export const RequireAuth = ({ children }) => {
let auth = useAuth();
let location = useLocation();
if (!auth?.user) {
return <Navigate to={paths.login} state={{ from: location }} />;
}
return children;
};
profile_edit_page.jsx
import React, { useEffect,useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as yup from "yup";
import PicturePicker from "../../components/picture_picker";
import ProgressBar from "../../controls/progress_bar";
import LoadingIndicator from "../../components/loading_indicator";
import {getUserWithId } from "../../gql/user";
import { useAuth } from "../../auth/auth_provider";
import { useQuery } from "#apollo/client";
import paths from '../../routes/paths';
import { storage} from "../../helpers/init-firebase";
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
const formSchema = yup.object().shape({
name: yup.string().required("Name is required."),
email: yup.string().required("Email is required."),
});
const formData = {
name: "",
email: "",
photoURL: "",
};
const ProfileEditPage = (props) => {
const { id } = useParams();
const [uploadProgress, setUploadProgress] = useState(0);
const [pictureUrl, setPictureUrl] = useState();
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { data, loading: dataLoading, error } = useQuery(getUserWithId,{variables:{
"where": {
"id": id
}
}});
let auth = useAuth();
const handleUpdate = async (edit_data) => {
console.log("Id : "+ id);
const user_name = edit_data.name.trim();
const email = edit_data.email.trim();
const file = edit_data.photoURL;
if (pictureUrl === file) {
auth.updateUser(id, user_name, edit_data.photoURL);
} else {
const file = edit_data.photoURL;
const storageRef = ref(storage, "images/users/" + file.name);
const uploadTask = uploadBytesResumable(storageRef,file);
uploadTask.on(
"state_changed",
(snapshot) => {
var progress = Math.round(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log("Progrss: ", progress);
setUploadProgress({ progress });
},
(error) => {
console.log("File Upload Error: ", error.message);
throw error;
},
() => {
getDownloadURL(uploadTask.snapshot.ref).then(async(url) => {
await auth.updateUser(id, user_name, url);
navigate(paths.getProfile(id));
});
}
);
}
};
const handleBack = () => {
navigate(-1);
};
useEffect(()=>{
if(!dataLoading && !error){
if(data?.user) {
setPictureUrl(data?.user?.photoURL);
formData.name = data?.user.displayName;
formData.email = data?.user.email;
formData.photoURL = data?.user.photoURL;
}
}
},[dataLoading,error]);
const EntryForm = () => {
return (
<Formik initialValues={formData} enableReinitialize={true} onSubmit={handleUpdate} validationSchema={formSchema}>
{({ dirty, values, errors, touched, handleChange, handleSubmit, handleReset, setFieldValue }) => {
return (
<Form autoComplete="off">
<div className="text-sm">
<div className="flex flex-nowrap">
<div className="w-48 p-2 m-2">Email</div>
<div className="p-2 m-2">
{values.email}
</div>
</div>
<div className="flex flex-nowrap">
<div className="w-48 p-2 m-2">Name</div>
<div className="p-2 m-2">
<Field
type="text"
className="p-2 w-96 textarea textarea-primary"
id="name"
name="name"
placeholder="Display Name"
value={values.name}
onChange={handleChange}
/>
<ErrorMessage name="name" component="span" className="text-sm text-red-500 px-2" />
</div>
</div>
<div className="flex flex-nowrap">
<div className="w-48 p-2 m-2">Profile Image</div>
<div className="flex flex-col items-left align-middle">
<div className="flex flex-col px-4 mt-8 mx-4 h-60 items-cente p-2 m-2" style={{ height: "250px", width: "200px" }}>
<PicturePicker url={pictureUrl} onChange={(file) => setFieldValue("photoURL", file)} value={values.photoURL} />
<ProgressBar className="px-2 pt-2 pb-1" percent={uploadProgress} />
<span className="text-red-600 self-center text-sm">{touched.photoURL && errors.photoURL}</span>
<ErrorMessage name="photoURL" component="span" className="text-sm text-red-500 px-2" />
</div>
</div>
</div>
<div className="flex flex-nowrap p-3 mt-5">
<button type="submit" onClick={handleSubmit} disabled={!dirty} className="btn btn-primary btn-sm mx-2 my-1 w-[66px]">
Update
</button>
<button disabled={!dirty} className="btn btn-primary btn-sm mx-2 my-1 w-[66px]" onClick={handleReset}>
Clear
</button>
</div>
</div>
</Form>
);
}}
</Formik>
);
};
return (
<div className="card">
<header className="px-5 py-4 border-b border-gray-100">
<h2 className="font-semibold text-gray-800">Edit Profile</h2>
</header>
<EntryForm />
<LoadingIndicator loading={loading} color="#000099" label="Uploading..." />
</div>
);
};
export default ProfileEditPage;
profile.jsx (profile component using in header.jsx)
import { useState, useRef, useEffect } from "react";
import {
ProfileIcon,
ResetPasswordIcon,
LogoutIcon,
} from "../assets/icons/svg_icons";
import { useAuth } from "../auth/auth_provider";
import { useNavigate } from "react-router-dom";
import paths from "../routes/paths";
import ProfileModal from "./profile_modal";
const Profile = () => {
const [isOpen, setIsOpen] = useState(false);
const [modal, setModal] = useState({
open: false,
id: "",
});
const wrapperRef = useRef(null);
const auth = useAuth();
const navigate = useNavigate();
const handleLogout = (e) => {
e.preventDefault();
auth.logout();
};
const handleResetPassword = (e) => {
e.preventDefault();
navigate(paths.resetpassword, { replace: true });
};
const handleProfile = (e) => {
e.preventDefault();
navigate(paths.getProfile(auth.user.uid));
};
const handleModalClose = async () => {
setModal({ ...modal, open: false });
};
const handleModalOpen = async () => {
setModal({ ...modal, id: auth.user.uid, open: true });
};
useOutsideClick(wrapperRef);
function useOutsideClick(ref) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
setIsOpen(false);
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
return (
<div className="flex justify-end" ref={wrapperRef}>
<div className="relative">
<div className="cursor-pointer" onClick={() => setIsOpen(!isOpen)}>
<div className="avatar online placeholder">
<div className="bg-neutral-focus text-neutral-content rounded-full w-12 h-12">
{auth.user && (
<div className="avatar online placeholder">
<div className="bg-neutral-focus text-neutral-content rounded-full w-12 h-12">
{auth.user ? (
auth.user?.photoURL == null ? (
<ProfileIcon className="w-10 h-10" />
) : (
<img src={auth.user?.photoURL} />
)
) : (
<div></div>
)}
</div>
</div>
)}
</div>
</div>
</div>
{isOpen && (
<div
className="origin-top-right absolute right-0 mt-2 w-80 rounded-md shadow-lg
bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100
focus:outline-none cursor-pointer z-50"
>
<div className="py-5">
<div className="flex justify-center" onClick={handleModalOpen}>
<div className="relative text-gray-700">
{auth.user ? (
auth.user?.photoURL == null ? (
<ProfileIcon className="w-20 h-20" />
) : (
<img
className="object-cover w-20 h-20 rounded-full"
src={auth.user?.photoURL}
alt={auth.user?.displayName}
/>
)
) : (
<div></div>
)}
<div className="flex absolute right-0 bottom-0 w-8 h-8 bg-gray-400 rounded-full justify-center items-center">
<svg
className="w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="1em"
height="1em"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 1024 1024"
>
<path
fill="currentColor"
d="M864 260H728l-32.4-90.8a32.07 32.07 0 0 0-30.2-21.2H358.6c-13.5 0-25.6 8.5-30.1 21.2L296 260H160c-44.2 0-80 35.8-80 80v456c0 44.2 35.8 80 80 80h704c44.2 0 80-35.8 80-80V340c0-44.2-35.8-80-80-80zM512 716c-88.4 0-160-71.6-160-160s71.6-160 160-160s160 71.6 160 160s-71.6 160-160 160zm-96-160a96 96 0 1 0 192 0a96 96 0 1 0-192 0z"
/>
</svg>
</div>
</div>
</div>
<div className="pt-2" onClick={handleProfile}>
{auth.user ? (
<h1 className="text-sm text-gray-700 p-0 m-0">
{auth.user.displayName}
</h1>
) : (
<div></div>
)}
{auth.user ? (
<h2 className="text-sm text-gray-700 p-0 m-0">
{auth.user.email}
</h2>
) : (
<div></div>
)}
</div>
<div className="pt-4">
<button
type="submit"
className="btn btn-sm btn-ghost rounded-lg mx-6 my-1 px-4 w-auto text-black border border-gray-400"
onClick={handleProfile}
>
Manage your account
</button>
</div>
</div>
<div className="py-1 mx-2">
<a
className="group flex items-center px-4 py-2 text-sm text-gray-700
hover:bg-primary hover:text-white"
onClick={handleResetPassword}
>
<ResetPasswordIcon className="w-8 h-8" />
<span className="px-1">Reset Password</span>
</a>
</div>
<div className="divide-x" />
<div className="py-1 mx-2">
<a
className="group flex items-center px-4 py-2 text-sm text-gray-700
hover:bg-primary hover:text-white"
onClick={handleLogout}
>
<LogoutIcon className="w-8 h-8" />
<span className="px-1">Logout</span>
</a>
</div>
</div>
)}
</div>
{modal.open && (
<ProfileModal id={auth.user.uid} onClose={handleModalClose} />
)}
</div>
);
};
export default Profile;
If you are sure that url has changed add key to img tag
I am trying to submit a form that has 4 form fields.
export type ChapterData = {
chapterNumber: number;
volumeNumber?: number;
chapterName?: string;
chapterImages: ImageListType;
};
I have made a custom hook, useAddChapterForm.
The initial state is as:
const initialState: ChapterData = {
chapterNumber: 0,
volumeNumber: 0,
chapterName: '',
chapterImages: [],
};
I want to update the state of all these elements in a single state and have used this approach.
const [chapterData, setChapterData] = useState<ChapterData>(initialState);
const changeHandler = (e: ChangeEvent<HTMLInputElement>) => {
setChapterData({ ...chapterData, [e.target.name]: e.target.value });
};
But I want to handle the change of the chapterImages differently.
So, my hook looks like so.
export const useAddChapterForm = () => {
const [chapterData, setChapterData] = useState<ChapterData>(initialState);
const [images, setImages] = useState([]);
const maxNumber = 69;
const onChangeImageHandler = (imageList: ImageListType, addUpdateIndex: number[] | undefined) => {
console.log(imageList, addUpdateIndex);
setImages(imageList as never[]);
setChapterData({ ...chapterData, [chapterData.chapterImages]: imageList });
};
const changeHandler = (e: ChangeEvent<HTMLInputElement>) => {
setChapterData({ ...chapterData, [e.target.name]: e.target.value });
};
return {
chapterData,
maxNumber,
changeHandler,
images,
onChangeImageHandler,
};
};
I want to set the value of chapterImages when there is a change in with onChangeImageHandler so I am able to remove the images too.
setChapterData({ ...chapterData, [chapterData.chapterImages]: imageList });
There is a typescript error. So, how do I change the state of this key chapterData.chapterImageswith setState?
Updated the question as per suggested answer:
const onChangeImageHandler = (imageList: ImageListType, addUpdateIndex: number[] | undefined) => {
console.log(imageList, addUpdateIndex);
setImages(imageList as never[]);
setChapterData({ ...chapterData, chapterImages: imageList });
};
const changeHandler = (e: ChangeEvent<HTMLInputElement>) => {
setChapterData({ ...chapterData, [e.target.name]: e.target.value });
};
Here is handle submit:
const { chapterData, changeHandler } = useAddChapterForm();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
console.log(chapterData);
};
return (
<form onSubmit={handleSubmit}>
...
I am using this component to upload the image.
export const ImageUploadField: FunctionComponent<ImageUploadFieldProps> = (
props: ImageUploadFieldProps
) => {
const { error } = props;
const { images, onChangeImageHandler, maxNumber } = useAddChapterForm();
return (
<FieldWrapper label={'Select cover image'} errorMessage={error}>
<ImageUploading multiple value={images} onChange={onChangeImageHandler} maxNumber={maxNumber}>
{({
imageList,
onImageUpload,
onImageRemoveAll,
onImageUpdate,
onImageRemove,
isDragging,
dragProps,
}) => (
<div className="outline-dashed outline-2 outline-offset-2">
{imageList.length === 0 && (
<button
className="w-full text-center"
style={isDragging ? { color: 'red' } : undefined}
onClick={onImageUpload}
{...dragProps}
>
Click or Drop here
</button>
)}
<div className="flex flex-col justify-center">
{imageList.length > 0 && (
<div className="px-2 py-1 rounded bg-red-500 flex">
<button className=" space-x-2 w-full" onClick={onImageRemoveAll}>
Remove all images
</button>
<XCircle />
</div>
)}
<div className="flex flex-wrap">
{imageList.map((image, index) => (
<div key={index} className="flex">
<div className="flex flex-col p-2">
<Image
src={image.dataURL as string}
alt={image.file?.name}
width={150}
height={200}
quality={65}
className="rounded-tl rounded-bl"
/>
<p>{image.file?.name}</p>
<div className="flex space-x-4">
<div className="flex px-2 py-1 rounded bg-green-500 space-x-2">
<button onClick={() => onImageUpdate(index)}>Update</button>
<PencilEdit />
</div>
<div className="flex px-2 py-1 rounded bg-red-500 space-x-2">
<button onClick={() => onImageRemove(index)}>Remove</button>
<XCircle />
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)}
</ImageUploading>
</FieldWrapper>
);
};
You dont have to use computed property name for this line
setChapterData({ ...chapterData, [chapterData.chapterImages]: imageList });
You can just use this setter function like this
setChapterData({ ...chapterData, chapterImages: imageList });
I'm struggling to get "lovedList" values as the page loads for the first time. When re-rendering, the list is correctly updated. How can I make it so it waits for the list values to render the first objects from useEffect?
import { Post, PostProps } from "./Post";
import { useEffect, useState, useCallback } from "react";
import { api } from "../services/api";
import { useSession } from "next-auth/client";
export function PostList() {
const [session, status] = useSession();
const [loading, setLoading] = useState(true);
const [data, setData] = useState<PostProps[]>([])
const [skip, setSkip] = useState(0)
const take = 3
let list = []
async function getUserPurchases() {
if (session) {
const response = await api.get(`/purchases/${session?.id}`)
response.data.map(purchase => {
list.push(purchase.postId)
})
}
return list
}
async function handleLoadMore() {
setLoading(true)
const lovedList = await getUserPurchases();
setSkip(skip + take)
const newPosts = await api.get(`/post?take=${take}&skip=${skip}`)
const formattedData = newPosts.data.map(post => {
console.log(lovedList)
return {
id: post.id,
image: post.image,
institution: post.institution,
title: post.title,
description: post.description,
createdAt: new Date(post.createdAt).toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric'
}),
loved: lovedList.includes(post.id),
author: {
image: post.author.image,
name: post.author.name,
id: post.author.id
}
}
})
setData([...data, ...formattedData])
setLoading(false)
}
useEffect(() => {
handleLoadMore()
}, [])
if (loading) {
return <h1>Carregando</h1>
}
return (
<main className="flex flex-col md:max-w-3xl xl:max-w-6xl mx-auto text-gray-200 text-6xl ">
{data && data.map(post => (
<Post
key={post.id}
image={post.image}
institution={post.institution}
title={post.title}
description={post.description}
createdAt={post.createdAt}
loved={post.loved}
author={post.author}
id={post.id}
/>
))}
<button
type="button"
className="px-4 py-3 bg-blue-400 rounded-full outline-none flex items-center
justify-center text-sm my-16 mx-auto w-4/12 hover:bg-blue-500
transition-colors duration-200 ease-out"
onClick={handleLoadMore}
>
Carregar Mais
</button>
</main>
);
}
When I load the page, the console.log returns empty arrays. If the next few items load, they console.log the propper list. How can I make it so it load properly when accessing the page for the first time?
Thanks!
You can make use either one of these:-
A. use your loading state
only display data if loading=false
return (
<main className="flex flex-col md:max-w-3xl xl:max-w-6xl mx-auto text-gray-200 text-6xl ">
{!loading
? data.map(post => (
<Post
key={post.id}
image={post.image}
institution={post.institution}
title={post.title}
description={post.description}
createdAt={post.createdAt}
loved={post.loved}
author={post.author}
id={post.id}
/>
))
: 'Loading...'
}
<button
type="button"
className="px-4 py-3 bg-blue-400 rounded-full outline-none flex items-center
justify-center text-sm my-16 mx-auto w-4/12 hover:bg-blue-500
transition-colors duration-200 ease-out"
onClick={handleLoadMore}
>
Carregar Mais
</button>
</main>
);
B. set your data state initially to undefined
currently you set them as const [data, setData] = useState<PostProps[]>([])
change it to undefined, const [data, setData] = useState<PostProps[]>(). I'm not sure whether you can do it like this or not. I've never use this useState<Props>([]) syntax before
then your current condition data && data.map(post => (... will work as intended.
It didn't work before cause data was set to be []/empty array initially.
Huge thanks to Joseph from Rockeseat for the solution!
Basically, If I want to have the list ready when the page is loaded, I need to use server-side rendering. Meaning I had to use the ServerSideProps from Next in the page that is using this component.
To validate the data array you should validate the lenght, because "data" constant already exist:
data.lenght > 0
additional to this you can add a prevent to the objects "?":
post?.id
Final code:
import { Post, PostProps } from "./Post";
import { useEffect, useState, useCallback } from "react";
import { api } from "../services/api";
import { useSession } from "next-auth/client";
export function PostList() {
const [session, status] = useSession();
const [loading, setLoading] = useState(true);
const [data, setData] = useState<PostProps[]>([])
const [skip, setSkip] = useState(0)
const take = 3
let list = []
async function getUserPurchases() {
if (session) {
const response = await api.get(`/purchases/${session?.id}`)
response.data.map(purchase => {
list.push(purchase.postId)
})
}
return list
}
async function handleLoadMore() {
setLoading(true)
const lovedList = await getUserPurchases();
setSkip(skip + take)
const newPosts = await api.get(`/post?take=${take}&skip=${skip}`)
const formattedData = newPosts.data.map(post => {
console.log(lovedList)
return {
id: post.id,
image: post.image,
institution: post.institution,
title: post.title,
description: post.description,
createdAt: new Date(post.createdAt).toLocaleDateString('pt-BR', {
day: '2-digit',
month: 'long',
year: 'numeric'
}),
loved: lovedList.includes(post.id),
author: {
image: post.author.image,
name: post.author.name,
id: post.author.id
}
}
})
setData([...data, ...formattedData])
setLoading(false)
}
useEffect(() => {
handleLoadMore()
}, [])
if (loading || data?.lenght === 0) {
return <h1>Carregando</h1>
}
return (
<main className="flex flex-col md:max-w-3xl xl:max-w-6xl mx-auto text-gray-200 text-6xl ">
{data?.length > 0 && data?.map(post => (
<Post
key={post?.id}
image={post?.image}
institution={post?.institution}
title={post?.title}
description={post?.description}
createdAt={post?.createdAt}
loved={post?.loved}
author={post?.author}
id={post?.id}
/>
))}
<button
type="button"
className="px-4 py-3 bg-blue-400 rounded-full outline-none flex items-center
justify-center text-sm my-16 mx-auto w-4/12 hover:bg-blue-500
transition-colors duration-200 ease-out"
onClick={handleLoadMore}
>
Carregar Mais
</button>
</main>
);
}
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;