diff --git a/app/components/property/DateSelectionCalendar.js b/app/components/property/DateSelectionCalendar.js new file mode 100644 index 0000000..fd8bea0 --- /dev/null +++ b/app/components/property/DateSelectionCalendar.js @@ -0,0 +1,246 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { ChevronRight, ChevronLeft } from "lucide-react"; + +const MONTH_KEYS = [ + "month.january", + "month.february", + "month.march", + "month.april", + "month.may", + "month.june", + "month.july", + "month.august", + "month.september", + "month.october", + "month.november", + "month.december", +]; + +const DAY_KEYS = [ + "dayAbbr.sun", + "dayAbbr.mon", + "dayAbbr.tue", + "dayAbbr.wed", + "dayAbbr.thu", + "dayAbbr.fri", + "dayAbbr.sat", +]; + +export default function DateSelectionCalendar({ + mode, + availableDates, + selectedStart, + selectedEnd, + onSelectStart, + onSelectEnd, +}) { + const { t } = useTranslation(); + const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth()); + const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear()); + const [rangeError, setRangeError] = useState(null); + + const isPastDate = useCallback((dateStr) => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return new Date(dateStr) < today; + }, []); + + const isDateAvailable = useCallback( + (dateStr) => (availableDates ? availableDates.has(dateStr) : true), + [availableDates] + ); + + const isMonthFullyAvailable = useCallback( + (year, monthIdx) => { + const daysInMonth = new Date(year, monthIdx + 1, 0).getDate(); + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + if (isPastDate(dateStr) || !isDateAvailable(dateStr)) return false; + } + return true; + }, + [isPastDate, isDateAvailable] + ); + + const isPastMonth = useCallback((year, monthIdx) => { + const now = new Date(); + return ( + year < now.getFullYear() || + (year === now.getFullYear() && monthIdx < now.getMonth()) + ); + }, []); + + const handleDayClick = (dateStr) => { + setRangeError(null); + if (!selectedStart || selectedEnd) { + onSelectStart(dateStr); + onSelectEnd(null); + return; + } + if (new Date(dateStr) <= new Date(selectedStart)) { + onSelectStart(dateStr); + onSelectEnd(null); + return; + } + const start = new Date(selectedStart); + const end = new Date(dateStr); + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const ds = d.toISOString().split("T")[0]; + if (!isDateAvailable(ds)) { + setRangeError(t("unavailableInRange", { defaultValue: "توجد أيام غير متاحة ضمن الفترة المحددة" })); + return; + } + } + onSelectEnd(dateStr); + }; + + const handleMonthClick = (year, monthIdx) => { + setRangeError(null); + const monthStr = `${year}-${String(monthIdx + 1).padStart(2, "0")}`; + const firstDay = `${monthStr}-01`; + if (!selectedStart || selectedEnd || monthStr <= (selectedStart || "").substring(0, 7)) { + onSelectStart(firstDay); + onSelectEnd(null); + } else { + const lastDay = new Date(year, monthIdx + 1, 0).getDate(); + onSelectEnd(`${monthStr}-${String(lastDay).padStart(2, "0")}`); + } + }; + + const navigateMonth = (delta) => { + let month = calendarMonth + delta; + let year = calendarYear; + if (month < 0) { + month = 11; + year--; + } + if (month > 11) { + month = 0; + year++; + } + setCalendarMonth(month); + setCalendarYear(year); + }; + + const isEntryStep = !selectedStart || !!selectedEnd; + + return ( +
+ {/* Step Indicator */} +
+
+
+ {t("selectStartDate")} +
+
+
+
+ {t("selectEndDate")} +
+
+ + {mode === "daily" ? ( +
+
+ + + {t(MONTH_KEYS[calendarMonth])} {calendarYear} + + +
+ +
+ {DAY_KEYS.map((dk, i) => ( +
+ {t(dk)} +
+ ))} +
+ + {(() => { + const firstDay = new Date(calendarYear, calendarMonth, 1).getDay(); + const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate(); + const cells = []; + for (let i = 0; i < firstDay; i++) { + cells.push(
); + } + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const past = isPastDate(dateStr); + const available = isDateAvailable(dateStr); + const isSelStart = dateStr === selectedStart; + const isSelEnd = dateStr === selectedEnd; + const inRange = + selectedStart && + selectedEnd && + new Date(dateStr) > new Date(selectedStart) && + new Date(dateStr) < new Date(selectedEnd); + const disabled = past || !available; + cells.push( + + ); + } + return
{cells}
; + })()} +
+ ) : ( +
+
+ + {calendarYear} + +
+
+ {MONTH_KEYS.map((mk, idx) => { + const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`; + const isSelStart = selectedStart && selectedStart.startsWith(monthStr); + const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr); + const inRange = + selectedStart && + selectedEnd && + monthStr > selectedStart.substring(0, 7) && + monthStr < selectedEnd.substring(0, 7); + const disabled = isPastMonth(calendarYear, idx) || !isMonthFullyAvailable(calendarYear, idx); + return ( + + ); + })} +
+
+ )} + + {rangeError && ( +
+ {rangeError} +
+ )} +
+ ); +} diff --git a/app/property/[id]/PropertyDetail.js b/app/property/[id]/PropertyDetail.js index 4ece20a..d60c2cf 100644 --- a/app/property/[id]/PropertyDetail.js +++ b/app/property/[id]/PropertyDetail.js @@ -60,6 +60,7 @@ import AuthService from "../../services/AuthService"; import { useFavorites } from "@/app/contexts/FavoritesContext"; import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from "../../enums"; import PropertyRatingList from "@/app/components/ratings/PropertyRatingList"; +import DateSelectionCalendar from "@/app/components/property/DateSelectionCalendar"; import { getPropertyAverageRating } from "../../utils/ratings"; import "leaflet/dist/leaflet.css"; import Loading from "@/app/loading"; @@ -257,11 +258,8 @@ export default function PropertyDetailsPage() { const [bookingError, setBookingError] = useState(null); const [bookingSuccess, setBookingSuccess] = useState(false); const [availableRanges, setAvailableRanges] = useState([]); - const [bookingStep, setBookingStep] = useState("entry"); const [selectedStart, setSelectedStart] = useState(null); const [selectedEnd, setSelectedEnd] = useState(null); - const [calendarMonth, setCalendarMonth] = useState(() => new Date().getMonth()); - const [calendarYear, setCalendarYear] = useState(() => new Date().getFullYear()); const [pricingMode, setPricingMode] = useState("daily"); const [isOwnProperty, setIsOwnProperty] = useState(false); const [favLoading, setFavLoading] = useState(false); @@ -402,22 +400,6 @@ export default function PropertyDetailsPage() { } }; - const MONTH_KEYS = [ - "month.january", - "month.february", - "month.march", - "month.april", - "month.may", - "month.june", - "month.july", - "month.august", - "month.september", - "month.october", - "month.november", - "month.december", - ]; - const DAY_KEYS = ["dayAbbr.sun", "dayAbbr.mon", "dayAbbr.tue", "dayAbbr.wed", "dayAbbr.thu", "dayAbbr.fri", "dayAbbr.sat"]; - const availableDatesSet = useMemo(() => { const dates = new Set(); if (!Array.isArray(availableRanges)) return dates; @@ -431,30 +413,6 @@ export default function PropertyDetailsPage() { return dates; }, [availableRanges]); - const isDateAvailable = (dateStr) => availableDatesSet.has(dateStr); - const isPastDate = (dateStr) => { - const today = new Date(); - today.setHours(0, 0, 0, 0); - return new Date(dateStr) < today; - }; - - const handleDayClick = (dateStr) => { - if (bookingStep === "entry") { - setSelectedStart(dateStr); - setSelectedEnd(null); - setBookingStep("exit"); - } else { - if (new Date(dateStr) <= new Date(selectedStart)) { - setSelectedStart(dateStr); - setSelectedEnd(null); - setBookingStep("exit"); - } else { - setSelectedEnd(dateStr); - setBookingStep("entry"); - } - } - }; - const handleBookingConfirm = async () => { if (!AuthService.isAuthenticated()) { setShowLoginDialog(true); @@ -480,21 +438,6 @@ export default function PropertyDetailsPage() { } }; - const navigateMonth = (delta) => { - let month = calendarMonth + delta; - let year = calendarYear; - if (month < 0) { - month = 11; - year--; - } - if (month > 11) { - month = 0; - year++; - } - setCalendarMonth(month); - setCalendarYear(year); - }; - const handleRatingSuccess = () => { setShowRatingForm(false); if (property) fetchAvgRating(property.id); @@ -1028,115 +971,18 @@ export default function PropertyDetailsPage() {
)} - {/* Step Indicator */} -
-
-
- {t("selectStartDate")} -
-
-
-
- {t("selectEndDate")} -
-
- {/* Calendar */} - {effectivePricingMode === "daily" ? ( -
-
- - - {t(MONTH_KEYS[calendarMonth])} {calendarYear} - - -
- -
- {DAY_KEYS.map((dk, i) => ( -
- {t(dk)} -
- ))} -
- - {(() => { - const firstDay = new Date(calendarYear, calendarMonth, 1).getDay(); - const daysInMonth = new Date(calendarYear, calendarMonth + 1, 0).getDate(); - const adjustedFirstDay = (firstDay + 1) % 7; - const cells = []; - for (let i = 0; i < adjustedFirstDay; i++) { - cells.push(
); - } - for (let day = 1; day <= daysInMonth; day++) { - const dateStr = `${calendarYear}-${String(calendarMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - const past = isPastDate(dateStr); - const available = isDateAvailable(dateStr); - const isSelStart = dateStr === selectedStart; - const isSelEnd = dateStr === selectedEnd; - const inRange = selectedStart && selectedEnd && new Date(dateStr) > new Date(selectedStart) && new Date(dateStr) < new Date(selectedEnd); - const disabled = past || !available; - cells.push( - , - ); - } - return
{cells}
; - })()} -
- ) : ( -
-
- - {calendarYear} - -
-
- {MONTH_KEYS.map((mk, idx) => { - const monthStr = `${calendarYear}-${String(idx + 1).padStart(2, "0")}`; - const isSelStart = selectedStart && selectedStart.startsWith(monthStr); - const isSelEnd = selectedEnd && selectedEnd.startsWith(monthStr); - const inRange = selectedStart && selectedEnd && monthStr > selectedStart.substring(0, 7) && monthStr < selectedEnd.substring(0, 7); - return ( - - ); - })} -
-
- )} + { + setSelectedStart(d); + setSelectedEnd(null); + }} + onSelectEnd={setSelectedEnd} + /> {/* Summary */} {selectedStart && ( @@ -1156,7 +1002,7 @@ export default function PropertyDetailsPage() { {effectivePricingMode === "daily" ? t("numberOfDays") : t("numberOfMonths")} {effectivePricingMode === "daily" - ? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24)) + 1) + ? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24))) : new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12 + 1}
@@ -1165,7 +1011,7 @@ export default function PropertyDetailsPage() { {formatCurrency( effectivePricingMode === "daily" - ? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24)) + 1) * property.priceDisplay.daily + ? Math.max(1, Math.round((new Date(selectedEnd) - new Date(selectedStart)) / (1000 * 60 * 60 * 24))) * property.priceDisplay.daily : (new Date(selectedEnd).getMonth() - new Date(selectedStart).getMonth() + (new Date(selectedEnd).getFullYear() - new Date(selectedStart).getFullYear()) * 12 + 1) * property.priceDisplay.monthly, )}{" "} diff --git a/full-project-analysis.md b/full-project-analysis.md index ae6ec16..85ebd5f 100644 --- a/full-project-analysis.md +++ b/full-project-analysis.md @@ -1,442 +1,331 @@ -# SweetHome — Full Project Analysis +# SweetHome — Full Project Analysis (Updated Aug 2026) **Project:** SweetHome Next.js Real Estate Application -**Date:** June 16, 2026 -**Framework:** Next.js 16.1.6 (App Router) + React 18.3.1 +**Framework:** Next.js 16.1.6 (App Router) + React 18.3.1 + React Compiler enabled **Language:** JavaScript (no TypeScript) -**Styling:** Tailwind CSS v4 + DaisyUI (unused) + Flowbite (unused) -**API:** Custom `.NET` backend at `https://45.93.137.91.nip.io/api` +**Styling:** Tailwind CSS v4, Framer Motion, lucide-react +**Backend:** .NET REST API at `https://45.93.137.91.nip.io/api` (+ report API at `http://45.93.137.91/api`) --- ## 1. Project Structure -### Directory Tree (Top 3 Levels) - ``` SweetHome/ -├── .gitea/workflows/ # CI/CD deployer -├── app/ # ★ MAIN APP (112 source files) -│ ├── account-verification/ # OTP email/phone verification -│ ├── auth/choose-role/ # Owner/Tenant/Agent role picker -│ ├── blocked/ # Blocked account page -│ ├── booked-properties/ # User's booked properties -│ ├── change-password/ # Change password form -│ ├── components/ # ★ 14 reusable components -│ │ ├── home/ # HeroSearch, PropertyMap -│ │ ├── property/ # BookingCalendar, PropertyMap -│ │ ├── ratings/ # StarRating, PropertyRatingForm, etc. -│ │ ├── BottomNav.js # Mobile bottom nav -│ │ ├── FloatingSidebar.js # Desktop floating sidebar -│ │ ├── NavLinks.js # Nav link atoms -│ │ ├── NotificationHandler.js # Firebase FCM handler -│ │ └── PropertyMapWithMarkers.js # Standalone Leaflet map -│ ├── contexts/ # ★ 3 context providers -│ │ ├── FavoritesContext.js -│ │ ├── NotificationsContext.js -│ │ └── PropertyContext.js # ORPHANED (never mounted) -│ ├── enums/ # 20 enum constant files -│ ├── faq/ # FAQ accordion page -│ ├── favorites/ # Favorites list page -│ ├── forgot-password/ # 3-step forgot password -│ ├── i18n/ # i18next config (Arabic/English) -│ ├── login/ # 2-step login (credential + OTP) -│ ├── my-rates/ # Ratings received from owners -│ ├── notifications/ # Notifications list page -│ ├── onboarding/ # 3-step intro wizard -│ ├── owner/ # ★ Owner dashboard (6 sub-routes) -│ │ ├── account-book/ # Financial dashboard -│ │ ├── bookings/ # Booking management -│ │ ├── calendar/ # Monthly calendar view -│ │ ├── profits/ # Revenue/profit stats -│ │ ├── properties/ # CRUD property management -│ │ └── reservations/ # Reservation requests -│ ├── payments/ # Deposit payment page -│ ├── privacy/ # Privacy policy -│ ├── profile/ # User profile CRUD -│ ├── properties/ # Property listings grid -│ ├── property/[id]/ # Property detail (dynamic route) -│ ├── register/ # Registration (tenant/owner/agent) -│ ├── reports/ # 3-tab report submission -│ ├── reservations/ # Customer reservations -│ ├── services/ # AuthService.js -│ ├── settings/ # Account settings -│ ├── support/ # Support contact form -│ ├── terms/ # Terms of service -│ ├── utils/ # ★ api.js, calculations, firebase, etc. -│ ├── ClientLayout.js # Client layout shell (804 lines) -│ ├── layout.js # Root server layout -│ ├── globals.css # Tailwind + custom fonts -│ ├── page.js # Home page -│ ├── error.js # Error boundary -│ ├── loading.js # Loading state -│ └── not-found.js # 404 page -├── public/ # Static files (fonts, images, APK) -│ ├── files/SweetHome.apk # Android APK -│ ├── fonts/ # Madani Arabic (woff2 + ttf) -│ └── firebase-messaging-sw.js # FCM service worker -├── package.json # 18 prod + 7 dev dependencies -├── next.config.mjs # Next.js config -├── postcss.config.mjs # PostCSS + Tailwind -├── jsconfig.json # Path aliases (@/* → ./) -└── security-audit-report.md # Previous security audit +├── middleware.js # Root-level route protection (54 lines) +├── next.config.mjs # reactCompiler + images remotePatterns +├── full-project-analysis.md # This document +├── edit-property-flow.md # Flutter edit-property investigation (backend bug doc) +├── security-audit-report.md # Security findings +├── app/ +│ ├── layout.js # Root layout: fonts, metadata, providers +│ ├── ClientLayout.js # Nav (desktop+mobile), language, user menu +│ ├── page.js # Home page (hero, map, features) +│ ├── components/ # 20+ reusable components +│ ├── contexts/ # Favorites, Notifications, Theme (+orphan Property) +│ ├── enums/ # 24 enum files (index.js barrel) +│ ├── utils/ +│ │ ├── api.js # 70+ API functions + 4 fetch helpers +│ │ ├── ratings.js # Rating API (own private apiFetch) +│ │ └── constants.js +│ ├── services/AuthService.js # JWT cookie storage, role detection +│ ├── i18n/config.js # AR/EN translations (3173 lines) +│ ├── validations/ # OwnerValidatoin.js (email/phone/step validation) +│ ├── HelperFunction/ # ApiProperty.js mapper, UploadImage.js +│ ├── hooks/ # useApplyFilters, useAuth, UseFilterOfHeroSearch +│ ├── auth/ blocked/ faq/ favorites/ login/ onboarding/ ... # pages +│ └── owner/ # Dashboard (7 sub-routes) +├── public/ # APK, fonts, firebase-messaging-sw.js +└── package.json ``` -### Route Summary (33 pages) - -| Type | Routes | Count | -|------|--------|-------| -| Public | `/`, `/login`, `/register/*`, `/forgot-password`, `/auth/choose-role`, `/properties`, `/property/[id]`, `/terms`, `/privacy`, `/faq`, `/support`, `/onboarding` | 14 | -| Protected (auth only) | `/profile`, `/settings`, `/change-password`, `/account-verification`, `/favorites`, `/booked-properties`, `/reservations`, `/notifications`, `/payments`, `/my-rates`, `/reports`, `/blocked` | 12 | -| Owner only | `/owner/properties`, `/owner/properties/add`, `/owner/reservations`, `/owner/bookings`, `/owner/calendar`, `/owner/profits`, `/owner/account-book` | 7 | +**Routes (~33 pages):** Public (`/`, `/login`, `/register/*`, `/properties`, `/property/[id]`, `/terms`, `/privacy`, `/faq`, `/support`, `/onboarding`) | Auth-protected (`/profile`, `/reservations`, `/payments`, `/favorites`, `/booked-properties`, `/my-rates`, `/notifications`, `/reports`, `/settings`, `/change-password`) | Owner-only (`/owner/properties`, `/owner/properties/add`, `/owner/reservations`, `/owner/bookings`, `/owner/calendar`, `/owner/profits`, `/owner/account-book`) --- -## 2. Architecture Overview - -### Layout Hierarchy +## 2. Architecture +### Layout hierarchy ``` -app/layout.js (Server Component — root HTML, fonts, metadata) - └── app/ClientLayout.js (Client Component — 804 lines) - ├── (context) - │ └── (context) - │ └──
{children}
- ├── (mobile, authenticated only) - └── (FCM push notifications) +app/layout.js (server) — fonts (Geist + Madani Arabic), metadata, + └── + └── + └── + └── # nav, user menu, language switcher + └──
{children}
+ ├── # mobile (authenticated) + └── # Firebase FCM ``` -- **No nested layouts** — flat route structure -- **No `middleware.js`** — all route protection is client-side only -- Nav bar hidden on auth pages (`/login`, `/register/*`, `/blocked`, `/forgot-password`, `/auth/choose-role`) - -### Data Flow Pattern - +### Route protection (root middleware.js) +```js +// middleware.js — reads cookies: auth_token + cached_user +if (!token && !isPublicRoute && !isAuthRoute) return NextResponse.redirect(new URL("/login", request.url)); +if (token && isAuthRoute) return NextResponse.redirect(new URL("/", request.url)); +if (IsOwner && !isOwner) return NextResponse.redirect(new URL("/", request.url)); // owner routes +if (IsPrivateRoute && !token) return NextResponse.redirect(new URL("/login", request.url)); ``` -Page (useEffect on mount) - → API call (apiFetch from app/utils/api.js) - → AuthService.getToken() → Authorization: Bearer - → fetch() to https://45.93.137.91.nip.io/api/{endpoint} - → Response: auto-unwrap { data: ... } envelope - → Return parsed data or throw on non-ok - → Component state (useState) - → Render UI with Tailwind classes +- Owner detection relies on `cached_user` cookie's `roles` array (not the JWT). +- Static assets excluded via matcher regex. + +### Data flow +``` +Page (useEffect) → apiFetch(endpoint, options) → AuthService.getToken() → Bearer JWT + → fetch → unwrap { data } envelope → useState → render Tailwind UI ``` --- -## 3. API Layer +## 3. API Layer (`app/utils/api.js`, 783 lines) -**File:** `app/utils/api.js` (1198 lines — 370 commented legacy + 828 active) -**Ratings API:** `app/utils/ratings.js` (separate file with own `apiFetch`) +### Fetch helpers -### Fetch Helpers - -| Function | Lines | Method | Auth | Error Handling | Return Type | -|----------|-------|--------|------|----------------|-------------| -| `apiFetch(endpoint, options)` | 421-478 | Any | Auto Bearer | Throws on non-ok (except 206) | `data` (auto-unwrapped) | -| `authFetch(endpoint, body, token)` | 483-524 | POST | Manual param | Returns `{status,data,ok,message}` | Object | -| `reportFetch(endpoint, body)` | 526-558 | POST | None | Returns `{status,data,ok,message}` | Object | -| `multipartAuthFetch(endpoint, formData)` | 772-803 | POST | Auto Bearer | Returns `{status,data,ok,message}` | Object | - -### Exported API Functions (47 total) - -| Category | Functions | Count | -|----------|-----------|-------| -| Rent Properties | `getRentProperties`, `getRentProperty`, `getRentPropertyLocations`, `filterRentProperties` | 4 | -| Sale Properties | `getSaleProperties`, `getSaleProperty`, `getSalePropertyById` | 3 | -| Generic Property | `getProperty` | 1 | -| Recommendations | `getRecommendations`, `getTopRecommendations` | 2 | -| Reservations | `getAvailableDateRanges`, `getReservations`, `getReservation`, `checkAvailability`, `bookReservation`, `getUserReservations`, `getOwnerReservationRequests`, `getOwnerReservationsByStatuses`, `ownerConfirmReservation`, `confirmDepositPayment`, `adminConfirmDeposit`, `updateBookingStatus`, `payDeposit` | 13 | -| Owner Management | `getMyRentListings`, `getMySaleListings`, `addRentProperty`, `addSaleProperty`, `editRentProperty`, `editSaleProperty`, `updateRentPropertyStatus`, `updateSalePropertyStatus`, `getOwnerContactInformation`, `getOwnerStatistics` | 10 | -| Auth | `loginWithEmail`, `loginWithPhone`, `sendEmailOTP`, `sendPhoneOTP`, `verifyEmail`, `verifyPhone`, `changePassword`, `requestForgetPasswordOtp`, `verifyForgetPasswordOtp`, `resetPassword`, `deleteMyAccount` | 11 | -| Registration | `addOwner`, `addCustomer`, `registerRealEstateAgent` | 3 | -| Favorites | `getUserFavoriteProperties`, `addFavoriteProperty`, `removeFavoriteProperty` | 3 | -| Notifications | `getUserNotifications`, `setFCMToken` | 2 | -| Terms | `getARTerms`, `getENTerms`, `addOrUpdateTerms` | 3 | -| Currencies | `getCurrencies` | 1 | -| Files | `uploadPicture` | 1 | -| Reports | `sendGeneralReport`, `submitReport`, `submitReservationReport`, `updateReservationReport`, `submitSaleReport`, `updateSaleReport` | 6 | -| Profile | `getCustomerByUserId`, `getOwnerByUserId` | 2 | - -### Ratings API (5 functions) - -`addPropertyRating`, `addCustomerRating`, `getPropertyRatings`, `getCustomerRatings`, `getPropertyAverageRating` - -### API Endpoint Coverage - -| HTTP Method | Count | Examples | -|-------------|-------|----------| -| GET | 26 | `/RentProperties/GetRentProperties`, `/Reservations/GetAvailableDates/available/${id}` | -| POST | 17 | `/Auth/LogInWithEmail`, `/Reservations/PayDeposit/pay-deposit`, `/Rating/AddPropertyRating` | -| PUT | 8 | `/RentProperties/EditRentProperty/${id}`, `/Reservations/AdminConfirmDeposit/admin-confirm-deposit` | -| DELETE | 2 | `/FavoriteProperty/Remove`, `/User/DeleteMyAccount` | - ---- - -## 4. State Management - -### Context Providers - -| Context | File | State | Consumers | Status | -|---------|------|-------|-----------|--------| -| **FavoritesProvider** | `app/contexts/FavoritesContext.js` | `favorites[]`, `isLoading` | `FloatingSidebar`, `/favorites`, `/properties`, `/property/[id]` | ✅ Active | -| **NotificationsProvider** | `app/contexts/NotificationsContext.js` | `notifications[]`, `unreadCount`, `isLoading` | `BottomNav`, `FloatingSidebar` | ✅ Active | -| **PropertyContext** | `app/contexts/PropertyContext.js` | `properties[]`, `loading`, `error` | None | ❌ Orphaned (never mounted) | -| **PropertyContext** | `app/utils/PropertyContext.js` | Same as above | None | ❌ Orphaned duplicate | - -### localStorage Keys (10 actively written, ~5 legacy read-only) - -| Key | Data | Written By | Read By | -|-----|------|------------|---------| -| `auth_token` | JWT string | `AuthService.addToken()` | `AuthService.getToken()`, `firebase.js` | -| `cached_user` | User profile JSON | `AuthService.cacheUser()` | `AuthService.getCachedUser()` | -| `language` | "en" or "ar" | `ClientLayout.js` | `ClientLayout.js` | -| `sweethome_onboarding_completed` | "true" | `/onboarding/page.js` | `/onboarding/page.js` | -| `ownerProperties` | Properties array JSON | `/owner/properties/page.js` | `/owner/calendar/page.js` | -| `ownerBookings` | Bookings array JSON | `/owner/bookings/page.js` | `/owner/bookings/page.js` | -| `ownerProfitsTable` | Profit data JSON | `/owner/profits/page.js` | `/owner/profits/page.js` | -| `userProfile` | Profile form data JSON | `/profile/page.js` | `/profile/page.js` | -| `userAvatar` | Base64 data URL | `/profile/page.js` | `/profile/page.js` | -| `token`, `accessToken`, `authToken` | (legacy reads only) | — | `/settings`, `/payments`, `/reservations` | - -### Auth Flow +| Helper | Lines | Behavior | +|---|---|---| +| `apiFetch` | 38-87 | Auto Bearer, auto JSON-stringify (skips FormData), unwraps `{data}`, 206 tolerated, 451 → `/blocked` redirect, Arabic network error | +| `authFetch` | 89-134 | POST only, returns `{status,data,ok,message}` (no throw) | +| `reportFetch` | 136-172 | POST to REPORT_API_BASE (no auth) | +| `multipartAuthFetch` | 418-448 | POST + FormData + Bearer | +```js +async function apiFetch(endpoint, options = {}) { + const token = AuthService.getToken(); + const headers = { ...(token && { Authorization: `Bearer ${token}` }), ...(options.headers || {}) }; + // ... FormData passthrough, JSON.stringify for objects + // unwraps: if (json && "data" in json) return json.data; +} ``` -Login (app/login/page.js) - → loginWithEmail() / loginWithPhone() - → authFetch(POST /Auth/LogInWithEmail, { credential, password, device: 0 }) - → 200: AuthService.addToken(token) → localStorage.setItem('auth_token', token) - → 206: OTP required → sendEmailOTP() → verifyEmail(code) → get final token - → AuthService.cacheUser(user) → localStorage.setItem('cached_user', ...) - → router.push('/') -ClientLayout.js (every page navigation) - → AuthService.getUser() → decode JWT payload (base64 atob) - → setUser({ name, email, phone, role: isOwner() ? 'owner' : 'customer' }) - → UI branches on user.role (show owner nav / customer nav / guest nav) +### Key API functions (endpoints) -Page-level checks - → Each protected page independently calls: - AuthService.isAuthenticated() → else router.push('/login') - AuthService.isOwner() → else router.push('/') +| Function | Endpoint | Method | +|---|---|---| +| `getRentProperties` | `/RentProperties/GetRentProperties` | GET | +| `getRentProperty` | `/RentProperties/GetRentPropertyById/{id}` | GET | +| `getMyRentListings` / `getMySaleListings` | `/RentProperties/GetMyRentListings`, `/SaleProperties/GetMySaleListings` | GET | +| `addRentProperty` | `/RentProperties/AddRentProperty` (uses `buildRentPropertyPayload`) | POST | +| `editRentProperty` | `/RentProperties/EditRentProperty/{id}` — strict camelCase schema | PUT | +| `editSaleProperty` | `/SaleProperties/EditSaleProperty/{id}` | PUT | +| `updateRentPropertyStatus` | `/RentProperties/UpdateStatus/{id}` `{status}` | PUT | +| `uploadPicture` | `/Files/UploadPicture` (FormData `image`) → `{value: "/Pictures/xxx.jpg"}` | POST | +| `bookReservation` | `/Reservations/BookReservation/book` | POST | +| `getUserReservations` | `/Reservations/GetUserResevations` | GET | +| `getOwnerReservationRequests` | `/Reservations/GetOwnerResevationRequests` | GET | +| `ownerConfirmReservation` | `/Reservations/OwnerConfirmReservation/owner-confirm/{id}` | PUT | +| `payDeposit` | `/Reservations/PayDeposit/pay-deposit` (FormData) | POST | +| `getPaymentTypes` | `/PaymentType/GetAll` | GET | +| `getMyTransaction` | `/Customer/GetMyTransaction` | GET | +| `getOwnerStatistics` | `/Statistics/GetOwnerStatistics` | GET | +| `getOwnerContactInformation` | `/Owner/GetOwnerContactInformation?propertyInformationId=` | GET | +| `loginWithEmail` | `/Auth/LogInWithEmail` `{credential,password,device:0}` | POST | +| `loginWithPhone` | `/Auth/LogInWithPhoneNumber` | POST | +| `getCurrencies` | `/Currency/GetAll` | GET | +| `filterRentProperties` | `/RentProperties/FilterRentProperties?{params}` | GET | +| `getUserNotifications` | `/Notifications/GetUserNotifications` | GET | +| `setFCMToken` | `/User/SetFCMToken` `{token, deviceType:2}` | POST | +| `submitReservationReport` | `/ReservationReports` | POST | + +### `buildRentPropertyPayload` — city/governorate/documentType normalizer +```js +export function buildRentPropertyPayload(data = {}) { + const rawCityValue = data?.city ?? data?.governorate ?? propertyInformation?.city ?? 1; + const cityInt = parseInt(rawCityValue, 10) || 1; + return { ...data, city: cityInt, governorate: cityInt, documentType: data?.documentType ?? 1, + propertyInformation: { ...propertyInformation, city: cityInt, governorate: cityInt, documentType: ... } }; +} +``` + +### `editRentProperty` — strict swagger-contract payload (current, works) +```js +export async function editRentProperty(id, data) { + const pi = data?.propertyInformation || {}; + const images = Array.isArray(pi.images) ? pi.images : []; + const body = { + propertyInformation: { + activityStatus: 1, images: images.length > 0 ? images : [""], + cordsX: pi.cordsX ?? null, cordsY: pi.cordsY ?? null, + address: pi.address ?? "", description: pi.description ?? null, + numberOfBathRooms: pi.numberOfBathRooms ?? 0, numberOfRooms: pi.numberOfRooms ?? 0, + numberOfBedRooms: pi.numberOfBedRooms ?? 0, space: pi.space ?? 0, + detailsJSON: pi.detailsJSON ?? "", buildingType: pi.buildingType ?? 0, + status: pi.status ?? 0, propertyType: pi.propertyType ?? 0, city: data.city ?? pi.city ?? 1, + }, + deposit: data.deposit ?? 0, acceptedCertificate: data.acceptedCertificate ?? 0, + monthlyRent: data.monthlyRent ?? 0, dailyRent: data.dailyRent ?? 0, + rating: data.rating ?? 1, currencyId: data.currencyId ?? 1, rentType: data.rentType ?? 0, + isSmokeAllow: data.isSmokeAllow ?? false, specializedFor: data.specializedFor ?? false, + isVisitorAllow: data.isVisitorAllow ?? false, allowedPaymentPeriod: data.allowedPaymentPeriod ?? "", + type: data.type ?? 0, + }; + return apiFetch(`/RentProperties/EditRentProperty/${id}`, { method: "PUT", body }); +} ``` --- -## 5. Component Architecture +## 4. Auth (`app/services/AuthService.js`, 219 lines) -### Component Inventory (14 reusable components) +- **Storage:** `js-cookie` — `auth_token` (7d, secure, sameSite=lax), `cached_user`. +- **Role detection:** JWT claims via `atob(payload)`; roles from `http://schemas.microsoft.com/ws/2008/06/identity/claims/role` (string or array). +- **`getUser()`** merges cookie profile (name/email/phone) + JWT (`id` from `nameidentifier`/`sub`). +- **`login()`** flow: `loginWithEmail/Phone` → 200 = SUCCESS + `cacheCurrentUser()`; 206 = OTP_REQUIRED. -| Component | File | Lines | Purpose | State | -|-----------|------|-------|---------|-------| -| HeroSearch | `components/home/HeroSearch.js` | 321 | Hero search with filters | `filters`, login dialog | -| PropertyMap (home) | `components/home/PropertyMap.js` | 287 | Map with markers + popup | `selectedProperty`, map ref | -| PropertyMap (property) | `components/property/PropertyMap.js` | 166 | Map at property detail | Tooltip state | -| BookingCalendar | `components/property/BookingCalendar.js` | 124 | Monthly booking grid | `currentMonth`, `selectedStart` | -| PropertyMapWithMarkers | `components/PropertyMapWithMarkers.js` | 100 | Generic Leaflet map | Map ref, markers ref | -| StarRating | `components/ratings/StarRating.js` | 134 | 5-star input | `hoverRating` | -| PropertyRatingList | `components/ratings/PropertyRatingList.js` | 286 | Paginated reviews | `ratings`, `page`, `loading` | -| PropertyRatingForm | `components/ratings/PropertyRatingForm.js` | 317 | 4-field rating form | Clean/services/owner/experience | -| CustomerRatingForm | `components/ratings/CustomerRatingForm.js` | 92 | 3-field rating form | Furn/terms/behavior | -| BottomNav | `components/BottomNav.js` | 58 | Mobile bottom bar | Badge from context | -| FloatingSidebar | `components/FloatingSidebar.js` | 158 | Quick-access buttons | Framer-motion variants | -| NavLinks | `components/NavLinks.js` | 42 | Nav link atoms | `usePathname` active | -| NotificationHandler | `components/NotificationHandler.js` | 151 | FCM push handler | Permission state | +```js +getRoles() { + const payload = this.decodeToken(); + const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; + return Array.isArray(roles) ? roles : typeof roles === "string" ? [roles] : []; +} +isOwner() { return this.getRoles().includes("Owner"); } +isAgent() { return this.getRoles().includes("RealEstateAgent"); } +``` -### Largest Page Files - -| Rank | File | Lines | Complexity | -|------|------|-------|------------| -| 1 | `api.js` | 1198 | 60+ functions, 4 fetch helpers, 370 lines commented | -| 2 | `PropertyDetail.js` | 1743 | 56 imports, 8+ state vars, booking calendar, image gallery, owner check, ratings, map | -| 3 | `owner/properties/page.js` | 2057 | CRUD + modals + localStorage caching + API calls | -| 4 | `ClientLayout.js` | 804 | Nav (desktop+mobile), user menu, language switcher, context providers | -| 5 | `owner/properties/add/page.js` | 700+ | Multi-step form with map, image upload, pricing | -| 6 | `app/page.js` (Home) | 623 | Property fetching, multi-filter, hero, map, feature cards | +> ⚠ **Bug:** `login()` calls `sendEmailOTP()`/`sendPhoneOTP()` (lines 203-204) but they are **not imported** → ReferenceError on OTP-required login. --- -## 6. Key Patterns +## 5. Owner Dashboard -### Data Fetching (4 Patterns) +| Page | Lines | Data source | Endpoints | +|---|---|---|---| +| `/owner/reservations` | 1537 (active from 646) | **Real API** | GET `GetOwnerResevationRequests` + batch `GetRentProperties` enrichment; PUT `OwnerConfirmReservation/owner-confirm/{id}`; POST `ChangeReservationStatus?id=&newStatus=5`; POST `ReservationReports/ReportReservation` | +| `/owner/properties` | 2270 | **Real API + localStorage** | GET `GetMyRentListings`/`GetMySaleListings`; PUT `EditRentProperty/{id}`; PUT `EditSaleProperty/{id}`; PUT `UpdateStatus/{id}`; POST `UploadPicture`. **Delete is local-only** | +| `/owner/properties/add` | 1712 | **Real API** | POST `AddRentProperty`/`AddSaleProperty`; POST `UploadPicture`; GET `Currency/GetAll`; Nominatim geocoding | +| `/owner/bookings` | 600 | **Real API** | GET `GetOwnerResevationRequests` | +| `/owner/calendar` | 743 | **Mock/localStorage** | None (seeds fake properties; calls undefined `loadCalendar()`) | +| `/owner/profits` | 594 | **Mock/localStorage** | None (hard-coded sampleData, 5% commission, XLSX export) | +| `/owner/account-book` | 142 | **Real API** | GET `Statistics/GetOwnerStatistics` | -| Pattern | Usage | Found In | -|---------|-------|----------| -| `useEffect` + `useState` | ~90% of pages | Most pages | -| Context-level fetch | 2 providers | `FavoritesContext`, `NotificationsContext` | -| localStorage cache | Owner dashboards | `owner/bookings`, `owner/calendar`, `owner/profits` | -| Mock data fallback | 3 owner pages | `owner/bookings`, `owner/calendar`, `owner/profits` | +### Owner reservations page — key code +```js +// loadReservations (1259-1314): parallel fetch + batch enrich +const [resResult, rentProps] = await Promise.all([ + fetch(`${API_BASE}/Reservations/GetOwnerResevationRequests`, { headers: { Authorization: `Bearer ${token}` } }) + .then(async (res) => { ... return list; }), + getRentProperties().catch(() => []), +]); +const propMap = {}; +propsList.forEach(rp => { + const info = rp?.propertyInformation ?? {}; + propMap[rp.propertyInformationId] = info; + if (rp?.propertyInformation?.id) propMap[rp.propertyInformation.id] = info; +}); +const enriched = resResult.map(r => { if (r.propertyId && propMap[r.propertyId]) r._prop = propMap[r.propertyId]; return r; }); +``` -### Styling - -- 100% Tailwind CSS v4 (utility classes, no CSS modules) -- Custom `globals.css`: Madani Arabic fonts (9 weights), Leaflet overrides, keyframe animations -- Color palette: `amber-500/600` (primary), `#ede6e6` (bg), `#156874` (accent teal) -- RTL-first with dynamic LTR switching via `currentLanguage` state - -### Animations (Framer Motion v12.29.2) - -- `initial/animate` opacity + y/x translations (section reveals) -- `whileHover`/`whileTap` scale (buttons, interactive elements) -- `AnimatePresence` (property popups, user menus) -- `whileInView` + `viewport` (scroll-triggered animations on home page) -- `staggerChildren` (hero text) -- Spring transitions `{ type: "spring", damping: 25, stiffness: 300 }` - -### Form Handling - -- **All forms**: Controlled `useState` — no React Hook Form, Formik, or `useReducer` -- **Validation**: Inline `value === 0 &&

مطلوب

` -- **No form libraries** — all hand-rolled - -### Responsive Breakpoints - -| Breakpoint | Layout | -|------------|--------| -| Default (mobile) | Single column, BottomNav, hamburger menu | -| `md:` (768px) | Desktop nav, 2-3 column grids | -| `lg:` (1024px) | PropertyDetail sidebar, 3-column grids | +### Owner properties — edit modal +- **formData**: propertyType, furnished, description, bedrooms/bathrooms/floor/salons/balconies/livingRooms/area, services `{}`, serviceDetails `{}`, terms `{}`, customTerms[], 6 nearby distances, purpose, currencyId, daily/monthlyPrice, deposit, rentType, allowedPaymentPeriod, salePrice. +- **Services**: 13 enum-based (`PropertyService`) checkboxes with detail inputs. +- **Terms**: 3 enum-based (`PropertyTerm`) + custom terms chips. +- **Images**: `existingImages` (API) + `newImages` (upload via `uploadPicture`, preview via `URL.createObjectURL`, max 10, 5MB each) with delete/reorder. +- **Validation**: description, bedrooms≥1, bathrooms≥1, area>0, price required. +- **Save**: builds `detailsJSON` → `propInfo` → `editRentProperty(property.id, payload)`. --- -## 7. Owner Pages Analysis (Dashboard Section) +## 6. Customer Pages -| Route | File | Lines | Status | Data Source | -|-------|------|-------|--------|-------------| -| `/owner/properties` | `owner/properties/page.js` | 2057 | ✅ Active | API (`getMyRentListings`, `getMySaleListings`) + localStorage cache | -| `/owner/properties/add` | `owner/properties/add/page.js` | 700+ | ✅ Active | API (`addRentProperty`, `addSaleProperty`, `uploadPicture`) | -| `/owner/reservations` | `owner/reservations/page.js` | 1400+ | ❌ Commented out | Mock data (commented) | -| `/owner/bookings` | `owner/bookings/page.js` | 560+ | ⚠️ Demo | localStorage mock data only | -| `/owner/calendar` | `owner/calendar/page.js` | 580+ | ⚠️ Demo | localStorage mock data only | -| `/owner/profits` | `owner/profits/page.js` | 600+ | ⚠️ Demo | localStorage mock data only | -| `/owner/account-book` | `owner/account-book/page.js` | 500+ | ✅ Active | API (`getOwnerStatistics`) | +| Page | Lines | Data source | +|---|---|---| +| `/reservations` | 1826 (active from 391) | `getUserReservations` + `getRentProperties` enrich + `getPaymentTypes` | +| `/payments` | 600 | `getMyTransaction` + `getPaymentTypes` | +| `/property/[id]` | 1231 | `getRentProperty` → `getSalePropertyById` → `getSaleProperty` cascade | +| `/properties` | 851 | `FilterRentProperties` + `getSaleProperties` hybrid | +| `/` home | HomeClient 71 + hooks | `getRentProperties` + `getSaleProperties` client-side filter | +| `/booked-properties` | 220 | `getUserReservations` | +| `/favorites` | 128 | `FavoritesContext` | +| `/my-rates` | 148 | `getCustomerRatings` | +| `/notifications` | 159 | `getUserNotifications` (mark-read client-only) | + +### Customer reservations — PaymentDialog (Haram receipt flow) +```js +const selectedMethod = paymentMethods?.find((m) => String(m?.id ?? m?.name) === String(selectedPayment)); +const isHaram = selectedMethod?.name?.toLowerCase?.() === "haram"; +const canPay = payingId !== reservation.id && hasSelectedMethod && (!isHaram || !!receiptImage); +// handleConfirmPay: payDeposit({ reservationId, paymentTypeId: selectedPayment, comment, paymentImage: receiptImage }) +// payDeposit → FormData: ReservationId, PaymentTypeId, Comment, paymentImage (multipart) +``` +- CountdownTimer: deadline = `ownerApprovalDate + allowedPaymentPeriod` (`parseTimeSpan` handles `d.hh:mm:ss`). +- Inline rating form (4 categories) for `depositPaid`/`completed`. + +### Property detail +- Image gallery (chevron prev/next, thumbnails, counter), Leaflet map, availability calendar (`availableDatesSet` from `GetAvailableDates/available/{id}`), booking flow (daily/monthly toggle, calendar month grid, summary, `bookReservation`). +- **Unused**: owner-contact feature (`getOwnerContactInformation` defined but no UI), `handleBookNow`/`bookingDates` dead code. --- -## 8. Admin Functionality +## 7. i18n (`app/i18n/config.js`, 3173 lines) -| Component | Status | Data Source | -|-----------|--------|-------------| -| BookingRequests | ✅ Active | Real API (`getReservations`, `adminConfirmDeposit`) | -| Users | ❌ Not implemented | — | -| Properties | ❌ Not implemented | — | -| LedgerBook | ⚠️ Mock only | 3 hardcoded mock transactions | -| Dashboard | ❌ Not implemented | — | - -Designed for internal admin use at `/admin` — tabs for Bookings/Users/Properties/Ledger/Dashboard. +- 2 languages: `en` (1-1662), `ar` (1663-3155); flat dot-namespaced keys; `fallbackLng: "en"`. +- Init: `i18n.use(LanguageDetector).use(initReactI18next).init({ resources, fallbackLng: "en", detection: { order: ["localStorage", "navigator"], caches: ["localStorage"] } })`. +- ClientLayout overrides: `localStorage("language")` default `"ar"`, flips `document.documentElement.dir` (rtl/ltr). +- Active language toggle: mobile header + authenticated sticky bar (desktop navbar one commented out). --- -## 9. Known Technical Debt +## 8. State Management -### Dead Code - -| File | Dead Lines | Issue | -|------|-----------|-------| -| `api.js` | Lines 1-370 (370 lines) | Entire legacy version commented out | -| `StarRating.js` | Lines 1-93 (93 lines) | Old framer-motion version commented out | -| `PropertyRatingList.js` | ~150 lines | Old version commented out | -| `PropertyRatingForm.js` | ~220 lines | Old version commented out | -| `contexts/PropertyContext.js` | Entire file | Never mounted | -| `utils/PropertyContext.js` | Entire file | Duplicate orphan | - -### Code Duplication - -| Pattern | Files | Description | -|---------|-------|-------------| -| Leaflet maps | 3 separate files | `PropertyMapWithMarkers.js`, `home/PropertyMap.js`, `property/PropertyMap.js` — all setup Leaflet identically | -| Rating forms | `PropertyRatingForm.js` (317) + `CustomerRatingForm.js` (92) | ~75% code overlap; both define identical `RatingField` inline | -| formatCurrency | 4+ locations | Scattered across files with slightly different logic | -| Image URL building | 4+ locations | Different implementations for same task | -| Notifications page | `/notifications/page.js` vs `NotificationsContext` | Page duplicates all context state/logic | - -### Architecture Issues - -| Issue | Impact | -|-------|--------| -| No middleware.js | All route protection is client-side only | -| JWT in localStorage | XSS-vulnerable token storage | -| Client-side role checks | Users can modify JWT to elevate privileges | -| Owner pages use mock data | `/owner/bookings`, `/owner/calendar`, `/owner/profits` are demo-only | -| 3 unused UI libraries | DaisyUI, Flowbite, Yandex Maps imported but never used | -| No TypeScript | 112 JS files, no type safety | -| No ESLint | No linting configured | -| No testing framework | Zero tests | -| Flat route structure | No nested layouts; all pages share root layout | -| Active console.log | ~30+ files with console.log statements (security risk) | +| Context | Status | +|---|---| +| `FavoritesProvider` (app/contexts/FavoritesContext.js) | ✅ Mounted in layout.js; favorites list, add/remove (optimistic w/ rollback), refetch | +| `NotificationsProvider` (app/contexts/NotificationsContext.js) | ✅ Mounted; unreadCount = full array length; markAsRead is client-only | +| `ThemeProvider` | ✅ Mounted | +| `PropertyContext` (contexts/ + utils/ duplicate) | ❌ Orphaned — never mounted/imported; pure in-memory mock | --- -## 10. Dependencies +## 9. Enums (24 files in `app/enums/`) -### Production (18) - -| Package | Version | Purpose | -|---------|---------|---------| -| next | 16.1.6 | Framework | -| react / react-dom | ^18.3.1 | UI | -| firebase | ^12.11.0 | FCM push notifications | -| leaflet / react-leaflet | 1.9.4 / 4.2.1 | Maps | -| @pbe/react-yandex-maps | ^1.2.5 | Yandex Maps (unused) | -| framer-motion | ^12.29.2 | Animations | -| lucide-react | ^0.563.0 | Icons | -| flowbite / flowbite-react | 4.0.1 / 0.12.16 | UI library (unused) | -| react-hot-toast | ^2.6.0 | Toasts | -| i18next / react-i18next / i18next-browser-languagedetector | 25.8 / 16.5 / 8.2 | i18n | -| jspdf / html2canvas | 4.2.1 / 1.4.1 | PDF generation | -| xlsx | ^0.18.5 | Excel export | -| react-intersection-observer | ^10.0.3 | Scroll detection | - -### Dev (7) - -| Package | Version | Purpose | -|---------|---------|---------| -| tailwindcss | ^4.1.18 | CSS framework | -| @tailwindcss/postcss | ^4 | PostCSS plugin | -| postcss / autoprefixer | ^8.5.6 / ^10.4.23 | CSS processing | -| daisyui | ^5.5.14 | UI components (unused) | -| babel-plugin-react-compiler | 1.0.0 | React compiler optimization | - -### Missing (notable absences) -- No TypeScript -- No ESLint -- No testing (Jest, Playwright, Cypress) -- No state management library (Redux, Zustand) -- No data fetching library (React Query, SWR) -- No form library (React Hook Form, Formik) -- No security lib (helmet, cors, csurf) -- No HTTP client (axios, ky) -- No JWT lib (jsonwebtoken, jose) +Key ones: `BuildingType` (0-9 numeric), `PropertyService` (13 string), `PropertyTerm` (3 string), `PropertyStatus` (0/1/2), `BookingStatus` (string, ⚠ mixed casing `PENDING` vs `ownerConfirmed`), `RentType` (MONTHLY 0, DAILY 1), `Currency` (SYP 1, USD 2, EUR 3, TRY 4), `Governorate` (13), `DocumentType` (Passport/IDCard/Both), `UserRole`, `City` (Arabic values + CityEnum 0-12), `RentPropertyCondition`, `RentPropertyType`, `TransactionType`, `CancellationReason`. --- -## 11. Security Summary +## 10. Known Bugs & Issues -*(See `security-audit-report.md` for full details)* +| # | Issue | Location | +|---|---|---| +| 1 | `sendEmailOTP`/`sendPhoneOTP` used but not imported → ReferenceError on 206 | AuthService.js 203-204 | +| 2 | Edit endpoint ignores `images` + `detailsJSON` (backend bug) | Backend PUT handler | +| 3 | Delete property is local-only (no API call) | owner/properties/page.js 1657-1666 | +| 4 | `/owner/calendar` calls undefined `loadCalendar()` (line 499) | owner/calendar/page.js | +| 5 | `/owner/calendar` + `/owner/profits` are mock/localStorage only | — | +| 6 | `useApplyFilters.js:62` price bucket `"2000-3000"` compares `> 300` (typo, should be 3000) | hooks | +| 7 | HeroSearch propertyType onChange writes to `filters.city` (line 69) | components/home/HeroSearch.js | +| 8 | `addOwner` appends both back-ID + license images under same key `RearIdCarImagePath` | api.js 470-471 | +| 9 | `getSaleProperty` fetches whole list and filters client-side | api.js 194 | +| 10 | Notifications markAsRead client-only (no persistence) | contexts + page | +| 11 | `BookingCalendar.js` date click is a no-op | components/property | +| 12 | BookingStatus enum mixed casing | enums/BookingStatus.js | +| 13 | Ratings API has own private apiFetch without 451 handling | utils/ratings.js | +| 14 | `home/PropertyMap.js:162` quick-book uses `alert()` stub | components/home | +| 15 | Hardcoded phone `+963567823411` in payments + reservations | multiple | +| 16 | Firebase config hard-coded in NotificationHandler.js | components | +| 17 | Owner-contact feature dead on property detail | PropertyDetail.js 269-361 | + +## 11. Dead Code + +- `app/contexts/PropertyContext.js` + `app/utils/PropertyContext.js` (both orphaned) +- `reservations/page.js` lines 1-390, `profits/page.js` lines 1-291, `StarRating.js` 1-94, `PropertyRatingList.js` 1-151, `PropertyRatingForm.js` 1-219 (commented legacy) +- `handleBookNow`/`bookingDates` in PropertyDetail.js (383-403) +- Desktop language switcher (ClientLayout 259-268, commented) + +## 12. Dependencies + +**Prod (20):** next 16.1.6, react 18.3.1, firebase, leaflet/react-leaflet, framer-motion, lucide-react, react-hot-toast, i18next/react-i18next/browser-languagedetector, jspdf, html2canvas, xlsx, react-intersection-observer, flowbite/flowbite-react (unused), @pbe/react-yandex-maps (unused), js-cookie. + +**Dev (7):** tailwindcss v4, @tailwindcss/postcss, postcss, autoprefixer, daisyui (unused), babel-plugin-react-compiler. + +**Missing:** TypeScript, ESLint, tests, state library, data-fetching library, form library, HTTP client, JWT lib. + +## 13. Security (summary — see security-audit-report.md) | # | Finding | Severity | -|---|---------|----------| -| 1 | JWT in localStorage (XSS-theft) | 🔴 Critical | -| 2 | Stored XSS via Leaflet popup HTML | 🔴 Critical | -| 3 | OTP code logged to console | 🔴 Critical | -| 4 | No server-side middleware | 🔴 Critical | -| 5 | Passwords in URL query params | 🔴 Critical | -| 6 | IDOR on property edit/status APIs | 🔴 Critical | -| 7 | Client-only role checks | 🟠 High | -| 8 | HTTP endpoints leak tokens | 🟠 High | -| 9 | No CSP/security headers | 🟠 High | -| 10 | Mass assignment via spread | 🟠 High | -| 11 | Error messages leak internals | 🟠 High | -| 12 | Hardcoded IPs (10+ files) | 🟡 Medium | - ---- - -## 12. File Size Heatmap - -| Size Range | Files | Examples | -|------------|-------|----------| -| 1500-2100 lines | 2 | `PropertyDetail.js` (1743), `owner/properties/page.js` (2057) | -| 600-1200 lines | 6 | `api.js` (1198), `ClientLayout.js` (804), `login/page.js` (800+), `owner/properties/add/page.js` (700+), `app/page.js` (623), `owner/bookings/page.js` (560+) | -| 200-600 lines | 15 | Most page files | -| < 200 lines | ~70 | Enum files, smaller components, error/loading pages | +|---|---|---| +| 1 | JWT in cookie (secure flag OK) but roles in client-decodable cookie | High | +| 2 | Client-side role gating (cached_user cookie editable) | High | +| 3 | Passwords passed via URL query params (`changePassword`, `resetPassword`) | Critical | +| 4 | Stored XSS via Leaflet HTML popups | Critical | +| 5 | OTP codes logged to console | Critical | +| 6 | IDOR on property edit/status APIs (no server ownership check) | Critical | +| 7 | Firebase config public | Medium | +| 8 | Hardcoded IPs/credentials across files | Medium | diff --git a/middleware.js b/middleware.js index 1737f4d..028db57 100644 --- a/middleware.js +++ b/middleware.js @@ -1,36 +1,36 @@ import { NextResponse } from "next/server"; -const publicRoutes = ["/", "/properties"]; +const publicRoutes = ["/", "/properties", "/terms", "/privacy", "/faq", "/support"]; const authRoutes = ["/login", "/auth/choose-role", "/forgot-password", "/register/owner", "/register/tenant", "/register/agent"]; -const ownerRoutes = ["/owner/account-book", "/owner/bookings", "/owner/calendar", "/owner/profits", "/owner/properties/add", "/owner/reservations"]; -//ممكن كبو لانو زيادة -const priveteRoute = ["/account-verification", "/booked-properties", "/change-password", "/favorites", "/my-rates", "/notifications", "/payments", "/profile", "/reservations"]; +const ownerRoutes = ["/owner", "/owner/account-book", "/owner/bookings", "/owner/calendar", "/owner/profits", "/owner/properties", "/owner/reservations"]; + +function getRolesFromToken(token) { + if (!token) return []; + try { + const base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"); + const payload = JSON.parse(atob(base64)); + const roles = payload["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; + if (Array.isArray(roles)) return roles; + return roles ? [roles] : []; + } catch { + return []; + } +} + export function middleware(request) { const { pathname } = request.nextUrl; const token = request.cookies.get("auth_token")?.value; - const userCookie = request.cookies.get("cached_user")?.value; - - let isOwner = null; - - if (userCookie) { - try { - const user = JSON.parse(decodeURIComponent(userCookie)); - isOwner = user.roles?.includes("Owner"); - } catch (error) { - console.error("Cookie parse error:", error); - } - } + const isOwner = getRolesFromToken(token).includes("Owner"); const isPublicRoute = publicRoutes.some((route) => pathname === route || (route !== "/" && pathname.startsWith(route))); const isAuthRoute = authRoutes.some((route) => pathname === route || pathname.startsWith(route)); - const IsOwner = ownerRoutes.some((route) => route === pathname || pathname.startsWith(route)); + const isOwnerRoute = ownerRoutes.some((route) => pathname === route || pathname.startsWith(route)); - const IsPrivateRoute = priveteRoute.some((route) => pathname === route || pathname.startsWith(route)); if (!token && !isPublicRoute && !isAuthRoute) { return NextResponse.redirect(new URL("/login", request.url)); } @@ -39,13 +39,10 @@ export function middleware(request) { return NextResponse.redirect(new URL("/", request.url)); } - if (IsOwner && !isOwner) { + if (isOwnerRoute && !isOwner) { return NextResponse.redirect(new URL("/", request.url)); } - if (IsPrivateRoute && !token) { - return NextResponse.redirect(new URL("/login", request.url)); - } return NextResponse.next(); } diff --git a/package-lock.json b/package-lock.json index 89b0a7c..4993e67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -64,7 +64,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -83,7 +83,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2374,7 +2374,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.0"