Skip to content

Commit e6aeb9c

Browse files
authored
add support for image upload
add support for image upload
2 parents ec9adc9 + 0727b60 commit e6aeb9c

File tree

6 files changed

+316
-68
lines changed

6 files changed

+316
-68
lines changed

package-lock.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"firebase": "^9.18.0",
1515
"react": "^18.2.0",
1616
"react-dom": "^18.2.0",
17+
"react-dropzone": "^14.2.3",
1718
"react-hook-form": "^7.43.9",
1819
"react-icons": "^4.9.0",
1920
"react-paginate": "^8.2.0",

src/components/Modal.jsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from "react";
2+
3+
function Modal({ closeModal, children }) {
4+
return (
5+
<div className="fixed z-10 inset-0 overflow-y-auto">
6+
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
7+
<div
8+
className="fixed inset-0 transition-opacity"
9+
aria-hidden="true"
10+
onClick={closeModal}
11+
>
12+
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
13+
</div>
14+
<span
15+
className="hidden sm:inline-block sm:align-middle sm:h-screen"
16+
aria-hidden="true"
17+
>
18+
&#8203;
19+
</span>
20+
<div
21+
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
22+
role="dialog"
23+
aria-modal="true"
24+
aria-labelledby="modal-headline"
25+
>
26+
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
27+
<div className="sm:flex sm:items-start">
28+
<div className="mt-3 text-center sm:mt-0 sm:text-left">
29+
<div className="mt-2">{children}</div>
30+
</div>
31+
</div>
32+
</div>
33+
<div className="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
34+
<button
35+
type="button"
36+
onClick={closeModal}
37+
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-500 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600 sm:ml-3 sm:w-auto sm:text-sm"
38+
>
39+
Close
40+
</button>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
);
46+
}
47+
48+
export default Modal;

src/firebase.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { initializeApp } from "firebase/app";
22
import { getFirestore } from "@firebase/firestore";
33
import { getAuth } from "firebase/auth";
4+
import { getStorage } from "firebase/storage";
45

56
const firebaseConfig = {
67
apiKey: "AIzaSyCxXu5d2dB_QMe8JEdJq0HjJkrpvsoXQBA",
@@ -15,4 +16,4 @@ const firebaseConfig = {
1516
const app = initializeApp(firebaseConfig);
1617
export const db = getFirestore(app);
1718
export const auth = getAuth(app);
18-
19+
export const storage = getStorage(app);

src/pages/AddProject.jsx

Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import React, { useState } from 'react';
2-
import { collection, addDoc } from "firebase/firestore";
3-
import { db } from '../firebase';
1+
import React, { useState, useCallback } from 'react';
2+
import { collection, addDoc } from 'firebase/firestore';
3+
import { db, storage } from '../firebase';
44
import { useNavigate } from 'react-router-dom';
55
import { useForm } from 'react-hook-form';
66
import { yupResolver } from '@hookform/resolvers/yup';
77
import { WithContext as ReactTags } from 'react-tag-input';
8+
import { useDropzone } from 'react-dropzone';
89
import * as yup from 'yup';
910
import '../tags.css';
11+
import { ref, uploadBytesResumable, getDownloadURL } from 'firebase/storage';
1012

1113
const KeyCodes = {
1214
comma: 188,
@@ -22,55 +24,104 @@ function AddProject() {
2224
const [projectLink, setProjectLink] = useState('');
2325
const [demoLink, setDemoLink] = useState('');
2426
const [category, setCategory] = useState('');
25-
const userData = collection(db, 'projects');
2627
const [tags, setTags] = useState([]);
28+
const [selectedImages, setSelectedImages] = useState([]);
29+
const [droppedImages, setDroppedImages] = useState([]);
30+
const [errorMessage, setErrorMessage] = useState('');
31+
const [uploadProgress, setUploadProgress] = useState(0); // State for progress indicator
2732
const navigate = useNavigate();
2833

29-
const saveData = async () => {
30-
const tagsArr = tags.map(tag => tag.text);
34+
const saveData = async (data) => {
35+
const imageFiles = selectedImages.concat(droppedImages);
36+
const tagsArr = tags.map((tag) => tag.text);
3137
const projectData = {
3238
title: title,
3339
description: description,
3440
userGithubLink: githubLink,
3541
projectGithubLink: projectLink,
3642
demoLink: demoLink,
3743
tags: tagsArr,
38-
category: category
44+
category: category,
3945
};
40-
await addDoc(userData, projectData);
41-
navigate('/viewprojects');
46+
47+
// Upload the image files to Firebase Storage
48+
const storagePromises = imageFiles.map((imageFile) => {
49+
const storageRef = ref(storage, 'images/' + imageFile.name);
50+
51+
// Create a unique upload task for each file
52+
const uploadTask = uploadBytesResumable(storageRef, imageFile);
53+
54+
// Track the upload progress
55+
uploadTask.on('state_changed', (snapshot) => {
56+
const progress = Math.round(
57+
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
58+
);
59+
setUploadProgress(progress);
60+
});
61+
62+
// Return a promise that resolves when the upload is complete
63+
return new Promise((resolve, reject) => {
64+
uploadTask
65+
.then((snapshot) => {
66+
// Get the download URL after the upload is complete
67+
getDownloadURL(snapshot.ref)
68+
.then((downloadUrl) => resolve(downloadUrl))
69+
.catch(reject);
70+
})
71+
.catch(reject);
72+
});
73+
});
74+
75+
try {
76+
// Wait for all image uploads to complete
77+
const downloadUrls = await Promise.all(storagePromises);
78+
79+
// Add the image URLs to the project data
80+
projectData.imageUrls = downloadUrls;
81+
82+
// Save the project data to Firestore
83+
const docRef = await addDoc(collection(db, 'projects'), projectData);
84+
navigate('/viewprojects');
85+
} catch (error) {
86+
setErrorMessage(error.message);
87+
}
4288
};
4389

4490

4591
const schema = yup.object({
46-
title: yup.string().required("Project Title is required"),
47-
description: yup.string().required("Project Description is required"),
48-
userGithubLink: yup.string().required("User Github Link is required"),
49-
projectGithubLink: yup.string().required("Project Github Link is required"),
50-
demoLink: yup.string().required("Demo Link is required"),
92+
title: yup.string().required('Project Title is required'),
93+
description: yup.string().required('Project Description is required'),
94+
userGithubLink: yup.string().required('User Github Link is required'),
95+
projectGithubLink: yup.string().required('Project Github Link is required'),
96+
demoLink: yup.string().required('Demo Link is required'),
5197
}).required();
5298

5399
const { register, handleSubmit, formState: { errors } } = useForm({
54100
resolver: yupResolver(schema)
55101
});
56102

57103
const onSubmit = (data) => {
58-
saveData();
104+
saveData(data);
105+
};
106+
107+
const handleImageChange = (e) => {
108+
const files = Array.from(e.target.files);
109+
setSelectedImages(files);
59110
};
60111

61112
const suggestions = [
62-
{ id: "1", text: "Javascript" },
63-
{ id: "2", text: "Python" },
64-
{ id: "3", text: "Java" },
65-
{ id: "4", text: "HTML" },
66-
{ id: "5", text: "PHP" },
67-
{ id: "6", text: "TypeScript" },
68-
{ id: "7", text: "React" },
69-
{ id: "8", text: "Vue" },
70-
{ id: "9", text: "Angular" },
71-
{ id: "10", text: "Bootstrap" },
72-
{ id: "11", text: "Tailwind" },
73-
{ id: "12", text: "CSS" },
113+
{ id: '1', text: 'Javascript' },
114+
{ id: '2', text: 'Python' },
115+
{ id: '3', text: 'Java' },
116+
{ id: '4', text: 'HTML' },
117+
{ id: '5', text: 'PHP' },
118+
{ id: '6', text: 'TypeScript' },
119+
{ id: '7', text: 'React' },
120+
{ id: '8', text: 'Vue' },
121+
{ id: '9', text: 'Angular' },
122+
{ id: '10', text: 'Bootstrap' },
123+
{ id: '11', text: 'Tailwind' },
124+
{ id: '12', text: 'CSS' },
74125
];
75126

76127
const handleAddition = tag => {
@@ -81,9 +132,21 @@ function AddProject() {
81132
setTags(tags.filter((tag, index) => index !== i));
82133
};
83134

135+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
136+
accept: 'image/*',
137+
onDrop: (acceptedFiles) => {
138+
const invalidFiles = acceptedFiles.filter(file => !file.type.startsWith('image/'));
139+
if (invalidFiles.length > 0) {
140+
setErrorMessage('Invalid file types. Please drop only image files.');
141+
} else {
142+
setDroppedImages((prevImages) => [...prevImages, ...acceptedFiles]);
143+
}
144+
},
145+
});
146+
84147
return (
85148
<section className=''>
86-
<div className='flex md:pl-10 flex-col items-center ml-1 md:ml-60 h-[100%] pb-4 mb-8 pt-10 Context'>
149+
<div className='flex md:pl-10 flex-col items-center ml-1 md:ml-60 h-[100%] pb-8 mb-8 pt-10 Context'>
87150
<form onSubmit={handleSubmit(onSubmit)}>
88151
<h2 className="font-medium leading-tight text-md mt-10 mx-1 md:mt-[50px] mb-4 text-gray-700">Add your project details by filling the form below</h2>
89152
<div className='flex flex-col md:flex-row justify-start items-start w-[100%]'>
@@ -92,6 +155,25 @@ function AddProject() {
92155
<p className='text-red-500'>{errors.title?.message}</p>
93156
<textarea placeholder='Project Description' {...register("description")} className='placeholder:text-slate-500 block bg-white min-h-[170px] w-[90vw] md:w-[36vw] lg:w-[32vw] border border-slate-300 rounded-md my-4 py-2 pl-4 pr-3 shadow-sm focus:outline-none focus:border-sky-500 focus:ring-sky-500 focus:ring-1' onChange={(e) => setDescription(e.target.value)}></textarea>
94157
<p className='text-red-500'>{errors.description?.message}</p>
158+
<section className='flex flex-col w-full justify-start bg-gray-200 border border-gray-400 p-3'>
159+
<div>
160+
<div {...getRootProps()}>
161+
<input {...getInputProps()} />
162+
{isDragActive ? (
163+
<p>Drop the files here ...</p>
164+
) : (
165+
<p>Drag and drop some files here, or click to select files</p>
166+
)}
167+
</div>
168+
{errorMessage && <p className="text-red-500">{errorMessage}</p>}
169+
<div className="w-full grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-2">
170+
{droppedImages.map((image, index) => (
171+
<img key={index} src={URL.createObjectURL(image)} alt="Selected" className="w-32 h-32 my-4" />
172+
))}
173+
</div>
174+
</div>
175+
</section>
176+
95177
</div>
96178

97179
<div className='md:ml-5 w-11/12 h-64 md:w-2/3 lg:w-1/2 my-5 mx-5 md:my-0'>
@@ -108,14 +190,17 @@ function AddProject() {
108190
className='block bg-white w-[90vw] md:w-[36vw] lg:w-[32vw] border border-slate-300 rounded-md my-4 py-2 pl-4 pr-3 shadow-sm focus:outline-none focus:border-sky-500 focus:ring-sky-500 focus:ring-1'
109191
>
110192
<option value="">Select Category</option>
111-
<option value="Development">Development</option>
112-
<option value="Mobile App Development">Mobile App Development</option>
193+
<option value="Web Development">Web Development</option>
194+
<option value="Mobile Development">Mobile Development</option>
113195
<option value="Data Science">Data Science</option>
196+
<option value="Machine Learning">Machine Learning</option>
114197
<option value="Artificial Intelligence">Artificial Intelligence</option>
115-
<option value="Game Development">Game Development</option>
198+
<option value="Blockchain">Blockchain</option>
199+
<option value="Cybersecurity">Cybersecurity</option>
116200
<option value="UI/UX Design">UI/UX Design</option>
117-
<option value="E-commerce">E-commerce</option>
201+
<option value="Other">Other</option>
118202
</select>
203+
<p className='text-red-500'>{errors.category?.message}</p>
119204
<div className='placeholder:text-slate-500 block bg-white w-[90vw] md:w-[36vw] lg:w-[32vw] border border-slate-300 rounded-md my-4 py-2 pl-4 pr-3 shadow-sm focus:outline-none focus:border-sky-500 focus:ring-sky-500 focus:ring-1'>
120205
<ReactTags
121206
tags={tags}
@@ -136,15 +221,31 @@ function AddProject() {
136221
}}
137222
/>
138223
</div>
224+
<div className='my-4'>
225+
<button type='submit' className='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded'>
226+
Submit
227+
</button>
228+
<button onClick={() => navigate('/viewprojects')} className='bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded ml-4'>
229+
Cancel
230+
</button>
231+
</div>
139232
</div>
140233
</div>
141-
<button className="px-6 py-2 mt-8 mx-1 font-medium text-white bg-blue-800 rounded-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:scale-110 hover:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" type='submit'>
142-
Save Project
143-
</button>
234+
{uploadProgress > 0 && (
235+
<div className="relative pt-1">
236+
<div className="overflow-hidden h-2 mb-4 text-xs flex rounded bg-gray-200">
237+
<div
238+
style={{ width: `${uploadProgress}%` }}
239+
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-green-500"
240+
></div>
241+
</div>
242+
<div className="text-center">{uploadProgress}%</div>
243+
</div>
244+
)}
144245
</form>
145246
</div>
146247
</section>
147-
)
248+
);
148249
}
149250

150251
export default AddProject;

0 commit comments

Comments
 (0)