diff --git a/backend/pms/src/main/resources/data.sql b/backend/pms/src/main/resources/data.sql index b2714e2..e17e333 100644 --- a/backend/pms/src/main/resources/data.sql +++ b/backend/pms/src/main/resources/data.sql @@ -87,7 +87,7 @@ VALUES ( 'House for Sell in Iowa', 'RENT', '2024-02-07 00:29:48.576972', - 3 + 2 ), ( 'Land', @@ -106,7 +106,7 @@ VALUES ( 'Great Building Site Right off Hwy 34', 'RENT', '2024-02-07 00:44:43.55343', - 3 + 2 ), ( 'House', @@ -123,7 +123,7 @@ VALUES ( '1,000', 'RENT', '2024-02-07 01:04:03.030894', - 3 + 2 ); diff --git a/frontend/src/components/Property.js b/frontend/src/components/Property.js index ede1599..56c1828 100644 --- a/frontend/src/components/Property.js +++ b/frontend/src/components/Property.js @@ -4,6 +4,7 @@ import { MdFavoriteBorder, MdOutlineFavorite } from "react-icons/md"; import { Link } from "react-router-dom"; import { formatMoney } from "../utils/money"; import { api } from "../libs/api"; +import { FiEdit2, FiEye } from "react-icons/fi"; const Property = ({ price, numberOfRoom, @@ -15,21 +16,23 @@ const Property = ({ favorite, refetch, viewOffer, + updateProperty, + unFav, }) => { const addFavorite = async (id) => { api.post(`favorites/${id}`).then((res) => { - console.log(res); + refetch(); setIsFav(true); }); }; const removeFavorite = (id) => { api.delete(`favorites/${id}`).then((res) => { - console.log(res); + refetch(); setIsFav(false); }); }; - const [isFav, setIsFav] = useState(favorite); + const [isFav, setIsFav] = useState(favorite || unFav); return ( @@ -40,41 +43,48 @@ const Property = ({ Location: {location} -
- - Rooms: {numberOfRoom} - {offerStatus} + Rooms: {numberOfRoom} + + {offerStatus} + - - - - { - viewOffer && ( +
- ) - } + {updateProperty && ( + + + + + )} + {viewOffer && ( + + )} + +
diff --git a/frontend/src/pages/Favorite.js b/frontend/src/pages/Favorite.js index ba97031..6b7ee49 100644 --- a/frontend/src/pages/Favorite.js +++ b/frontend/src/pages/Favorite.js @@ -15,7 +15,7 @@ const Favorite = () => { {data.map((property) => { return ( - + ) })} diff --git a/frontend/src/pages/MyProperty.js b/frontend/src/pages/MyProperty.js index f866661..0cdeecb 100644 --- a/frontend/src/pages/MyProperty.js +++ b/frontend/src/pages/MyProperty.js @@ -41,6 +41,7 @@ const MyProperty = () => { {...property} refetch={refetch} viewOffer={handleViewOffer} + updateProperty={true} /> ); diff --git a/frontend/src/pages/PropertyDetail.js b/frontend/src/pages/PropertyDetail.js index e321c3c..41c73e1 100644 --- a/frontend/src/pages/PropertyDetail.js +++ b/frontend/src/pages/PropertyDetail.js @@ -23,9 +23,7 @@ import { api } from "../libs/api"; const PropertyDetail = () => { const { id } = useParams(); - const { user } = useSelector((state) => state.auth); - const isOwner = user?.roles.map((role) => role.role).includes("Owner"); - console.log(isOwner); + const { data, isLoading, isError } = useQuery(`properties/${id}`); const navigate = useNavigate(); const [show, setShow] = React.useState(false); @@ -86,33 +84,27 @@ const PropertyDetail = () => {

{formatMoney(data.price)}

{data.location}

Rooms: {data.numberOfRoom}

- Status :{data.offerStatus} - -
- { - isOwner ? ( - - - - ): ( - - ) + Status : + + {data.offerStatus} + +
+
diff --git a/frontend/src/pages/UpdateProperty.js b/frontend/src/pages/UpdateProperty.js index a3f3043..bef237e 100644 --- a/frontend/src/pages/UpdateProperty.js +++ b/frontend/src/pages/UpdateProperty.js @@ -1,416 +1,426 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { - Button, - Card, - Col, - Form, - Image, - InputGroup, - Row, + Button, + Form, + Row, + Col, + Image, + Nav, + Card, + InputGroup, } from "react-bootstrap"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { - CATEGORY, - HOUSE_TYPES, - Map, - handleChangePropertyType, -} from "./AddProperty"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useSelector } from "react-redux"; -import { useMutation } from "react-query"; -import { api } from "../libs/api"; -import { useLocation, useNavigate } from "react-router-dom"; import { apiBaseUrl } from "../libs/constants"; +import { api } from "../libs/api"; +import { useMutation, useQuery } from "react-query"; +import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; +import { getCurrLocation, icon, iconURL } from "../utils/map"; +import { FaLocationDot } from "react-icons/fa6"; +import { useNavigate, useParams } from "react-router-dom"; +import Loading from "../components/Loading"; -function UpdateProperty() { - const [propertyType, setPropertyType] = React.useState(HOUSE_TYPES); - const { accessToken } = useSelector((state) => state.auth); - const [images, setImages] = React.useState({}); - const [existingImages, setExistingImages] = React.useState({}); - const [imageKeys, setImageKeys] = React.useState([]); - const [latLong, setLatLong] = React.useState(0); - const location = useLocation(); - const id = 1; - const navigate = useNavigate(); +export const CATEGORY = ["House", "Apartment", "Condo", "Land"]; +export const LAND_TYPES = ["Residential", "Commercial", "Agricultural"]; +export const HOUSE_TYPES = ["Single Family", "Multi Family", "Townhouse"]; +export const APARTMENT_TYPES = ["Studio", "Loft", "Duplex"]; +export const CONDO_TYPES = ["High Rise", "Low Rise", "Mid Rise"]; +export const handleChangePropertyType = (type, setPropertyType) => { + switch (type) { + case "House": + setPropertyType(HOUSE_TYPES); + break; + case "Apartment": + setPropertyType(APARTMENT_TYPES); + break; + case "Condo": + setPropertyType(CONDO_TYPES); + break; + case "Land": + setPropertyType(LAND_TYPES); + break; + default: + setPropertyType(HOUSE_TYPES); + break; + } +}; - const PropertySchema = z.object({ - type: z.string(), - title: z.string().min(3).max(150), - price: z.string().nullable(), - description: z.string().min(3).max(255), - location: z.string().min(3).max(255), - category: z.string().min(3).max(255), - subCategory: z.string().min(3).max(200), - numberOfRoom: z.string().nullable(), - }); +const UpdateProperty = () => { + const [propertyType, setPropertyType] = React.useState(HOUSE_TYPES); + const { accessToken } = useSelector((state) => state.auth); + const [images, setImages] = React.useState({}); + const [imageKeys, setImageKeys] = React.useState([]); + const [latLong, setLatLong] = React.useState([0, 0]); + const navigate = useNavigate(); + const { id } = useParams(); - const { - register, - handleSubmit, - formState: { errors }, - watch, - } = useForm({ - resolver: zodResolver(PropertySchema), - defaultValues: { type: "RENT" }, - }); + const PropertySchema = z.object({ + type: z.string(), + title: z.string().min(3).max(150), + price: z.string().nullable(), + description: z.string().min(3).max(255), + location: z.string().min(3).max(255), + category: z.string().min(3).max(255), + subCategory: z.string().min(3).max(200), + numberOfRoom: z.string().nullable(), + }); - const onSubmit = (data) => { - data.pictures = imageKeys; - data.latitude = latLong.lat || 0; - data.longitude = latLong.lng || 0; - propertyMutation.mutate(data); - }; + const { + register, + handleSubmit, + formState: { errors }, + watch, + setValue, + } = useForm({ + resolver: zodResolver(PropertySchema), + defaultValues: { type: "RENT" }, + }); + const { data, isLoading } = useQuery(`properties/${id}`); - const type = watch("type"); - const category = watch("category"); - const handleRemoveImage = (index) => { - setImages((prev) => { - const temp = { ...prev }; - delete temp[index]; - return temp; + useEffect(() => { + if (data) { + setValue("type", data.type); + setValue("title", data.title); + setValue("price", data.price); + setValue("description", data.description); + setValue("location", data.location); + setValue("category", data.category); + setValue("subCategory", data.subCategory); + setValue("numberOfRoom", data.numberOfRoom); + setLatLong([data.latitude, data.longitude]); + + setImageKeys(imageUrlToKey(data.pictures)); + setImages(data.pictures); + } + }, [data]); + + const imageUrlToKey = (images = []) => { + return images.map((img) => img.split("/")[6]); + }; + + const propertyMutation = useMutation((data) => { + api + .put(`properties/${id}`, data) + .then((res) => { + navigate(`/my-properties`, { + state: { message: "Property updated successfully" }, }); - }; + }) + .catch((error) => { + console.log(error); + }); + }); - const propertyMutation = useMutation((data) => { - api.put("properties/" + id, data) - .then((res) => { - navigate("/my-properties", { - state: { message: "Property added successfully" }, - }); - }) - .catch((error) => { - console.log(error); - }); + const onSubmit = (data) => { + data.pictures = imageKeys; + data.latitude = latLong.lat || 0; + data.longitude = latLong.lng || 0; + propertyMutation.mutate(data); + }; + const type = watch("type"); + const category = watch("category"); + const handleRemoveImage = (index) => { + setImages((prev) => { + const temp = { ...prev }; + delete temp[index]; + return temp; }); + }; - const [property, setProperty] = React.useState({}); - const propertyFetch = () => { - api.get("properties/" + id) - .then((res) => { - console.log(res.data); - setProperty(res.data); - handleChangePropertyType(res.data.type, setPropertyType); - // alert('ddd', res.data.latitude) - setLatLong({ lat: res.data.latitude, lng: res.data.longitude }); - // setExistingImages(res.data.pictures) - setImages(res.data.pictures.map((p) => p)); - }) - .catch((error) => { - console.log(error); - }); - }; - - useEffect(() => { - propertyFetch(); - }, []); - const propertyForm = useRef(property); + const handleUploadImage = (files) => { + setImages((prev) => { + const temp = { ...prev }; + for (let i = 0; i < files.length; i++) { + temp[i] = files[i]; + } + return temp; + }); - const handleChangeProperty = (e) => { - const { name, value } = e.target; - setProperty({ ...property, [name]: value }); - }; + for (let i = 0; i < files.length; i++) { + const formData = new FormData(); + formData.append("file", files[i]); - const handleUploadImage = (files) => { - setImages((prev) => { + fetch(apiBaseUrl + "files/upload", { + method: "POST", + body: formData, + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((response) => response.json()) + .then((result) => { + setImageKeys((prev) => { + return [...prev, result.key]; + }); + setImages((prev) => { const temp = { ...prev }; - for (let i = 0; i < files.length; i++) { - temp[i + images.length] = files[i]; - } + temp[i] = result; return temp; + }); + }) + .catch((error) => { + console.error("Error:", error); }); + } + }; - for (let i = 0; i < files.length; i++) { - const formData = new FormData(); - formData.append("file", files[i]); + useEffect(() => { + const geo = getCurrLocation(); + console.log(geo); + }, []); - fetch(apiBaseUrl + "files/upload", { - method: "POST", - body: formData, - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }) - .then((response) => response.json()) - .then((result) => { - setImageKeys((prev) => { - return [...prev, result.key]; - }); - }) - .catch((error) => { - console.error("Error:", error); - }); - } - }; + const imageArr = Object.keys(images).map((key) => images[key]); - const imageArr = Object.keys(images).map((key) => images[key]); + if (isLoading) ; - return ( -
- - -

Update Property

-
- -
- - - - Seller Type - - - - - - - Title - - handleChangeProperty(e) - } - /> - - {errors.title?.message} - - - - Number of Rooms - - handleChangeProperty(e) - } - /> - - {errors.numberOfRoom?.message} - - + return ( +
+ + +

Edit Property

+
+ + + + + + Seller Type + + + + + + + Title + + + {errors.title?.message} + + + + Number of Rooms + + + {errors.numberOfRoom?.message} + + - - Price - - - handleChangeProperty(e) - } - /> + + Price + + - - {type === "Rent" - ? "$ / month" - : "$"} - - - - {errors.price?.message} - - - - - - Description - - - {errors.description?.message} - - - - - - - - category - - - {errors.category?.message} - - - - - - {category} Type - - - {errors.subCategory?.message} - - - - + + {type === "Rent" ? "$ / month" : "$"} + + + + {errors.price?.message} + + + + + + Description + + + {errors.description?.message} + + + + + + + + category + + + {errors.category?.message} + + + + + + {category} Type + + + {errors.subCategory?.message} + + + + - - location - - - {errors.location?.message} - - - - Image - - handleUploadImage(e.target.files) - } - /> + + location + + + {errors.location?.message} + + + + Image + handleUploadImage(e.target.files)} + /> - - {imageArr?.map((img, index) => { - return ( - - -

{img.name}

- - - ); - })} -
- - - {/* */} - - -
- - -
-
-
- ); -} + + {imageArr?.map((img, index) => { + return ( + + +

{img.name}

+ + + ); + })} +
+ + + + + + + + +
+
+
+ ); +}; export default UpdateProperty; + +export const Map = ({ set, center = defaultCenter }) => { + const [map, setMap] = useState(null); + const [latLong, setLatLong] = useState(null); + + useEffect(() => { + set(latLong); + }, [latLong]); + + const displayMap = useMemo( + () => ( +
+
+ +
+ + + +
+ ), + [] + ); + + return ( +
+ {map ? : null} + {displayMap} +
+ ); +}; + +const defaultCenter = [41.023248, -91.966827]; +const zoom = 15; + +function DisplayPosition({ map, setLatLong }) { + const [position, setPosition] = useState(() => map.getCenter()); + + const onClick = useCallback(() => { + map.setView(defaultCenter, zoom); + }, [map]); + + const onMove = useCallback(() => { + setPosition(map.getCenter()); + setLatLong(map.getCenter()); + }, [map]); + + useEffect(() => { + map.on("move", onMove); + return () => { + map.off("move", onMove); + }; + }, [map, onMove]); + + return ( +

+ latitude: {position.lat.toFixed(4)}, longitude: {position.lng.toFixed(4)}{" "} + +

+ ); +} diff --git a/frontend/src/routes/Router.js b/frontend/src/routes/Router.js index 048b768..50a79e9 100644 --- a/frontend/src/routes/Router.js +++ b/frontend/src/routes/Router.js @@ -30,7 +30,7 @@ const Router = () => { } /> } /> } /> - } /> + } /> } /> } /> } />