This is the first time I try to upload a file from a client (Vue.js), through a server (Node.js, Express.js) to a database (MongoDB).
I would like to do it without any library.
The logic I'm trying to implement is the following: user can upload a file from an input. This file (can only be an image) is encoded to base64. Then it is sent to the server and recorded in the database as a string. It actually works. Data provided are well recorded in the database. But it also generates these errors:
This error appears just after loading the image in the input.
Theses errors appear just after submitting the form.
<script setup>
import FormInputMolecule from "#/molecules/FormInput.vue";
import ButtonAtom from "#/atoms/Button.vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { useSkillStore } from "#/store/skillStore";
import { skillSchema } from "#/validations/skillSchema";
import { validationErrors, validateData } from "#/services/yup";
const router = useRouter();
const skillStore = useSkillStore();
const loading = computed(() => skillStore.loaders.createSkill);
const skillData = ref({
name: null,
image: null,
progress: null,
});
const previewImage = async (event) => {
const preview = document.getElementById("preview-image");
skillData.value.image = event.target.files[0];
if (skillData.value.image) {
const imageUrl = URL.createObjectURL(skillData.value.image);
preview.src = imageUrl;
} else {
preview.src = "";
}
};
const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const resetError = (field) => {
if (validationErrors.value.skillCreationForm?.[field]) {
validationErrors.value.skillCreationForm[field] = null;
}
};
const createSkill = async () => {
try {
validationErrors.value["skillCreationForm"] = {};
const isValid = await validateData(
["skillCreationForm"],
skillSchema,
skillData.value
);
if (!isValid) return;
skillData.value.image = await toBase64(skillData.value.image);
skillData.value.progress = parseInt(skillData.value.progress);
await skillStore.createSkill(skillData.value);
console.log(skillData.value);
router.push({ name: "skills" });
} catch (error) {
console.error(error);
}
};
</script>
<template>
<form #submit.prevent="createSkill">
<form-input-molecule
label="Compétence"
id="name"
name="name"
v-model="skillData.name"
placeholder="Exemple: JavaScript"
:error-message="validationErrors.skillCreationForm?.name"
#input="resetError('name')"
></form-input-molecule>
<form-input-molecule
label="Image"
id="image"
name="image"
type="file"
v-model="skillData.image"
:error-message="validationErrors.skillCreationForm?.image"
#change="previewImage"
#input="resetError('image')"
></form-input-molecule>
<img id="preview-image" width="100" />
<form-input-molecule
label="Progression"
id="progress"
name="progress"
type="number"
v-model="skillData.progress"
placeholder="Exemple: 60"
:min="0"
:max="100"
:error-message="validationErrors.skillCreationForm?.progress"
#input="resetError('progress')"
></form-input-molecule>
<div class="button-wrapper">
<button-atom type="submit" :loading="loading">Créer</button-atom>
</div>
</form>
</template>
I really do not know where these errors are coming from. If you guys could please help me.
If you can put up this code in stackblitz etc with all your dependencies it might be easier for people to help you.
Nevertheless, based on the error messages you have received..regarding the first error...
You have used a v-model for input element which is skillData.image and in the createSkill function you are assigning it a base64 value
skillData.value.image = await toBase64(skillData.value.image);
I think the error is saying that this input field can only have the file name. So maybe you want to look at why you are doing this update. You can just store it in a const.
Regarding the second error, it seems that the Skills route is loading before the values required for that page are received. So maybe you can try to do a v-if in on the skills page to only load on data received. Unable to dig more as you have not shared that page.
Related
I am trying to run this 'getting started' example from the docs. However I think there's been a change and programs.metadata.Metadata (shown there) no longer works.
https://docs.metaplex.com/sdk/js/getting-started
They suggest this:
import { Connection, programs } from '#metaplex/js';
const connection = new Connection('devnet');
const tokenPublicKey = 'Gz3vYbpsB2agTsAwedtvtTkQ1CG9vsioqLW3r9ecNpvZ';
const run = async () => {
try {
const ownedMetadata = await programs.metadata.Metadata.load(connection, tokenPublicKey);
console.log(ownedMetadata);
} catch {
console.log('Failed to fetch metadata');
}
};
run();
I have this in my React app:
import { Connection, programs } from '#metaplex/js';
const connection = new Connection('devnet');
const tokenPublicKey = 'Gz3vYbpsB2agTsAwedtvtTkQ1CG9vsioqLW3r9ecNpvZ';
const run = async () => {
try {
const ownedMetadata = await programs.metadata.Metadata.load(connection, tokenPublicKey);
console.log(ownedMetadata);
} catch(error) {
console.log('Failed to fetch metadata');
console.log(error);
}
};
function App() {
return (
<div className="App">
<p onClick={run}>would be cool if it worked</p>
</div>
);
}
export default App;
I get an error as though programs.metadata.Metadata doesn't exist - "Cannot read properties of undefined (reading 'Metadata')". I even took this out of React and did a plain node script to just run the example code, which fails in the same way.
Any idea on how I could fix this?
For users that find this question via search:
There is now a better solution than using #metaplex-foundation/mpl-token-metadata directly. Have a look at #metaplex/js-next findByMint
Those lines should be all you need. imageUrl would be the image path.
import { Metaplex } from "#metaplex-foundation/js-next";
import { Connection, clusterApiUrl } from "#solana/web3.js";
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const metaplex = new Metaplex(connection);
const mint = new PublicKey("ATe3DymKZadrUoqAMn7HSpraxE4gB88uo1L9zLGmzJeL");
const nft = await metaplex.nfts().findByMint(mint);
const imageUrl = nft.metadata.image;
I'm using expo/react-native, developing on MacOS, testing on the iOS emulator. Following various examples I keep getting 400s (invalid url, missing file parameter etc...) when I POST a file selected from the photo library to Cloudinary. Unfortunately Cloudinary doesn't have very good documentation for react-native.
I also want to display a progress indicator so using fetch() is not an option.
I'm doing the uploads directly from the mobile app rather than passing through my server which is why Cloudinary's nodeJS docs don't apply. For security these upload requests will eventually be signed.
This took way longer to figure out than I liked so I'm posting the solution here. The real key is that Cloudinary expects image data to be provided as base64 from mobile whereas from the web a local file URL was sufficient. Fortunately the base64 data is available directly from the expo-image-picker component but it still must be formatted as a data uri to be uploaded.
import React, { useEffect, useState } from 'react';
import * as ImagePicker from 'expo-image-picker';
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types';
import { Platform, TouchableOpacity } from 'react-native';
import { CloudinaryImage } from './CloudinaryImage';
export const MyComponent = () => {
const [cloudinaryId, setCloudinaryId] = useState('');
const [progress, setProgress] = useState(100);
useEffect(() => {
(async () => {
if (Platform.OS !== 'web') {
const { status } =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted')
alert('Sorry, we need camera roll permissions to make this work!');
}
})();
}, []);
return (progress === 100) ?
(
<TouchableOpacity onPress={pickImage}>
<CloudinaryImage cloudinaryId={cloudinaryId} />
</TouchableOpacity>
) : ( ... a progress indicator of your choosing );
The pickImage function follows. This runs when the user touches the TouchableOpacity component.
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
base64: true, // this must be set for base64 to be returned!
});
if (!result.cancelled) {
const { uri, base64 } = result;
cloudinaryUpload({ image: { uri, base64 }, setProgress, setCloudinaryId });
}
};
The cloudinaryUpload function follows. This uses XMLHttpRequest instead of fetch in order to support progress.
const cloudName = 'yourCloud';
const uploadPreset = 'yourPreset';
const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`;
export const cloudinaryUpload = async ({ image, setProgress, setCloudinaryId }) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// Update progress (can be used to show progress indicator)
xhr.upload.addEventListener('progress', (e) => {
const progress = Math.round((e.loaded * 100.0) / e.total);
setProgress(progress);
});
// handle successful upload
xhr.onreadystatechange = (e) => {
if (xhr.readyState == 4 && xhr.status == 200) {
const response = JSON.parse(xhr.responseText);
setProgress(100);
setCloudinaryId(response.public_id);
}
};
xhr.addEventListener('error', (e) => console.error(e));
const fd = new FormData();
const data = `data:;base64,${image.base64}`; // the critical step!
fd.append('file', data);
fd.append('upload_preset', uploadPreset);
fd.append('tags', 'your tags');
setProgress(0);
xhr.send(fd);
};
<CloudinaryImage /> is just a component that renders a Cloudinary image based on its ID. The source to that component is the answer to a different question.
I have yet to try this on Android.
I am trying to read a user-selected file in my React app, using the following code:
function Entries() {
async function importEntries() {
let file = document.getElementById('file-select').files[0];
let response = await fetch(file);
let textResponse = await response.text();
console.log(textResponse);
}
return (
<div className='Entries'>
<h2>Import entries</h2>
<label htmlFor='file-select'>Select file:</label>
<input type='file' name='file' id='file-select' />
<button onClick={importEntries}>Upload</button>
</div>
);
}
This is somehow consistently giving me the contents of the index.html file in the public directory of my React app, no matter which file I select.
Anyone knows what is causing this?
use then and catch in fetch method
Example :
fetch('http://example.com/movies.json')
.then(response => response.json())
.then(data => console.log(data));
.catch(err => console.log(err))
Is there any reason you're trying to fetch this file? As far as I have seen, you can do something like
async function importEntries() {
let file = document.getElementById('file-select').files[0];
let textResponse = await file.text();
console.log(textResponse);
}
or use a FileReader.
Also, unrelated, you might want to avoid using document.getElementById and useRef instead, like so:
const fileInputRef = useRef();
async function importEntries() {
let file = fileInputRef.current.files[0];
let textResponse = await file.text();
console.log(textResponse);
}
//Later in the code
<input type='file' name='file' ref={fileInputRef} />
Try this:
Replace <input type='file' name='file' id='file-select' /> to <input type='file' name='file' id='file-select' onChange={importEntries}/>
And then replace your importEntries method to this:
async function importEntries(e) {
let file = e.target.files[0];
/* Try this edit */
let reader = new FileReader();
reader.readAsText(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
}
I haven't tested the code let me know if this worked.
I am using the react-dropzone library to drag or select files from the browser.
I have the below component to let the user select multiple files. Everything works except that if the user chooses more files let say 2 or more files with size 1 MB, the selection of documents is taking time. If the number of files is more, the more time it takes to selected the user-selected files.
As per my reading of the react-dropzone docs, it processes files as soon as it uploads. So I tried setting autoProcessQueue='false'.
but no luck.
I just want to let the user select all the files without blocking for 30 or 40 secs after selecting files, and before send to the backend server.
I did the debug and the setSelectedUserFiles() is reached after all the files are processed internally by the DropZone. I am not sure if there is a way to disable it and let it process as part final submission of the form or click on the button.
In case if we can't achieve the same, is there a way to show the message to the user that files are being attached.
Any help will be appreciated.
Below is my react component
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
const handleUserFileUpload = async (acceptedFiles) => {
await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
<MyButton>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}
Try something like this
Create separate api component that only send files,
Your component that has dropzone manages file upload
locally without sending to the server. You only click the button
and send it.
this link was very helpful:
https://www.robinwieruch.de/react-hooks-fetch-data
first component:
export const useApiCall = () => {
const [data, setData] = useState({});
const [selectedAllUserFiles, setSelectedAllUserFiles] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const pushToserver = async () => {
setIsError(false);
setIsLoading(true);
try {
const formData = new FormData();
Array.from(selectedAllUserFiles).forEach((file) => {
form.append('fileData', file);
});
const result = await axios.post(someUrl, formData);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
if (selectedAllUserFiles.length > 0) {
pushToserver();
}
}, [selectedAllUserFiles]);
//passing reference to the calling component
return [{ data, isLoading, isError }, setSelectedAllUserFiles];
};
Second component:
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
//Ref from logical api call component
const [{ data, isLoading, isError }, setSelectedAllUserFiles] = useApiCall();//new component
const handleUserFileUpload = async (acceptedFiles) => {
if (acceptedFiles) {
acceptedFiles.map((file) => {
setSelectedUserFiles((selectedUserFiles) => selectedUserFiles.concat(file));
return selectedUserFiles;
});
}
//await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
//on click call api and pass collected user files all together
<MyButton onClick={()=>setSelectedAllUserFiles(selectedUserFiles)}>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}
I am trying to upload a file in the firestore and the following code is working:
export const ticketEdit = values => {
return async (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
// delete file
const file = values.file && values.file;
delete values.file;
try {
// uploadFile
if (file) await dispatch(ticketUploadFile(file, ticketId));
// notification
toastr.success(i18n.t("Success", i18n.t("The ticket is updated")));
} catch (e) {
console.log(e);
}
};
};
However, If I will put the lines that I am deleting the file
// delete file
const file = values.file && values.file;
delete values.file;
In the try catch statement like this :
export const ticketEdit = values => {
return async (dispatch, getState, { getFirebase, getFirestore }) => {
const firestore = getFirestore();
try {
// delete file
const file = values.file && values.file;
delete values.file;
if (file) await dispatch(ticketUploadFile(file, ticketId));
// notification
toastr.success(i18n.t("Success", i18n.t("The ticket is updated")));
} catch (e) {
console.log(e);
}
};
};
I get the error
Unsupported field value: a custom File object.
and I would like to know why. Thank you!
The problem was not the try catch statement but that I was trying to update the values before deleting the file.
Lesson learned: delete the files before updating the document.