# SweetHome — Full Project Analysis (Updated Aug 2026) **Project:** SweetHome Next.js Real Estate Application **Framework:** Next.js 16.1.6 (App Router) + React 18.3.1 + React Compiler enabled **Language:** JavaScript (no TypeScript) **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 ``` SweetHome/ ├── 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 ``` **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 ### Layout hierarchy ``` app/layout.js (server) — fonts (Geist + Madani Arabic), metadata, └── └── └── └── # nav, user menu, language switcher └──
{children}
├── # mobile (authenticated) └── # Firebase FCM ``` ### 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)); ``` - 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 (`app/utils/api.js`, 783 lines) ### Fetch helpers | 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; } ``` ### Key API functions (endpoints) | 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 }); } ``` --- ## 4. Auth (`app/services/AuthService.js`, 219 lines) - **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. ```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"); } ``` > ⚠ **Bug:** `login()` calls `sendEmailOTP()`/`sendPhoneOTP()` (lines 203-204) but they are **not imported** → ReferenceError on OTP-required login. --- ## 5. Owner Dashboard | 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` | ### 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; }); ``` ### 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)`. --- ## 6. Customer Pages | 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. --- ## 7. i18n (`app/i18n/config.js`, 3173 lines) - 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). --- ## 8. State Management | 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 | --- ## 9. Enums (24 files in `app/enums/`) 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`. --- ## 10. Known Bugs & Issues | # | 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 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 |