create update page functionality
This commit is contained in:
parent
06d1da277f
commit
0b792b751c
5 changed files with 290 additions and 4 deletions
|
@ -7,6 +7,7 @@ import {Profile} from './pages/Profile'
|
|||
import Navbar from './components/Navbar'
|
||||
import PrivateRoute from './components/PrivateRoute'
|
||||
import CreatListing from './pages/CreatListing'
|
||||
import UpdateListing from './pages/UpdateListing'
|
||||
|
||||
|
||||
const App = () => {
|
||||
|
@ -18,6 +19,7 @@ const App = () => {
|
|||
<Route element={<PrivateRoute/>}>
|
||||
<Route path='/profile' element={<Profile/>}/>
|
||||
<Route path='/createListing' element={<CreatListing/>}/>
|
||||
<Route path='/updateListing/:listingId' element={<UpdateListing/>}/>
|
||||
</Route>
|
||||
<Route path='/login' element={<Login/>}/>
|
||||
<Route path='/signup' element={<SignUp/>}/>
|
||||
|
|
|
@ -122,7 +122,24 @@ export const Profile = () => {
|
|||
setUserListingsError(true);
|
||||
}
|
||||
};
|
||||
//delete user listing functionality
|
||||
const handleListingDelete = async (listingId) => {
|
||||
try {
|
||||
const res = await fetch(`/server/listing/delete/${listingId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await res.json();
|
||||
if(data.success === false){
|
||||
return;
|
||||
}
|
||||
setUserListings((prev) =>
|
||||
prev.filter((listing) => listing._id !== listingId)
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.log(error.message)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
|
@ -165,8 +182,8 @@ export const Profile = () => {
|
|||
<p>{listing.name}</p>
|
||||
</Link>
|
||||
<div className="flex gap-4">
|
||||
<button className='text-red-700'><FaRegTrashAlt/></button>
|
||||
<button className='text-green-700'><BiEdit/></button>
|
||||
<button onClick={() => handleListingDelete(listing._id)} className='text-red-700'><FaRegTrashAlt/></button>
|
||||
<Link to={`/updateListing/${listing._id}`}><button className='text-green-700'><BiEdit/></button></Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
215
real_estate/client/src/pages/UpdateListing.jsx
Normal file
215
real_estate/client/src/pages/UpdateListing.jsx
Normal file
|
@ -0,0 +1,215 @@
|
|||
import { useEffect, useState } from "react"
|
||||
import {getDownloadURL, getStorage, ref, uploadBytesResumable} from 'firebase/storage'
|
||||
import {app} from '../firebase'
|
||||
import {useSelector} from 'react-redux'
|
||||
import {useNavigate, useParams} from 'react-router-dom'
|
||||
|
||||
|
||||
const UpdateListing = () => {
|
||||
const {currentUser} = useSelector((state) => state.user)
|
||||
const navigate =useNavigate();
|
||||
const params = useParams();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [formData, setFormData] = useState({
|
||||
imageUrls: [],
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
type: 'rent',
|
||||
bed: 1,
|
||||
bath: 1,
|
||||
currentPrice: 100,
|
||||
discountPrice: 0,
|
||||
offer: false,
|
||||
parking: false,
|
||||
furnished: false,
|
||||
});
|
||||
const [imageUploadError, setImageUploadError] = useState(false);
|
||||
const [upload, setUpload] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
const fetchListing = async() => {
|
||||
const listingId = params.listingId;
|
||||
const res = await fetch(`/server/listing/get/${listingId}`);
|
||||
const data = await res.json();
|
||||
if(data.success === false){
|
||||
setError(data.message);
|
||||
return;
|
||||
}
|
||||
setFormData(data);
|
||||
};
|
||||
fetchListing();
|
||||
}, []);
|
||||
|
||||
const handleImageSubmit = (e) => {
|
||||
if(files.length > 0 && files.length + formData.imageUrls.length < 11){
|
||||
setUpload(true);
|
||||
setImageUploadError(false);
|
||||
|
||||
const promises = [];
|
||||
for(let i = 0; i < files.length; i++){
|
||||
promises.push(storeImage(files[i]));
|
||||
}
|
||||
Promise.all(promises).then((urls) => {
|
||||
setFormData({...formData, imageUrls: formData.imageUrls.concat(urls),
|
||||
});
|
||||
setImageUploadError(false);
|
||||
setUpload(false);
|
||||
}).catch((error) => {
|
||||
setImageUploadError('Image upload failed (2mb max per image)');
|
||||
setUpload(false);
|
||||
});
|
||||
}else{
|
||||
setImageUploadError('You can upload maximum 10 images');
|
||||
setUpload(false);
|
||||
}
|
||||
};
|
||||
const storeImage = async(file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storage = getStorage(app);
|
||||
const fileName = new Date().getTime() + file.name
|
||||
const storageRef = ref(storage, fileName);
|
||||
const uploadTask = uploadBytesResumable(storageRef, file);
|
||||
uploadTask.on(
|
||||
"state_changed",(snapshot) => {
|
||||
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
|
||||
console.log(`Upload is ${progress}% done`)
|
||||
},
|
||||
(error) => {
|
||||
reject(error);
|
||||
},
|
||||
() => {
|
||||
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
|
||||
resolve(downloadURL)
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
const handleDeleteImage = (index) => {
|
||||
setFormData({...formData, imageUrls: formData.imageUrls.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
if (e.target.id === 'sale' || e.target.id === 'rent') {
|
||||
setFormData({...formData, type: e.target.id, });
|
||||
}
|
||||
if(e.target.id === 'parking' || e.target.id === 'furnished' || e.target.id === 'offer'){
|
||||
setFormData({...formData, [e.target.id]: e.target.checked, });
|
||||
}
|
||||
if(e.target.type === 'number' || e.target.type === 'text' || e.target.type === 'textarea'){
|
||||
setFormData({...formData, [e.target.id]: e.target.value, });
|
||||
}
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if(formData.imageUrls.length < 1)
|
||||
return setError('You need to upload at least one image');
|
||||
if (+formData.currentPrice < +formData.discountPrice)
|
||||
return setError('Discount must be lower than your current price');
|
||||
|
||||
setLoading(true);
|
||||
setError(false);
|
||||
const res = await fetch(`/server/listing/update/${params.listingId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({...formData, userRef: currentUser._id,}),
|
||||
});
|
||||
const data = await res.json();
|
||||
setLoading(false);
|
||||
if(data.success === false){
|
||||
setError(data.message)
|
||||
}
|
||||
navigate(`/listing/${data._id}`)
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setLoading(false);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 max-w-4xl mx-auto">
|
||||
<h1 className="text-2xl uppercase text-center text-blue-900 font-serif my-10 tracking-wide">Update a listing</h1>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex flex-col flex-1 gap-4">
|
||||
<input type="text" placeholder="Title" onChange={handleChange} value={formData.name} id="name" className='border p-2 rounded-md text-sm ' maxLength='62' minLength='10' required/>
|
||||
<textarea type="text" placeholder="Description" onChange={handleChange} value={formData.description} id="description" className='border p-2 rounded-md text-sm resize-none' required/>
|
||||
<input type="text" placeholder="Address" onChange={handleChange} value={formData.address} id="address" className='border p-2 rounded-md text-sm ' required/>
|
||||
<div className="flex gap-4 flex-wrap text-sm">
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" onChange={handleChange} checked={formData.type === "sale"} id="sale" className="w-4"/>
|
||||
<span>Sell</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" onChange={handleChange} checked={formData.type === "rent"} id="rent" className="w-4"/>
|
||||
<span>Rent</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" onChange={handleChange} checked={formData.parking} id="parking" className="w-4"/>
|
||||
<span>Garage</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" onChange={handleChange} checked={formData.furnished} id="furnished" className="w-4"/>
|
||||
<span>Furnished</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="checkbox" onChange={handleChange} checked={formData.offer} id="offer" className="w-4"/>
|
||||
<span>Offer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className=" flex flex-wrap gap-6 text-sm">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input type='number' onChange={handleChange} value={formData.bed} id="bed" min='1' required className="p-2 border border-gray-500 rounded-lg w-20"/>
|
||||
<p>Beds</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input type='number' onChange={handleChange} value={formData.bath} id="bath" min='1' required className="p-2 border border-gray-500 rounded-lg w-20"/>
|
||||
<p>Baths</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<input type='number' onChange={handleChange} value={formData.currentPrice} id="currentPrice" min='100' max='10000000' required className="p-2 border border-gray-500 rounded-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"/>
|
||||
<div className="flex flex-col items-center">
|
||||
<p>Current price</p>
|
||||
<span className="text-xs">($ per mounth)</span>
|
||||
</div>
|
||||
</div>
|
||||
{formData.offer && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<input type='number' onChange={handleChange} value={formData.discountPrice} id="discountPrice" min='0' max='10000000' required className="p-2 border border-gray-500 rounded-lg [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"/>
|
||||
<div className="flex flex-col items-center">
|
||||
<p>Discounted price</p>
|
||||
<span className="text-xs">($ per mounth)</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 gap-4">
|
||||
<p className="font-semibold">Images: <span className="font-normal text-gray-500 ml-2">You can upload maximum 10 images</span></p>
|
||||
<div className="flex gap-4">
|
||||
<input type="file" multiple accept="image/*" onChange={(e) => setFiles(e.target.files)} id="images" className="p-2 border border-gray-300 rounded w-full"/>
|
||||
<button disabled={loading} type='button' onClick={handleImageSubmit} className="p-2 text-grenn-900 text-xs tracking-wider border border-blue-800 rounded uppercase hover:bg-blue-100 hover:border-blue-300 disabled:opacity-80">{upload ? 'Uploading...' : 'Upload'}</button>
|
||||
</div>
|
||||
<p className="text-red-700 text-sm self-center">{imageUploadError && imageUploadError}</p>
|
||||
{
|
||||
formData.imageUrls.length > 0 && formData.imageUrls.map((url, index) => (
|
||||
<div key={url} className="flex justify-between p-2 border items-center">
|
||||
<img src={url} alt="" className="w-14 h-14 rounded-md object-contain"/>
|
||||
<button type="button" onClick={() => handleDeleteImage (index)} className="p-2 text-red-700 text-xs rounded-lg uppercase hover:opacity-80">Delete</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<button disabled={loading || upload} className="p-2 bg-blue-950 text-white text-sm font-semibold rounded-md uppercase hover:opacity-95 disabled:bg-opacity-80">{loading ? 'Updating...' : 'Update listing'}</button>
|
||||
{ error && <p className="text-red-700 text-sm">{error}</p>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateListing
|
|
@ -1,4 +1,5 @@
|
|||
import Listing from "../models/listing.model.js";
|
||||
import { errorHandler } from "../utils/error.js";
|
||||
|
||||
export const createListing = async (req, res, next) => {
|
||||
try {
|
||||
|
@ -7,4 +8,52 @@ export const createListing = async (req, res, next) => {
|
|||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteListing = async (req, res, next) => {
|
||||
const listing = await Listing.findById(req.params.id);
|
||||
if(!listing){
|
||||
return next(errorHandler(404, 'Listing not found'));
|
||||
}
|
||||
if (req.user.id !== listing.userRef){
|
||||
return next(errorHandler(401, 'You can only delete your own listings'));
|
||||
}
|
||||
try{
|
||||
await Listing.findByIdAndDelete(req.params.id);
|
||||
res.status(200).json('Listing has been deleted');
|
||||
} catch (error){
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateListing = async (req, res, next) => {
|
||||
const listing = await Listing.findById(req.params.id);
|
||||
if(!listing){
|
||||
return next(errorHandler(404, 'Listing not found'));
|
||||
}
|
||||
if(req.user.id !== listing.userRef){
|
||||
return next(errorHandler(401, 'You can only update your listings'));
|
||||
}
|
||||
try{
|
||||
const updatedListing = await Listing.findByIdAndUpdate(
|
||||
req.params.id,
|
||||
req.body,
|
||||
{new: true}
|
||||
);
|
||||
res.status(200).json(updatedListing);
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getListing = async (req, res, next) => {
|
||||
try {
|
||||
const listing = await Listing.findById(req.params.id);
|
||||
if(!listing){
|
||||
return next(errorHandler(404, 'Listing not found'));
|
||||
}
|
||||
res.status(200).json(listing);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
import express from 'express';
|
||||
import { createListing } from '../controllers/listing.controller.js';
|
||||
import { createListing, deleteListing, updateListing, getListing } from '../controllers/listing.controller.js';
|
||||
import { verifyToken } from '../utils/verifyUser.js';
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/create', verifyToken,createListing)
|
||||
router.post('/create', verifyToken, createListing)
|
||||
router.delete('/delete/:id', verifyToken, deleteListing)
|
||||
router.post('/update/:id', verifyToken, updateListing)
|
||||
router.get('/get/:id', getListing)
|
||||
|
||||
export default router;
|
Loading…
Add table
Reference in a new issue