stay_commerce-frontend 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/bundle.css +8665 -6069
  3. data/app/assets/builds/bundle.css.map +3 -3
  4. data/app/assets/builds/bundle.js +37877 -27557
  5. data/app/assets/builds/bundle.js.map +4 -4
  6. data/app/assets/builds/styles.css +6328 -5031
  7. data/app/assets/builds/styles.css.map +3 -3
  8. data/app/javascript/react/components/Accountpage/AccountInfo.jsx +2 -1
  9. data/app/javascript/react/components/AddNewProperty/CommonLayout.jsx +0 -2
  10. data/app/javascript/react/components/AddNewProperty/Description.jsx +51 -52
  11. data/app/javascript/react/components/AddNewProperty/Details.jsx +212 -129
  12. data/app/javascript/react/components/AddNewProperty/Images.jsx +122 -45
  13. data/app/javascript/react/components/AddNewProperty/Location.jsx +21 -14
  14. data/app/javascript/react/components/AddNewProperty/Room.jsx +639 -548
  15. data/app/javascript/react/components/AvatarDropdown/AvatarDropDown.jsx +9 -1
  16. data/app/javascript/react/components/FacilitiesSection/Facilities.jsx +18 -0
  17. data/app/javascript/react/components/FixedNavbar/FixedNav.jsx +20 -14
  18. data/app/javascript/react/components/HeroSectionDesign/BookingForm.jsx +136 -88
  19. data/app/javascript/react/components/HeroSectionDesign/MyPropertiesListing.jsx +79 -69
  20. data/app/javascript/react/components/Layout/Layout.js +8 -1
  21. data/app/javascript/react/components/Listing-stay-Detail/ApartmentCard.jsx +3 -3
  22. data/app/javascript/react/components/Listing-stay-Detail/BookingModal.jsx +167 -122
  23. data/app/javascript/react/components/Listing-stay-Detail/CardManager.jsx +285 -0
  24. data/app/javascript/react/components/Listing-stay-Detail/CheckoutForm.jsx +147 -84
  25. data/app/javascript/react/components/Listing-stay-Detail/ListingStayDetailPage.jsx +1 -7
  26. data/app/javascript/react/components/Listing-stay-Detail/PropertiesPage.jsx +464 -0
  27. data/app/javascript/react/components/MobileNav/MobileMenu.jsx +1 -4
  28. data/app/javascript/react/components/PropertyListing/MyProperties.jsx +45 -44
  29. data/app/javascript/react/components/PropertyListing/StayBooking/BookingDetails.jsx +4 -4
  30. data/app/javascript/react/components/PropertyListing/StayBooking/MyBooking.jsx +41 -29
  31. data/app/javascript/react/components/StayCard/StayCard.jsx +5 -3
  32. data/app/javascript/react/packs/index.jsx +1 -0
  33. data/app/javascript/react/packs/routes/Route.jsx +18 -1
  34. data/app/javascript/react/pages/Home.jsx +6 -4
  35. data/app/javascript/react/redux/slices/PropertySlice/PropertySlice.jsx +21 -21
  36. data/app/javascript/react/redux/slices/PropertySlice/Searchslice.jsx +53 -6
  37. data/app/javascript/react/redux/slices/UserSlice/UserSlice.jsx +1 -0
  38. data/app/javascript/react/shared/Avatar/Avatar.jsx +5 -8
  39. data/app/javascript/react/shared/Button/SecondryButton.jsx +9 -0
  40. data/app/javascript/react/shared/Loader.jsx +13 -0
  41. data/app/javascript/react/shared/Pagination.jsx +53 -0
  42. data/app/javascript/react/shared/Schema/FormSchema +143 -0
  43. data/app/javascript/react/styles/BookingDetails.scss +1 -0
  44. data/app/javascript/react/styles/CardManager.scss +608 -0
  45. data/app/javascript/react/styles/Loader.scss +30 -0
  46. data/app/javascript/react/styles/Pagination.scss +33 -0
  47. data/app/javascript/react/styles/PropertiesPage.scss +0 -4
  48. data/app/javascript/react/styles/RenderSection.scss +1 -0
  49. data/app/javascript/react/styles/accountpage.scss +3 -0
  50. data/app/javascript/react/styles/application.scss +13 -1
  51. data/app/javascript/react/styles/bookingform.scss +56 -28
  52. data/app/javascript/react/styles/buttonSecondry.scss +24 -0
  53. data/app/javascript/react/styles/checkbox.scss +34 -35
  54. data/app/javascript/react/styles/commonlayout.scss +7 -2
  55. data/app/javascript/react/styles/commonpage.scss +5 -1
  56. data/app/javascript/react/styles/description.scss +3 -0
  57. data/app/javascript/react/styles/facilities.scss +2 -1
  58. data/app/javascript/react/styles/fixednavbar.scss +8 -0
  59. data/app/javascript/react/styles/listingstaydetailpage.scss +5 -0
  60. data/app/javascript/react/styles/mobilemenu.scss +0 -1
  61. data/app/javascript/react/styles/mybooking.scss +20 -0
  62. data/app/javascript/react/styles/myproperty.scss +26 -0
  63. data/app/javascript/react/styles/propertydetailscard.scss +265 -267
  64. data/app/javascript/react/styles/react-datepicker/react-datepicker.css +869 -0
  65. data/app/javascript/react/styles/room.scss +13 -8
  66. data/app/javascript/react/utils/helpers/ToastErros.js +12 -0
  67. data/db/migrate/20250627101451_add_role_to_stay_users.rb +5 -0
  68. data/lib/stay_commerce/frontend/version.rb +1 -1
  69. metadata +15 -5
  70. data/app/javascript/react/components/HeroSectionDesign/PropertiesPage.jsx +0 -122
  71. data/app/javascript/react/shared/DateField/CustomDatePicker.jsx +0 -69
  72. data/app/javascript/react/styles/customdatepicker.scss +0 -120
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import "../../../styles/mybooking.scss";
3
3
  import StayCard from "../../StayCard/StayCard";
4
4
  import {
@@ -7,6 +7,8 @@ import {
7
7
  } from "../../../redux/slices/PropertySlice/PropertySlice";
8
8
  import { useDispatch, useSelector } from "react-redux";
9
9
  import { useNavigate } from "react-router-dom";
10
+ import Pagination from "../../../shared/Pagination";
11
+ import Loader from "../../../shared/Loader";
10
12
 
11
13
  const MyBooking = () => {
12
14
  const dispatch = useDispatch();
@@ -14,35 +16,32 @@ const MyBooking = () => {
14
16
  const [bookingData, setBookingData] = useState(null);
15
17
  const [query, setQuery] = useState("");
16
18
  const [isLoading, setIsLoading] = useState(false);
19
+ const [currentPage, setCurrentPage] = useState(1);
17
20
  const { data: currentUser } = useSelector((state) => state.user);
18
21
 
19
- const fetchBookings = useCallback(
20
- async (page) => {
22
+ useEffect(() => {
23
+ const fetchBookings = async (page) => {
24
+ if (!currentUser) return;
25
+
21
26
  setIsLoading(true);
22
27
  try {
23
- if (currentUser?.is_host) {
24
- const response = await dispatch(
25
- getHostBookingsDetails({ page })
26
- ).unwrap();
27
- setBookingData(response);
28
- } else {
29
- const response = await dispatch(
30
- getUserBookingsDetails({ page })
31
- ).unwrap();
32
- setBookingData(response);
33
- }
28
+ const response = await dispatch(
29
+ currentUser?.is_host
30
+ ? getHostBookingsDetails({ page })
31
+ : getUserBookingsDetails({ page })
32
+ ).unwrap();
33
+ setBookingData(response);
34
34
  } catch (error) {
35
35
  console.error("Error fetching bookings:", error);
36
36
  } finally {
37
37
  setIsLoading(false);
38
38
  }
39
- },
40
- [dispatch, currentUser]
41
- );
39
+ };
42
40
 
43
- useEffect(() => {
44
- fetchBookings(1);
45
- }, [fetchBookings]);
41
+ if (currentUser) {
42
+ fetchBookings(currentPage);
43
+ }
44
+ }, [dispatch, currentUser, currentPage]);
46
45
 
47
46
  return (
48
47
  <div className="my-booking-container">
@@ -52,6 +51,7 @@ const MyBooking = () => {
52
51
  View the complete list of all your booking requests.
53
52
  </p>
54
53
 
54
+ {/* Optional Search Bar */}
55
55
  {/* <div className="search-bar">
56
56
  <FormField
57
57
  type="text"
@@ -63,16 +63,28 @@ const MyBooking = () => {
63
63
  <ButtonPrimary className="search-button">Search</ButtonPrimary>
64
64
  </div> */}
65
65
 
66
- {/* Stay Cards Grid */}
67
- <div className="stay-cards">
68
- {isLoading ? (
69
- <div className="loader">Loading bookings...</div>
70
- ) : bookingData?.bookings?.length > 0 ? (
71
- bookingData.bookings.map((booking) => (
66
+ {/* Loader or Bookings List */}
67
+ {isLoading ? (
68
+ <Loader text="Loading your bookings..." />
69
+ ) : bookingData?.bookings?.length > 0 ? (
70
+ <div className="stay-cards">
71
+ {bookingData.bookings.map((booking) => (
72
72
  <StayCard key={booking.id} booking={booking} />
73
- ))
74
- ) : (
75
- <p className="no-results">No bookings found.</p>
73
+ ))}
74
+ </div>
75
+ ) : (
76
+ <p className="no-property-message">No bookings found.</p>
77
+ )}
78
+
79
+ {/* Pagination */}
80
+ <div className="pagination_header">
81
+ {bookingData?.meta?.total_pages > 1 && (
82
+ <Pagination
83
+ currentPage={bookingData.meta.current_page}
84
+ totalPages={bookingData.meta.total_pages}
85
+ onPageChange={setCurrentPage}
86
+ isLoading={isLoading}
87
+ />
76
88
  )}
77
89
  </div>
78
90
  </div>
@@ -21,8 +21,10 @@ const StayCard = ({ booking }) => {
21
21
  number_of_guests,
22
22
  total_amount,
23
23
  id,
24
+ room_images,
25
+ booked_room,
24
26
  } = booking;
25
- const { place_images, title } = property;
27
+ const { title } = property;
26
28
 
27
29
  const handleCardClick = () => {
28
30
  navigate(`/bookingDetails/${id}`);
@@ -34,10 +36,10 @@ const StayCard = ({ booking }) => {
34
36
  onClick={handleCardClick}
35
37
  style={{ cursor: "pointer" }}
36
38
  >
37
- <GallerySlider images={place_images} style={{ height: "300px" }} />
39
+ <GallerySlider images={room_images} style={{ height: "300px" }} />
38
40
 
39
41
  <div className="stay-card__content">
40
- <h2 className="stay-card__title">{title}</h2>
42
+ <h2 className="stay-card__title">{booked_room}</h2>
41
43
  {currentUser?.is_host && (
42
44
  <div className="stay-card__info">
43
45
  <span>Request by:</span>{" "}
@@ -13,6 +13,7 @@ import "@mantine/core/styles.css";
13
13
  import "@mantine/dates/styles.css";
14
14
  import { Elements } from "@stripe/react-stripe-js";
15
15
  import { loadStripe } from "@stripe/stripe-js";
16
+ import "react-datepicker/dist/react-datepicker.css";
16
17
  const stripePromise = loadStripe(
17
18
  "pk_test_51LCfAAJQIRLPDLbLcKwKmAbKIHjTiJYCUsWXxfbQ0UlF5N3AO2tfLgKXt4GPI7UZYJUVHi94Q4TEol5YN1PQD4AI00N1uBrnnR"
18
19
  );
@@ -24,8 +24,9 @@ import ForgetPassword from "../../components/ForgetPassword/ForgetPassword";
24
24
  import ResetPassword from "../../components/ResetPassword/ResetPassword";
25
25
  import ListingStayDetailPage from "../../components/Listing-stay-Detail/ListingStayDetailPage";
26
26
  import ApartmentCard from "../../components/Listing-stay-Detail/ApartmentCard";
27
- import PropertiesPage from "../../components/HeroSectionDesign/PropertiesPage";
28
27
  import BookingInterface from "../../components/PropertyListing/StayBooking/BookingDetails";
28
+ import CardManager from "../../components/Listing-stay-Detail/CardManager";
29
+ import PropertiesPage from "../../components/Listing-stay-Detail/PropertiesPage";
29
30
 
30
31
  const RequireHost = ({ children }) => {
31
32
  const user = useSelector((state) => state.user?.data);
@@ -35,6 +36,14 @@ const RequireHost = ({ children }) => {
35
36
  return children;
36
37
  };
37
38
 
39
+ const RequireUser = ({ children }) => {
40
+ const user = useSelector((state) => state.user?.data);
41
+ if (!user?.is_user) {
42
+ return <Navigate to="/unauthorized" />;
43
+ }
44
+ return children;
45
+ };
46
+
38
47
  const router = createBrowserRouter([
39
48
  {
40
49
  path: "/",
@@ -156,6 +165,14 @@ const router = createBrowserRouter([
156
165
  path: "*",
157
166
  element: <Page404 />,
158
167
  },
168
+ {
169
+ path: "/AddCard",
170
+ element: (
171
+ <RequireUser>
172
+ <CardManager />
173
+ </RequireUser>
174
+ ),
175
+ },
159
176
  ],
160
177
  },
161
178
  ]);
@@ -15,12 +15,14 @@ function Home() {
15
15
  <div>
16
16
  {/* Page Content */}
17
17
 
18
- <HeroSection />
19
- {/* <div className="container">
18
+ <div style={{ paddingLeft: "20px", paddingRight: "20px" }}>
19
+ <HeroSection />
20
+ </div>
21
+ <div className="container">
20
22
  <BookingForm />
21
- </div> */}
23
+ </div>
22
24
  </div>
23
- <Listing />
25
+ {/* <Listing /> */}
24
26
  <FeaturesSection />
25
27
  <AboutUs />
26
28
  <TestimonialSection />
@@ -194,27 +194,27 @@ export const getCard = createAsyncThunk(
194
194
  }
195
195
  );
196
196
 
197
- export const searchProperties = createAsyncThunk(
198
- "properties/searchProperties",
199
- async (searchQuery, { rejectWithValue }) => {
200
- try {
201
- const response = await API.get(ENDPOINTS.SEARCH_PROPERTIES(searchQuery));
202
- return {
203
- success: true,
204
- data: response.data,
205
- };
206
- } catch (error) {
207
- if (error.response && error.response.status === 404) {
208
- toast.error(error.response.data.error || error.response.data.message, {
209
- position: "top-right",
210
- });
211
- return rejectWithValue(error.response.data);
212
- }
213
- ErrorHandler(error);
214
- return rejectWithValue(error.message);
215
- }
216
- }
217
- );
197
+ // export const searchProperties = createAsyncThunk(
198
+ // "properties/searchProperties",
199
+ // async (searchQuery, { rejectWithValue }) => {
200
+ // try {
201
+ // const response = await API.get(ENDPOINTS.SEARCH_PROPERTIES(searchQuery));
202
+ // return {
203
+ // success: true,
204
+ // data: response.data,
205
+ // };
206
+ // } catch (error) {
207
+ // if (error.response && error.response.status === 404) {
208
+ // toast.error(error.response.data.error || error.response.data.message, {
209
+ // position: "top-right",
210
+ // });
211
+ // return rejectWithValue(error.response.data);
212
+ // }
213
+ // ErrorHandler(error);
214
+ // return rejectWithValue(error.message);
215
+ // }
216
+ // }
217
+ // );
218
218
 
219
219
  export const getPaymentmethods = createAsyncThunk(
220
220
  "payment/getCard",
@@ -1,11 +1,37 @@
1
- import { createSlice } from "@reduxjs/toolkit";
2
- import { searchProperties } from "./PropertySlice";
1
+ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2
+ import API from "../../../utils/helpers/APIHelper";
3
+ import { ENDPOINTS } from "../../../Api/apiConstants";
4
+ import ErrorHandler from "../../../utils/helpers/ErrorHandler";
5
+ import { toast } from "react-toastify";
6
+
7
+ export const searchProperties = createAsyncThunk(
8
+ "search/searchProperties",
9
+ async (searchQuery, { rejectWithValue }) => {
10
+ try {
11
+ const response = await API.get(ENDPOINTS.SEARCH_PROPERTIES(searchQuery));
12
+ return response;
13
+ } catch (error) {
14
+ if (error.response && error.response.status === 404) {
15
+ toast.error(error.response.data.message || "Not found");
16
+ return rejectWithValue(error.response.data);
17
+ }
18
+ ErrorHandler(error);
19
+ return rejectWithValue(error.message);
20
+ }
21
+ }
22
+ );
3
23
 
4
24
  const initialState = {
5
25
  searchResults: [],
6
26
  startDate: null,
7
27
  endDate: null,
8
28
  totalGuests: 1,
29
+ address: null,
30
+ latitude: null,
31
+ longitude: null,
32
+ loading: false,
33
+ error: null,
34
+ data: null,
9
35
  };
10
36
 
11
37
  const searchSlice = createSlice({
@@ -17,18 +43,39 @@ const searchSlice = createSlice({
17
43
  state.startDate = null;
18
44
  state.endDate = null;
19
45
  state.totalGuests = 1;
46
+ state.address = null;
47
+ state.latitude = null;
48
+ state.longitude = null;
49
+ state.error = null;
50
+ state.data = null;
20
51
  },
21
52
  setSearchMeta: (state, action) => {
22
- const { startDate, endDate, totalGuests } = action.payload;
53
+ const { startDate, endDate, totalGuests, address, latitude, longitude } =
54
+ action.payload;
23
55
  state.startDate = startDate;
24
56
  state.endDate = endDate;
25
57
  state.totalGuests = totalGuests;
58
+ state.address = address;
59
+ state.latitude = latitude;
60
+ state.longitude = longitude;
26
61
  },
27
62
  },
28
63
  extraReducers: (builder) => {
29
- builder.addCase(searchProperties.fulfilled, (state, action) => {
30
- state.searchResults = action.payload.data || [];
31
- });
64
+ builder
65
+ .addCase(searchProperties.pending, (state) => {
66
+ state.loading = true;
67
+ state.error = null;
68
+ })
69
+ .addCase(searchProperties.fulfilled, (state, action) => {
70
+ state.searchResults = action.payload?.properties || [];
71
+ state.data = action.payload;
72
+ state.loading = false;
73
+ state.error = null;
74
+ })
75
+ .addCase(searchProperties.rejected, (state, action) => {
76
+ state.loading = false;
77
+ state.error = action.payload || "Failed to search properties";
78
+ });
32
79
  },
33
80
  });
34
81
 
@@ -2,6 +2,7 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2
2
  import axios from "axios";
3
3
  import { ENDPOINTS } from "../../../Api/apiConstants";
4
4
  import ErrorHandler from "../../../utils/helpers/ErrorHandler";
5
+
5
6
  export const getCurrentUser = createAsyncThunk(
6
7
  "user/getCurrentUser",
7
8
  async (_, { rejectWithValue }) => {
@@ -1,14 +1,11 @@
1
- import React from "react";
1
+ import React, { useEffect } from "react";
2
2
  import { useSelector } from "react-redux";
3
3
  import "../../styles/avatar.scss";
4
4
 
5
- const Avatar = ({ onClick, sizeClass = "" }) => {
6
- const userData = useSelector((state) => state.user.data);
7
-
5
+ const Avatar = ({ onClick, sizeClass = "", userData }) => {
8
6
  if (!userData) return null;
9
-
10
- const profileImage = userData.image;
11
- const firstLetter = userData.first_name?.charAt(0).toUpperCase() || "?";
7
+ const profileImage = userData?.image;
8
+ const firstLetter = userData?.first_name?.charAt(0).toUpperCase() || "?";
12
9
 
13
10
  return (
14
11
  <div className={`avatar-container ${sizeClass}`} onClick={onClick}>
@@ -19,7 +16,7 @@ const Avatar = ({ onClick, sizeClass = "" }) => {
19
16
  className="avatar-image"
20
17
  onError={(e) => {
21
18
  e.target.onerror = null;
22
- // e.target.src = "/default-avatar.png"; // optional fallback image
19
+ // e.target.src = "/default-avatar.png";
23
20
  }}
24
21
  />
25
22
  ) : (
@@ -0,0 +1,9 @@
1
+ import Button from "./Button";
2
+ import React from "react";
3
+ import "../../styles/buttonSecondry.scss"; // Import SCSS for styling
4
+
5
+ const SecondryButton = ({ className = "", ...args }) => {
6
+ return <Button className={`sec-primary ${className}`} {...args} />;
7
+ };
8
+
9
+ export default SecondryButton;
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import "../styles/Loader.scss"; // Adjust the path as necessary
3
+
4
+ const Loader = ({ text = "Loading..." }) => {
5
+ return (
6
+ <div className="app-loader">
7
+ <div className="app-loader__spinner" />
8
+ <p className="app-loader__text">{text}</p>
9
+ </div>
10
+ );
11
+ };
12
+
13
+ export default Loader;
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import "../styles/Pagination.scss"; // Adjust the path as necessary
3
+
4
+ const Pagination = ({
5
+ currentPage,
6
+ totalPages,
7
+ onPageChange,
8
+ isLoading = false,
9
+ }) => {
10
+ const renderPageNumbers = () => {
11
+ const pages = [];
12
+
13
+ for (let i = 1; i <= totalPages; i++) {
14
+ pages.push(
15
+ <button
16
+ key={i}
17
+ className={`pagination__button ${
18
+ i === currentPage ? "pagination__button--active" : ""
19
+ }`}
20
+ onClick={() => onPageChange(i)}
21
+ >
22
+ {i}
23
+ </button>
24
+ );
25
+ }
26
+
27
+ return pages;
28
+ };
29
+
30
+ return (
31
+ <div className="pagination">
32
+ <button
33
+ className="pagination__button"
34
+ onClick={() => onPageChange(currentPage - 1)}
35
+ disabled={currentPage === 1 || isLoading}
36
+ >
37
+ Prev
38
+ </button>
39
+
40
+ {renderPageNumbers()}
41
+
42
+ <button
43
+ className="pagination__button"
44
+ onClick={() => onPageChange(currentPage + 1)}
45
+ disabled={currentPage === totalPages}
46
+ >
47
+ Next
48
+ </button>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export default Pagination;
@@ -0,0 +1,143 @@
1
+ import * as Yup from "yup";
2
+
3
+ export const descriptionValidationSchemas = Yup.object().shape({
4
+ title: Yup.string()
5
+ .required("Title is required")
6
+ .max(100, "Title cannot exceed 100 characters"),
7
+ property_category: Yup.string().required("Property category is required"),
8
+ property_type_id: Yup.string().required("Property type is required"),
9
+ country: Yup.string().required("Country is required"),
10
+ description: Yup.string()
11
+ .required("Description is required")
12
+ .min(200, "Description must be at least 200 characters long"),
13
+ address: Yup.string().required("Address is required"),
14
+ });
15
+
16
+ export const ImagevalidationSchema = Yup.object({
17
+ place_images: Yup.array()
18
+ .min(1, "Please upload at least one image")
19
+ .required("Images are required"),
20
+ });
21
+
22
+ // Validation schema
23
+ export const detailsvalidationSchema = Yup.object({
24
+ property_size: Yup.number()
25
+ .required("Property size is required")
26
+ .positive("Property size must be a positive number")
27
+ .integer("Property size must be a whole number"),
28
+ total_rooms: Yup.number()
29
+ .required("Total rooms is required")
30
+ .positive("Total rooms must be a positive number")
31
+ .integer("Total rooms must be a whole number")
32
+ .max(50, "Total rooms cannot exceed 50"),
33
+ additionalRules: Yup.array().of(
34
+ Yup.object({
35
+ name: Yup.string().required("Rule name is required"),
36
+ })
37
+ ),
38
+ });
39
+
40
+ export const LocationSchema = Yup.object().shape({
41
+ city: Yup.string().required("City is required"),
42
+ state: Yup.string().required("State is required"),
43
+ zipcode: Yup.string().required("Zipcode is required"),
44
+ latitude: Yup.number()
45
+ .typeError("Latitude must be a number")
46
+ .required("Latitude is required"),
47
+ longitude: Yup.number()
48
+ .typeError("Longitude must be a number")
49
+ .required("Longitude is required"),
50
+ });
51
+
52
+ export const roomValidationSchema = Yup.object().shape({
53
+ rooms_attributes: Yup.array().of(
54
+ Yup.object().shape({
55
+ name: Yup.string()
56
+ .required("Room name is required")
57
+ .min(2, "Room name must be at least 2 characters")
58
+ .max(100, "Room name must not exceed 100 characters"),
59
+ price_per_month: Yup.number()
60
+ .required("Price per month is required")
61
+ .positive("Price must be positive")
62
+ .min(1, "Price must be at least 1"),
63
+ status: Yup.string()
64
+ .required("Status is required")
65
+ .oneOf(
66
+ ["active", "inactive"],
67
+ "Status must be either active or inactive"
68
+ ),
69
+ booking_start: Yup.date().required("Availability start date is required"),
70
+ booking_end: Yup.date()
71
+ .required("Availability end date is required")
72
+ .min(Yup.ref("booking_start"), "End date must be after start date"),
73
+ description: Yup.string()
74
+ .required("Description is required")
75
+ .min(200, "Description must be at least 200 characters"),
76
+ bed_type_id: Yup.string().required("Bed type is required"),
77
+ size: Yup.number()
78
+ .required("Room size is required")
79
+ .positive("Size must be positive")
80
+ .min(1, "Size must be at least 1 sq ft"),
81
+ room_type_id: Yup.string().required("Room type is required"),
82
+ max_guests: Yup.number()
83
+ .required("Max guests is required")
84
+ .positive("Max guests must be positive")
85
+ .integer("Max guests must be a whole number")
86
+ .min(1, "At least 1 guest must be allowed"),
87
+ room_images: Yup.array()
88
+ .test(
89
+ "room-images-required",
90
+ "At least one room image is required",
91
+ function (value) {
92
+ const { existing_room_images } = this.parent;
93
+ const totalImages =
94
+ (value?.length || 0) + (existing_room_images?.length || 0);
95
+ return totalImages >= 1;
96
+ }
97
+ )
98
+ .test(
99
+ "max-images",
100
+ "Maximum 10 images are allowed per room",
101
+ function (value) {
102
+ const { existing_room_images } = this.parent;
103
+ const totalImages =
104
+ (value?.length || 0) + (existing_room_images?.length || 0);
105
+ return totalImages <= 10;
106
+ }
107
+ )
108
+ .test(
109
+ "file-size",
110
+ "Each image must be less than 10MB",
111
+ function (value) {
112
+ if (!value || value.length === 0) return true;
113
+ return value.every((file) => {
114
+ if (file instanceof File) {
115
+ return file.size <= 10 * 1024 * 1024; // 10MB
116
+ }
117
+ return true;
118
+ });
119
+ }
120
+ )
121
+ .test(
122
+ "file-type",
123
+ "Only PNG, JPG, JPEG, and GIF files are allowed",
124
+ function (value) {
125
+ if (!value || value.length === 0) return true;
126
+ const allowedTypes = [
127
+ "image/png",
128
+ "image/jpg",
129
+ "image/jpeg",
130
+ "image/gif",
131
+ ];
132
+ return value.every((file) => {
133
+ if (file instanceof File) {
134
+ return allowedTypes.includes(file.type);
135
+ }
136
+ return true;
137
+ });
138
+ }
139
+ ),
140
+ existing_room_images: Yup.array(),
141
+ })
142
+ ),
143
+ });
@@ -92,6 +92,7 @@
92
92
  background: #081976;
93
93
  color: white;
94
94
  width: fit-content;
95
+ border-radius: 6px;
95
96
  }
96
97
 
97
98
  .icon {