Skip to content

Conversation

@Rahul5977
Copy link
Member

@Rahul5977 Rahul5977 commented Nov 5, 2025

Feature: Listings Display & Browsing System

Added Components

Listings/

  • ListingCard.jsx — Card component for displaying individual listing previews.
  • ListingGrid.jsx — Responsive grid layout for organizing and displaying multiple listings.
  • ListingFilters.jsx — Sidebar/panel for applying various filters to listings.
  • SearchBar.jsx — Search input field (with optional suggestion support).
  • SortDropdown.jsx — Dropdown for sorting listings by criteria (e.g., price, date).

UI/

  • Pagination.jsx — Pagination controls for navigating through paginated listings.

Page: Listings/BrowseListings.jsx

Key Features

  • Responsive grid layout (1–4 columns depending on screen size).
  • Advanced filtering panel:
    • Category dropdown
    • Condition checkboxes
    • Price range slider
    • Hostel/location filter
    • General search input
  • Sorting by:
    • Newest / Oldest
    • Price (Low → High / High → Low)
  • Pagination with adjustable page size.
  • Graceful loading and error state handling.
  • Dedicated empty state display when no listings are found.

API Integration (services/listingService.js)

const listingService = {
  getAllListings: (params) => GET('/listings', { params }),
  getListingById: (id) => GET(`/listings/${id}`),
  getUserListings: (userId, params) => GET(`/listings/user/${userId}`, { params }),
  // ...other methods
};


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Multi-step Create/Edit listing wizard with image upload, previews, progress, and price history
  * Browse listings: search, filters, sort, pagination, listing cards, image gallery, related items, and share actions
  * Listing detail page with seller info, contact flow, and owner actions (edit/delete)

* **Improvements**
  * Redesigned login/register UX and improved token refresh/auth handling
  * Local Docker compose for full dev stack and example env template

* **Documentation**
  * Test credentials and comprehensive DB queries added; README updated
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai
Copy link

coderabbitai bot commented Nov 5, 2025

Walkthrough

Adds Dockerfiles and docker-compose; backend image upload, categories, price-history, and image deletion endpoints; extensive frontend listing features (pages, components, hooks, services, multi-step forms, validation); token refresh/update logic; docs, env examples, and various build/config updates.

Changes

Cohort / File(s) Summary
Docker / Compose
backend/Dockerfile, frontend/Dockerfile, docker-compose.yml
New Dockerfiles for backend/frontend and a docker-compose.yml orchestrating mongodb, backend, frontend with healthchecks, volumes, network and restart policies.
Dockerignore / Gitignore / Env
backend/.dockerignore, frontend/.dockerignore, .gitignore, .env.example
Updated ignore lists and added example env file; .gitignore expanded with many project artifacts and specific ignores (cookies, uploads, mongodb_data).
Backend routes & controllers
backend/src/routes/listing.routes.js, backend/src/controllers/listing.controller.js
New routes: POST /upload-image, GET /categories, GET /:id/price-history, DELETE /:id/images, plus GET /me/listings and /me/dashboard; controllers added/updated (getCategories, deleteListingImage, getPriceHistory, markAsSold) and moved to ApiResponse/ApiError patterns.
Backend models / schema
backend/src/models/listing.model.js, backend/src/models/index.js
priceHistory entries now include changedBy reference; models index switched from default to named exports and adjusted ActivityLog path; DEPARTMENTS formatting changed.
Frontend app & routing
frontend/src/App.jsx, frontend/src/components/auth/ProtectedRoute.jsx
Routing wired to real pages (Home, BrowseListings, ListingDetail, CreateListing, EditListing), tokenized auth routes; ProtectedRoute now renders <Outlet /> when no children.
Listing pages & flows
frontend/src/pages/listings/CreateListing.jsx, frontend/src/pages/listings/EditListing.jsx, frontend/src/pages/listings/ListingsDetails.jsx, frontend/src/pages/listings/*
New CreateListing multi-step wizard and EditListing page with load/authorization, image management, price history, Save/Draft flows; ListingDetail page with gallery, related listings, owner actions, contact flow.
Listing components
frontend/src/components/listings/* (many files, e.g., BrowseListings.jsx, ListingCard.jsx, ListingGrid.jsx, RelatedListings.jsx, ImageGallery.jsx, ImageUpload.jsx, ImageManager.jsx, FormProgress.jsx, ListingFilters/LisitingFilters.jsx, SearchBar.jsx, SortDropdown.jsx, SellerInfoCard.jsx, PriceHistory.jsx, ShareButton.jsx)
Large set of new UI components for listing browsing, card/grid, search/filters/sort, image gallery/upload/management, seller card, price history, form progress, share actions and related listings.
Listing step components & schemas
frontend/src/components/listings/steps/*, frontend/src/schemas/listingSchema.js
Per-step components (BasicInfoStep, PricingStep, ImageStep, LocationStep) and Zod step schemas plus combined createListingSchema.
Hooks & services
frontend/src/hooks/useListings.js, frontend/src/services/api.js, frontend/src/services/listingService.js, frontend/src/services/authService.js
New useListings hook for filters/pagination; api client updated refresh flow (uses /users/refresh-token, reads tokens from localStorage/cookies, retries original requests); listingService expanded (getListingForEdit, deleteListingImage, getPriceHistory); authService stores/clears refreshToken.
UI primitives & layout
frontend/src/components/ui/*, frontend/src/pages/HomePage.jsx
New Breadcrumb, Pagination, ShareButtons, HomePage hero/features and CTAs.
Auth pages
frontend/src/pages/auth/Login.jsx, frontend/src/pages/auth/Register.jsx
Login and Register UI rewritten to card-based forms with inline validation, native inputs and password toggles.
Config & tooling
frontend/vite.config.js, package.json
Vite server.host set to 0.0.0.0 and polling enabled; package.json adds docker-related npm scripts.
Docs & credentials
markdown/TEST_CREDENTIALS.md, markdown/dbQueries.md, README.md
Added test credentials and DB queries docs; README reorganized/renamed sections and updated setup instructions.
Misc
backend/cookies.txt, frontend/.dockerignore, backend/.dockerignore, README.md
Added sample cookies file and README updates; dockerignore files updated to include README.md and other ignores.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FE as Frontend
    participant API as ApiClient
    participant Auth as Auth Endpoint
    participant BE as Backend

    User->>FE: Request protected resource
    FE->>API: HTTP request (Authorization: Bearer accessToken)
    API->>BE: Forward request

    alt 200 OK
        BE->>API: 200 + data
        API->>FE: data
        FE->>User: render
    else 401 Unauthorized
        BE->>API: 401
        API->>Auth: POST /users/refresh-token (refreshToken)
        alt refresh success
            Auth->>API: 200 + new accessToken
            API->>API: store tokens (localStorage + cookies)
            API->>BE: retry original request with new token
            BE->>API: 200 + data
            API->>FE: data
            FE->>User: render
        else refresh failed
            Auth->>API: 401
            API->>API: clear tokens & user
            API->>FE: redirect /login
            FE->>User: show login
        end
    end
Loading
sequenceDiagram
    participant User
    participant Browse as BrowseListings
    participant Hook as useListings
    participant Service as listingService
    participant API as Backend API

    User->>Browse: Open listings page
    Browse->>Hook: init (filters, page)
    Hook->>Service: getAllListings(params)
    Service->>API: GET /listings?...
    API->>Service: return data (paginated or array)
    Service->>Hook: listings + meta
    Hook->>Browse: render listings

    User->>Browse: change filters / sort / page
    Browse->>Hook: updateFilters()/changePage()
    Hook->>Service: fetch with updated params
    Service->>API: GET /listings?...
    API->>Service: return updated data
    Service->>Hook: update state
    Hook->>Browse: render updated results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Files/areas needing careful attention:
    • frontend/src/services/api.js — token refresh, retry logic, storage and cookie sync, potential race conditions.
    • backend/src/controllers/listing.controller.js & routes — image upload flow, Cloudinary integration, local file cleanup, ownership checks and ApiError usage.
    • frontend/src/pages/listings/CreateListing.jsx and EditListing.jsx — multi-step validation, image upload sequencing, draft/save flows, change detection.
    • frontend/src/hooks/useListings.js — pagination/filter merging and response shape handling (two response formats).
    • backend/src/models/index.js — switching default → named exports may affect imports elsewhere.
    • docker-compose.yml and Dockerfiles — port mappings, dev vs production flags, and healthchecks for local dev workflows.

Poem

🐇 I hopped through code with tiny paws so spry,
Built galleries bright beneath the dev sky.
Tokens refreshed and images took flight,
Containers snuggled, docs tucked in tight —
A rabbit's cheer: "Ship it — overnight!"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Listing Browse & Search' is partially related to the changeset but significantly understates the scope. While browsing and searching are present, the PR introduces extensive additional functionality including listing creation/editing, Docker containerization, authentication updates, and comprehensive documentation. Consider a more comprehensive title that reflects the major additions such as 'Add listing browse, search, creation, Docker setup, and comprehensive docs' or 'Implement listings system with browse/search/create and Docker support' to better represent the full scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/utils/validation.js (1)

22-27: Restore password strength validation.

All password strength checks have been removed, leaving only a presence check. This is a critical security regression that allows users to set extremely weak passwords (e.g., "a", "123", "password"), making accounts vulnerable to brute-force attacks.

Restore minimum password requirements:

 password: (password) => {
     if (!password) {
       return "Password is required";
     }
+    if (password.length < 8) {
+      return "Password must be at least 8 characters long";
+    }
+    if (!/(?=.*[a-z])/.test(password)) {
+      return "Password must contain at least one lowercase letter";
+    }
+    if (!/(?=.*[A-Z])/.test(password)) {
+      return "Password must contain at least one uppercase letter";
+    }
+    if (!/(?=.*\d)/.test(password)) {
+      return "Password must contain at least one number";
+    }
     return null;
   },
frontend/src/components/listings/SortDropdown.jsx (1)

116-116: Fix invalid Tailwind class syntax.

The class min-w-2.5rem is not valid Tailwind CSS syntax. For arbitrary values, use square brackets.

Apply this diff:

-              "min-w-2.5rem h-10 px-3 rounded-md text-sm font-medium transition-colors",
+              "min-w-[2.5rem] h-10 px-3 rounded-md text-sm font-medium transition-colors",
frontend/src/pages/auth/Login.jsx (1)

50-54: Fix remember-me checkbox wiring

handleChange in useForm only looks at event.target.value, so this checkbox always records "on" even when the user unticks it. Once toggled, values can never go back to false, meaning login receives a permanent “remember me” flag — a functional regression for the new control. Persist the value as a boolean and seed it in the initial form state.

-  const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
-    { email: "", password: "" },
-    handleLogin,
-    validateForm
-  );
+  const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
+    { email: "", password: "", rememberMe: false },
+    handleLogin,
+    validateForm
+  );-                <input
-                  id="remember-me"
-                  name="remember-me"
-                  type="checkbox"
-                  className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
-                />
+                <input
+                  id="rememberMe"
+                  name="rememberMe"
+                  type="checkbox"
+                  checked={values.rememberMe}
+                  onChange={(e) =>
+                    handleChange({
+                      target: {
+                        name: "rememberMe",
+                        value: e.target.checked,
+                      },
+                    })
+                  }
+                  className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
+                />-                <label
-                  htmlFor="remember-me"
+                <label
+                  htmlFor="rememberMe"
                   className="ml-2 block text-sm text-gray-700"
                 >
                   Remember me

Also applies to: 140-152

🧹 Nitpick comments (7)
markdown/dbQueries.md (1)

633-699: Consider adding a note about ObjectId placeholders in sample data.

The sample seed data includes hardcoded ObjectId values for the seller field. Consider adding a brief comment noting that these ObjectIds should be replaced with actual user IDs from the database before inserting.

 db.listings.insertMany([
+  // Note: Replace ObjectId values with actual user IDs from your database
   {
     title: "Casio Scientific Calculator",
docker-compose.yml (1)

49-54: Consider using a more reliable healthcheck method.

The backend healthcheck relies on curl, which may not be available in the Docker image if not explicitly installed. This could cause healthcheck failures.

Consider one of these alternatives:

Option 1: Add curl to the backend Dockerfile
Ensure curl is installed in your backend Dockerfile.

Option 2: Use a lightweight health-check tool
Replace curl with a built-in health check. For a Node.js backend:

-      test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthcheck"]
+      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/healthcheck', (res) => {if (res.statusCode !== 200) throw new Error(res.statusCode)});"]

Or use wget if available:

-      test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthcheck"]
+      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/healthcheck"]
frontend/src/components/listings/ListingCard.jsx (1)

74-74: Consider specifying locale for price formatting.

Since the component uses the Indian Rupee symbol (₹), explicitly specifying the 'en-IN' locale would ensure consistent number formatting across all browsers regardless of user locale settings.

-              ₹{price.toLocaleString()}
+              ₹{price.toLocaleString('en-IN')}
frontend/src/services/listingService.js (1)

13-22: Make error handling consistent across all methods.

Only getAllListings has a try-catch block while all other methods let errors propagate naturally. This inconsistency can make debugging harder and suggests unclear error handling strategy.

Recommended approach: Remove the try-catch from getAllListings to maintain consistency, as the error logging doesn't add significant value and axios errors already contain sufficient debug information:

   getAllListings: async (params = {}) => {
-    try {
-      const response = await api.get("/listings", { params });
-      console.log("API Response:", response.data);
-      return response.data;
-    } catch (error) {
-      console.error("Listing Service Error:", error);
-      throw error;
-    }
+    const response = await api.get("/listings", { params });
+    return response.data;
   },

Alternatively, add try-catch blocks to all methods if you need method-specific error handling or logging.

frontend/src/services/authService.js (1)

13-22: Consider simplifying the nested conditionals.

The nested if statements are correct but could be more concise for better readability.

Apply this diff to simplify the token storage logic:

-    // Store access token, refresh token, and user data
-    if (response.data.data) {
-      if (response.data.data.accessToken) {
-        localStorage.setItem("accessToken", response.data.data.accessToken);
-      }
-      if (response.data.data.refreshToken) {
-        localStorage.setItem("refreshToken", response.data.data.refreshToken);
-      }
-      if (response.data.data.user) {
-        localStorage.setItem("user", JSON.stringify(response.data.data.user));
-      }
-    }
+    // Store access token, refresh token, and user data
+    const { data } = response.data;
+    if (data) {
+      data.accessToken && localStorage.setItem("accessToken", data.accessToken);
+      data.refreshToken && localStorage.setItem("refreshToken", data.refreshToken);
+      data.user && localStorage.setItem("user", JSON.stringify(data.user));
+    }
frontend/src/components/listings/SortDropdown.jsx (1)

13-20: Move sortOptions outside component to prevent recreation.

The sortOptions array is recreated on every render. Since it's static data, it should be defined outside the component for better performance.

Apply this diff:

+const sortOptions = [
+  { value: "newest", label: "Newest First" },
+  { value: "oldest", label: "Oldest First" },
+  { value: "price-low-high", label: "Price: Low to High" },
+  { value: "price-high-low", label: "Price: High to Low" },
+  { value: "title-asc", label: "Title: A to Z" },
+  { value: "title-desc", label: "Title: Z to A" },
+];
+
 const SortDropdown = ({ value, onChange, className }) => {
   const [isOpen, setIsOpen] = useState(false);
   const dropdownRef = useRef(null);
 
-  const sortOptions = [
-    { value: "newest", label: "Newest First" },
-    { value: "oldest", label: "Oldest First" },
-    { value: "price-low-high", label: "Price: Low to High" },
-    { value: "price-high-low", label: "Price: High to Low" },
-    { value: "title-asc", label: "Title: A to Z" },
-    { value: "title-desc", label: "Title: Z to A" },
-  ];
frontend/src/components/listings/ListingGrid.jsx (1)

43-43: Consider a softer retry mechanism instead of hard reload.

Using window.location.reload() performs a full page refresh, which loses application state and provides a poor user experience. Consider accepting a retry callback prop instead.

Modify the component signature and error state:

-const ListingGrid = ({ listings, loading, error }) => {
+const ListingGrid = ({ listings, loading, error, onRetry }) => {
         <button
-          onClick={() => window.location.reload()}
+          onClick={onRetry || (() => window.location.reload())}
           className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
         >
           Try Again
         </button>

This allows parent components to implement proper retry logic while maintaining backward compatibility.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c5dda8 and 81d6c0c.

📒 Files selected for processing (32)
  • backend/.dockerignore (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/cookies.txt (1 hunks)
  • docker-compose.yml (1 hunks)
  • frontend/.dockerignore (1 hunks)
  • frontend/Dockerfile (1 hunks)
  • frontend/src/App.jsx (3 hunks)
  • frontend/src/components/listings/BrowseListings.jsx (1 hunks)
  • frontend/src/components/listings/ImageGallery.jsx (1 hunks)
  • frontend/src/components/listings/LisitingFilters.jsx (1 hunks)
  • frontend/src/components/listings/ListingCard.jsx (1 hunks)
  • frontend/src/components/listings/ListingGrid.jsx (1 hunks)
  • frontend/src/components/listings/RelatedListings.jsx (1 hunks)
  • frontend/src/components/listings/SearchBar.jsx (1 hunks)
  • frontend/src/components/listings/SellerInfoCard.jsx (1 hunks)
  • frontend/src/components/listings/SortDropdown.jsx (1 hunks)
  • frontend/src/components/ui/Breadcrumb.jsx (1 hunks)
  • frontend/src/components/ui/Pagination.jsx (1 hunks)
  • frontend/src/components/ui/ShareButton.jsx (1 hunks)
  • frontend/src/hooks/useListings.js (1 hunks)
  • frontend/src/pages/HomePage.jsx (1 hunks)
  • frontend/src/pages/auth/Login.jsx (3 hunks)
  • frontend/src/pages/auth/Register.jsx (1 hunks)
  • frontend/src/pages/listings/ListingsDetails.jsx (1 hunks)
  • frontend/src/services/api.js (2 hunks)
  • frontend/src/services/authService.js (2 hunks)
  • frontend/src/services/listingService.js (1 hunks)
  • frontend/src/utils/validation.js (1 hunks)
  • frontend/vite.config.js (1 hunks)
  • markdown/TEST_CREDENTIALS.md (1 hunks)
  • markdown/dbQueries.md (1 hunks)
  • package.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
frontend/src/components/listings/RelatedListings.jsx (2)
frontend/src/services/listingService.js (8)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (64-64)
  • response (75-75)
  • response (85-85)
  • listingService (7-107)
frontend/src/components/listings/ListingCard.jsx (2)
  • listing (9-19)
  • ListingCard (8-85)
frontend/src/components/ui/ShareButton.jsx (2)
frontend/src/components/listings/ListingCard.jsx (1)
  • listing (9-19)
frontend/src/pages/listings/ListingsDetails.jsx (1)
  • listing (31-31)
frontend/src/components/ui/Pagination.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/components/listings/SortDropdown.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/services/api.js (1)
frontend/src/services/authService.js (8)
  • response (10-10)
  • response (34-34)
  • response (44-44)
  • response (67-67)
  • response (77-77)
  • response (88-91)
  • response (100-100)
  • response (116-116)
frontend/src/components/listings/ListingGrid.jsx (2)
frontend/src/hooks/useListings.js (3)
  • loading (12-12)
  • error (13-13)
  • listings (11-11)
frontend/src/components/listings/ListingCard.jsx (2)
  • listing (9-19)
  • ListingCard (8-85)
frontend/src/hooks/useListings.js (2)
frontend/src/components/listings/BrowseListings.jsx (1)
  • useListings (14-24)
frontend/src/services/listingService.js (8)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (64-64)
  • response (75-75)
  • response (85-85)
  • listingService (7-107)
frontend/src/components/listings/SearchBar.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/pages/listings/ListingsDetails.jsx (7)
frontend/src/components/listings/RelatedListings.jsx (2)
  • loading (12-12)
  • RelatedListings (10-84)
frontend/src/services/listingService.js (8)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (64-64)
  • response (75-75)
  • response (85-85)
  • listingService (7-107)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/components/ui/Breadcrumb.jsx (1)
  • Breadcrumb (8-35)
frontend/src/components/listings/ImageGallery.jsx (1)
  • ImageGallery (8-154)
frontend/src/components/ui/ShareButton.jsx (1)
  • ShareButtons (8-75)
frontend/src/components/listings/SellerInfoCard.jsx (1)
  • SellerInfoCard (7-108)
frontend/src/pages/auth/Register.jsx (1)
frontend/src/hooks/useForm.js (5)
  • handleSubmit (36-61)
  • values (8-8)
  • handleChange (16-30)
  • errors (9-9)
  • isSubmitting (10-10)
frontend/src/components/listings/LisitingFilters.jsx (5)
frontend/src/services/listingService.js (8)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (64-64)
  • response (75-75)
  • response (85-85)
  • listingService (7-107)
frontend/src/hooks/useListings.js (2)
  • error (13-13)
  • filters (20-29)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/components/ui/Input.jsx (1)
  • Input (4-68)
frontend/src/services/listingService.js (1)
frontend/src/services/api.js (2)
  • response (49-53)
  • api (9-15)
frontend/src/components/listings/ListingCard.jsx (2)
frontend/src/pages/listings/ListingsDetails.jsx (2)
  • listing (31-31)
  • conditionColors (150-156)
backend/src/controllers/listing.controller.js (1)
  • images (78-78)
frontend/src/components/listings/BrowseListings.jsx (6)
frontend/src/hooks/useListings.js (11)
  • useListings (10-161)
  • useListings (10-161)
  • filters (20-29)
  • updateFilters (104-107)
  • clearFilters (112-123)
  • listings (11-11)
  • loading (12-12)
  • error (13-13)
  • pagination (14-19)
  • changePage (128-131)
  • changeItemsPerPage (136-142)
frontend/src/components/listings/SearchBar.jsx (1)
  • SearchBar (9-82)
frontend/src/components/listings/SortDropdown.jsx (1)
  • SortDropdown (9-83)
frontend/src/components/listings/LisitingFilters.jsx (1)
  • ListingFilters (12-243)
frontend/src/components/listings/ListingGrid.jsx (1)
  • ListingGrid (8-77)
frontend/src/components/ui/Pagination.jsx (1)
  • Pagination (8-145)
frontend/src/services/authService.js (1)
frontend/src/services/api.js (1)
  • response (49-53)
frontend/src/App.jsx (4)
frontend/src/pages/auth/ResetPassword.jsx (1)
  • ResetPassword (11-207)
frontend/src/pages/auth/VerifyEmail.jsx (1)
  • VerifyEmail (7-133)
frontend/src/components/layout/AppLayout.jsx (1)
  • AppLayout (7-40)
frontend/src/components/auth/ProtectedRoute.jsx (1)
  • ProtectedRoute (9-46)
frontend/src/pages/auth/Login.jsx (1)
frontend/src/hooks/useForm.js (5)
  • handleSubmit (36-61)
  • values (8-8)
  • handleChange (16-30)
  • errors (9-9)
  • isSubmitting (10-10)
🪛 Checkov (3.2.334)
docker-compose.yml

[medium] 39-40: Basic Auth Credentials

(CKV_SECRET_4)

🪛 Gitleaks (8.28.0)
docker-compose.yml

[high] 38-38: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

backend/cookies.txt

[high] 5-5: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


[high] 6-6: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🪛 LanguageTool
markdown/TEST_CREDENTIALS.md

[style] ~221-~221: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...``` --- Last Updated: November 6, 2025 Status: ✅ Docker and Local authen...

(MISSING_COMMA_AFTER_YEAR)

markdown/dbQueries.md

[style] ~630-~630: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...t** --- Last Updated: November 6, 2025 Project: Campus Marketplace - IIT...

(MISSING_COMMA_AFTER_YEAR)

🔇 Additional comments (11)
frontend/vite.config.js (1)

8-14: LGTM! Docker-friendly server configuration.

The server configuration correctly exposes the Vite dev server on all interfaces (0.0.0.0) and enables polling-based file watching, which is essential for containerized development environments where native file system events may not propagate correctly.

package.json (1)

12-17: LGTM! Convenient Docker orchestration scripts.

The Docker scripts provide useful shortcuts for common docker-compose operations. The inclusion of docker:clean with the -v flag is particularly helpful for complete cleanup including volumes.

backend/Dockerfile (1)

1-21: LGTM! Development Dockerfile configured correctly.

The Dockerfile is appropriately configured for a development environment with curl installed for healthchecks and the dev server running via npm run dev.

Note: This Dockerfile is development-specific. For production deployment, consider:

  • Using npm ci instead of npm install for reproducible builds
  • Multi-stage builds to reduce image size
  • Running npm start instead of npm run dev
  • Using NODE_ENV=production
frontend/Dockerfile (1)

1-18: LGTM! Frontend Dockerfile aligns with Vite configuration.

The Dockerfile correctly runs the Vite dev server with --host 0.0.0.0, which aligns with the configuration in frontend/vite.config.js. This setup is appropriate for containerized development.

Note: Similar to the backend, this is a development-specific Dockerfile. For production, consider using a multi-stage build that runs npm run build and serves static files via nginx or a similar web server.

backend/.dockerignore (1)

1-14: LGTM! Comprehensive Docker ignore patterns.

The .dockerignore file appropriately excludes common development artifacts, dependencies, and Docker-related files from the build context, which helps reduce image size and build time.

docker-compose.yml (1)

1-32: Service configuration looks good.

The MongoDB configuration, volume mounts for development hot-reload, network setup, and service dependencies are well-structured. The healthcheck intervals and retry logic are reasonable for a development environment.

Also applies to: 40-48, 56-83

frontend/.dockerignore (1)

1-17: Good Docker build configuration.

The .dockerignore file appropriately excludes development artifacts, dependency files, logs, build outputs, and environment-specific configurations from Docker image builds. This keeps image sizes lean and avoids shipping unnecessary files.

frontend/src/components/listings/SearchBar.jsx (1)

9-82: LGTM!

The SearchBar implementation follows best practices:

  • Proper debouncing with a 300ms delay to reduce unnecessary API calls
  • Correct cleanup of timeouts on unmount to prevent memory leaks
  • External value synchronization for controlled component behavior
  • Clear button with proper state management
  • Good accessibility with aria-label
frontend/src/services/api.js (1)

18-76: LGTM! Robust token refresh implementation.

The refactored authentication flow handles token refresh correctly:

  • Tries localStorage first with cookie fallback for broader compatibility
  • Proper retry flag to prevent infinite loops on 401 responses
  • Comprehensive cleanup on refresh failure
  • Stores new tokens in both localStorage and cookies for redundancy

The implementation follows best practices for token management in SPAs.

frontend/src/components/listings/ImageGallery.jsx (1)

48-48: Fix CSS class name typo.

Missing space between aspect-4/3 and overflow-hidden creates an invalid class name that will break the aspect ratio styling.

Apply this diff:

-      <div className="relative aspect-4/3overflow-hidden bg-gray-100 rounded-xl overflow-hidden group">
+      <div className="relative aspect-4/3 overflow-hidden bg-gray-100 rounded-xl group">

Note: Also removed the duplicate overflow-hidden class.

Likely an incorrect or invalid review comment.

frontend/src/components/ui/Breadcrumb.jsx (1)

8-34: LGTM!

The breadcrumb navigation logic is well-implemented with proper conditional rendering for links and appropriate visual indicators.

Comment on lines +1 to +6
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost FALSE / TRUE 0 refreshToken eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2OTBiYTU1YmQ0NTg0ZGQyMjE1ZWViNGYiLCJpYXQiOjE3NjIzNzEwMzIsImV4cCI6MTc2Mjk3NTgzMn0.hwJcY8W_9jIbmIzU3P6XRdRKa_iui1zuKuJ8jcpK3B0
#HttpOnly_localhost FALSE / TRUE 0 accessToken eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2OTBiYTU1YmQ0NTg0ZGQyMjE1ZWViNGYiLCJlbWFpbCI6InRlc3RAaWl0YmhpbGFpLmFjLmluIiwidXNlcm5hbWUiOiJ0ZXN0c3R1ZGVudCIsInJvbGVzIjpbInN0dWRlbnQiXSwiaWF0IjoxNzYyMzcxMDMyLCJleHAiOjE3NjIzNzE5MzJ9.uwxgRd6-h4rI3WMOks3e3AxP64sbng3GjHtf1l5rFsA
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: Remove real JWT tokens from version control immediately.

This file contains actual JWT tokens that grant authentication access. Committing secrets to version control is a critical security vulnerability that can lead to unauthorized access.

Impact:

  • Anyone with repository access can decode and use these tokens
  • Tokens reveal user IDs and credentials in the payload
  • Compromises test account security

Required actions:

  1. Remove this file immediately using git rm --cached backend/cookies.txt
  2. Add cookies.txt and *.txt containing credentials to .gitignore
  3. Rotate/invalidate the exposed tokens
  4. Use environment variables or secure secret management instead

Apply this to .gitignore:

+# Cookie files with credentials
+backend/cookies.txt
+**/cookies.txt

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 5-5: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


[high] 6-6: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In backend/cookies.txt around lines 1 to 6 there are real JWTs committed; remove
the file from the repo and stop tracking it, add an entry to .gitignore to
prevent future commits (e.g., cookies.txt and any pattern matching credential
files), and rotate/invalidate the exposed tokens immediately; replace the
committed secrets with secure handling (use environment variables or a secret
manager) and ensure the removal is done with git rm --cached so history doesn’t
continue tracking the file.

Comment on lines 33 to 39
ACCESS_TOKEN_SECRET: secret
REFRESH_TOKEN_SECRET: refreshsecret
CORS_ORIGIN: http://localhost:5173
CLOUDINARY_CLOUD_NAME: dft6plppq
CLOUDINARY_API_KEY: 969212669239411
CLOUDINARY_API_SECRET: RReTfRfCX0mpoWbWQHHm5Wu3e5k
CLOUDINARY_URL: cloudinary://969212669239411:RReTfRfCX0mpoWbWQHHm5Wu3e5k@dft6plppq
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: Remove hardcoded secrets and credentials from docker-compose.yml.

Sensitive credentials should never be committed to version control, even in development configurations. The exposed credentials include:

  • Weak token secrets ("secret", "refreshsecret")
  • Cloudinary API key and secret
  • Full Cloudinary connection string

Apply this diff to move secrets to an environment file:

    environment:
      PORT: 3000
      MONGO_URI: mongodb://mongodb:27017/campus-marketplace
-     ACCESS_TOKEN_SECRET: secret
-     REFRESH_TOKEN_SECRET: refreshsecret
-     CORS_ORIGIN: http://localhost:5173
-     CLOUDINARY_CLOUD_NAME: dft6plppq
-     CLOUDINARY_API_KEY: 969212669239411
-     CLOUDINARY_API_SECRET: RReTfRfCX0mpoWbWQHHm5Wu3e5k
-     CLOUDINARY_URL: cloudinary://969212669239411:RReTfRfCX0mpoWbWQHHm5Wu3e5k@dft6plppq
+     ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
+     REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
+     CORS_ORIGIN: ${CORS_ORIGIN}
+     CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME}
+     CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY}
+     CLOUDINARY_API_SECRET: ${CLOUDINARY_API_SECRET}
+     CLOUDINARY_URL: ${CLOUDINARY_URL}

Create a .env.local file (not committed to version control) with actual secrets, or use Docker secrets for production deployments.

🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 38-38: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In docker-compose.yml around lines 33 to 39, several secrets
(ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET and CLOUDINARY_URL) are hardcoded; remove these literal
values and reference environment variables or Docker secrets instead (e.g., use
env_file: .env.local or ${VAR_NAME} placeholders and/or docker secrets for
production). Create a .env.local (gitignored) containing the actual secrets or
configure Docker secrets for production, and update docker-compose.yml to read
those values rather than embedding them in the repository.

Comment on lines +58 to +59
<Route path="/reset-password/:token" element={<ResetPassword />} />
<Route path="/verify-email/:token" element={<VerifyEmail />} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Route parameter mismatch will break token-based flows.

The routes use path parameters (:token) but the ResetPassword and VerifyEmail components read tokens from query parameters using useSearchParams(). This mismatch will cause the token to always be null, breaking email verification and password reset functionality.

Solution 1 (Recommended): Update routes to use query parameters.

-          <Route path="/reset-password/:token" element={<ResetPassword />} />
-          <Route path="/verify-email/:token" element={<VerifyEmail />} />
+          <Route path="/reset-password" element={<ResetPassword />} />
+          <Route path="/verify-email" element={<VerifyEmail />} />

The routes would then be called as /reset-password?token=... and /verify-email?token=....

Solution 2: Update the components to use path parameters.

In ResetPassword.jsx and VerifyEmail.jsx, replace:

-const [searchParams] = useSearchParams();
-const token = searchParams.get("token");
+const { token } = useParams();

And add the import:

+import { useParams } from "react-router-dom";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Route path="/reset-password/:token" element={<ResetPassword />} />
<Route path="/verify-email/:token" element={<VerifyEmail />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/verify-email" element={<VerifyEmail />} />
🤖 Prompt for AI Agents
In frontend/src/App.jsx around lines 58-59, the routes declare tokens as path
params ("/reset-password/:token" and "/verify-email/:token") but the components
expect query params, so tokens will be null; fix by either (recommended)
changing these routes to use query parameters (remove ":token" so paths become
"/reset-password" and "/verify-email") so callers use /reset-password?token=...
and /verify-email?token=..., or update ResetPassword.jsx and VerifyEmail.jsx to
read path params instead by importing useParams from react-router-dom and
replacing the useSearchParams token lookup with const { token } = useParams();
ensure imports are added and tests/links updated accordingly.

Comment on lines +24 to +52
} = useListings();

return (
<div className="container py-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Browse Listings
</h1>
<p className="text-gray-600">
Discover amazing deals from your fellow students
</p>
</div>

{/* Search & Sort Bar */}
<div className="mb-6 flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<SearchBar
value={filters.search}
onChange={(value) => updateFilters({ search: value })}
placeholder="Search listings by title, description..."
/>
</div>
<div className="sm:w-64">
<SortDropdown
value={filters.sortBy}
onChange={(value) => updateFilters({ sortBy: value })}
/>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Show the real result count in the filter banner

The banner uses listings.length, so once pagination kicks in it always says “12 listings found” even if there are more pages. Surface the total count from pagination (fallback to the current page size when unavailable) to keep the messaging accurate.

   const {
     listings,
     loading,
     error,
     pagination,
     filters,
     updateFilters,
     clearFilters,
     changePage,
     changeItemsPerPage,
   } = useListings();
+
+  const totalResults =
+    pagination.totalItems && pagination.totalItems > 0
+      ? pagination.totalItems
+      : listings.length;-                  {listings.length}{" "}
-                  {listings.length === 1 ? "listing" : "listings"} found
+                  {totalResults}{" "}
+                  {totalResults === 1 ? "listing" : "listings"} found

Also applies to: 78-81

🤖 Prompt for AI Agents
In frontend/src/components/listings/BrowseListings.jsx around lines 24 to 52
(and also update the similar banner at lines ~78-81), the filter banner
currently displays listings.length which breaks when pagination is used; change
it to show the total result count from the pagination data (e.g.,
pagination.total or totalCount) and fall back to pagination.pageSize or
listings.length if total is undefined — update the displayed value and any
related text to use that totalCount variable so the banner reflects the real
total across pages.


// Handle empty or single image
const hasMultipleImages = images.length > 1;
const currentImage = images[currentIndex] || "/placeholder-image.jpg";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify placeholder image exists.

The code references /placeholder-image.jpg as a fallback. Ensure this file exists in the public directory, otherwise the fallback will show a broken image icon.

Run this script to check if the placeholder exists:


🏁 Script executed:

#!/bin/bash
# Check if placeholder image exists
fd -t f "placeholder-image.jpg" --base-directory frontend/public

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Check if frontend/public directory exists and list its contents
echo "=== Checking frontend/public directory ==="
if [ -d "frontend/public" ]; then
  ls -la frontend/public | head -20
else
  echo "frontend/public directory does not exist"
fi

echo ""
echo "=== Searching for any placeholder or fallback images ==="
fd -t f "placeholder\|fallback" frontend/public

echo ""
echo "=== Checking ImageGallery.jsx for placeholder references ==="
rg -n "placeholder" frontend/src/components/listings/ImageGallery.jsx

Length of output: 396



---

🏁 Script executed:

```shell
#!/bin/bash
# Find ImageGallery.jsx file
echo "=== Searching for ImageGallery.jsx ==="
fd -t f "ImageGallery.jsx" -o "ImageGallery.tsx"

echo ""
echo "=== Checking overall directory structure ==="
find . -maxdepth 3 -type d \( -name "frontend" -o -name "public" -o -name "components" \) | head -20

echo ""
echo "=== Searching for placeholder image references in codebase ==="
rg -n "placeholder-image\|placeholder.jpg\|placeholder.png" --type=jsx --type=js --type=tsx --type=ts

Length of output: 383


🏁 Script executed:

#!/bin/bash
# Find ImageGallery files with correct syntax
echo "=== Searching for ImageGallery files ==="
fd "ImageGallery" --type f

echo ""
echo "=== Listing frontend/public directory contents ==="
ls -la frontend/public/

echo ""
echo "=== Searching for placeholder references in all files ==="
rg "placeholder-image" --type js --type tsx --type ts

Length of output: 398


🏁 Script executed:

#!/bin/bash
# View ImageGallery.jsx around line 14
echo "=== Viewing ImageGallery.jsx around line 14 ==="
sed -n '10,20p' frontend/src/components/listings/ImageGallery.jsx

Length of output: 406


Create the missing placeholder image file or update the fallback path.

The code references /placeholder-image.jpg as a fallback, but this file does not exist in frontend/public/. Only vite.svg is present. Either create the placeholder image file in frontend/public/ or update the fallback to reference an existing asset.

🤖 Prompt for AI Agents
In frontend/src/components/listings/ImageGallery.jsx around line 14, the
fallback path "/placeholder-image.jpg" points to a non-existent file in
frontend/public; either add the placeholder-image.jpg file into frontend/public
(matching the referenced filename) or change the fallback to an existing public
asset (for example "/vite.svg" or another valid asset path), and ensure any
import/usage elsewhere matches the updated filename.


const HomePage = () => {
return (
<div className="min-h-screen bg-linear-gradient-to-br from-blue-50 to-indigo-100">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid Tailwind CSS class name.

The class bg-linear-gradient-to-br is invalid. Tailwind uses bg-gradient-to-br for gradient backgrounds. This typo prevents the gradient from rendering.

Apply this diff:

-    <div className="min-h-screen bg-linear-gradient-to-br from-blue-50 to-indigo-100">
+    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="min-h-screen bg-linear-gradient-to-br from-blue-50 to-indigo-100">
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
🤖 Prompt for AI Agents
In frontend/src/pages/HomePage.jsx around line 6, the Tailwind class name
`bg-linear-gradient-to-br` is invalid; replace it with the correct
`bg-gradient-to-br` so the gradient renders (keep the existing `from-blue-50
to-indigo-100` classes as-is).

Comment on lines +44 to +45
const response = await listingService.getListingById(id);
setListing(response.data);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stop discarding the listing payload

listingService.getListingById already returns the response body, so dereferencing .data here leaves listing as undefined. The page immediately falls into the error state even for valid IDs. Use the payload directly.

-      const response = await listingService.getListingById(id);
-      setListing(response.data);
+      const listingResponse = await listingService.getListingById(id);
+      setListing(listingResponse.data ?? listingResponse);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await listingService.getListingById(id);
setListing(response.data);
const listingResponse = await listingService.getListingById(id);
setListing(listingResponse.data ?? listingResponse);
🤖 Prompt for AI Agents
In frontend/src/pages/listings/ListingsDetails.jsx around lines 44-45, the code
dereferences .data on the result of listingService.getListingById which already
returns the payload, so setListing(response.data) yields undefined; change it to
setListing(response) (i.e., pass the service return value directly) and remove
the .data access so the component receives the actual listing payload.

Comment on lines +150 to +206
const conditionColors = {
"brand-new": "bg-green-100 text-green-800",
"like-new": "bg-blue-100 text-blue-800",
good: "bg-yellow-100 text-yellow-800",
fair: "bg-orange-100 text-orange-800",
poor: "bg-red-100 text-red-800",
};

const statusColors = {
available: "bg-green-100 text-green-800",
sold: "bg-gray-100 text-gray-800",
reserved: "bg-yellow-100 text-yellow-800",
};

return (
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
{/* Breadcrumb */}
<Breadcrumb items={breadcrumbItems} />

{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column - Images & Details */}
<div className="lg:col-span-2 space-y-6">
{/* Image Gallery */}
<ImageGallery images={listing.images} title={listing.title} />

{/* Listing Info Card */}
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
{/* Title & Price */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900 mb-3">
{listing.title}
</h1>
<div className="flex items-center gap-3 flex-wrap">
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
statusColors[listing.status]
}`}
>
{listing.status.charAt(0).toUpperCase() +
listing.status.slice(1)}
</span>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
conditionColors[listing.condition]
}`}
>
{listing.condition
.split("-")
.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</span>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a color mapping for new listings

conditionColors doesn’t cover the "new" value emitted by the filters, so the chip renders with a literal "undefined" class. Add the missing key (and keep a sensible fallback) to preserve the badge styling.

-  const conditionColors = {
-    "brand-new": "bg-green-100 text-green-800",
-    "like-new": "bg-blue-100 text-blue-800",
-    good: "bg-yellow-100 text-yellow-800",
-    fair: "bg-orange-100 text-orange-800",
-    poor: "bg-red-100 text-red-800",
-  };
+  const conditionColors = {
+    new: "bg-green-100 text-green-800",
+    "brand-new": "bg-green-100 text-green-800",
+    "like-new": "bg-blue-100 text-blue-800",
+    good: "bg-yellow-100 text-yellow-800",
+    fair: "bg-orange-100 text-orange-800",
+    poor: "bg-red-100 text-red-800",
+  };
+
+  const conditionClass =
+    conditionColors[listing.condition] || "bg-gray-100 text-gray-800";-                    <span
-                      className={`px-3 py-1 rounded-full text-sm font-medium ${
-                        conditionColors[listing.condition]
-                      }`}
-                    >
+                    <span
+                      className={`px-3 py-1 rounded-full text-sm font-medium ${conditionClass}`}
+                    >
🤖 Prompt for AI Agents
frontend/src/pages/listings/ListingsDetails.jsx around lines 150-206: the
conditionColors map is missing a "new" key causing the badge to get an undefined
class; add a "new" entry (e.g., "new": "bg-green-100 text-green-800") to
conditionColors and ensure the component uses a safe fallback when indexing the
map (e.g., default to a fallback class like "bg-gray-100 text-gray-800" if
conditionColors[listing.condition] is undefined).

getAllListings: async (params = {}) => {
try {
const response = await api.get("/listings", { params });
console.log("API Response:", response.data);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove console.log from production code.

Logging API responses to the console in production can expose sensitive data and create noise. Either remove this debug statement or wrap it in a development-only check.

Apply this diff:

      const response = await api.get("/listings", { params });
-      console.log("API Response:", response.data);
       return response.data;

Or wrap it in a development check:

      const response = await api.get("/listings", { params });
-      console.log("API Response:", response.data);
+      if (import.meta.env.DEV) {
+        console.log("API Response:", response.data);
+      }
       return response.data;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/services/listingService.js around line 16, remove the
console.log("API Response:", response.data) from production code; either delete
the statement or guard it with a development-only check (e.g.,
process.env.NODE_ENV === "development") so API responses are only logged in dev
builds, keeping production free of sensitive/noisy console output.

Comment on lines +1 to +16
# Test Credentials for Campus Marketplace

## Authentication Testing

All users have been successfully registered and tested for login functionality.

### Test Users

| Role | Username | Email | Password | Status |
| ------------ | -------------- | ----------------------------- | ------------- | ---------- |
| Student | `student_demo` | `student_demo@university.edu` | `password123` | ✅ Working |
| Vendor Admin | `vendor_demo` | `vendor_demo@university.edu` | `password123` | ✅ Working |
| Club Admin | `club_demo` | `club_demo@university.edu` | `password123` | ✅ Working |
| Moderator | `mod_demo` | `mod_demo@university.edu` | `password123` | ✅ Working |
| Admin | `admin_demo` | `admin_demo@university.edu` | `password123` | ✅ Working |

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add prominent security warning for test credentials.

While this documentation is helpful for development, it should include an explicit warning that these credentials are for development/testing only and should never be used in production environments.

Add this warning at the top of the document:

 # Test Credentials for Campus Marketplace
 
+> ⚠️ **SECURITY WARNING**: These credentials are for **LOCAL DEVELOPMENT AND TESTING ONLY**. 
+> Never use these credentials, passwords, or patterns in production environments.
+> All production credentials must be strong, unique, and securely managed.
+
 ## Authentication Testing
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Test Credentials for Campus Marketplace
## Authentication Testing
All users have been successfully registered and tested for login functionality.
### Test Users
| Role | Username | Email | Password | Status |
| ------------ | -------------- | ----------------------------- | ------------- | ---------- |
| Student | `student_demo` | `student_demo@university.edu` | `password123` | ✅ Working |
| Vendor Admin | `vendor_demo` | `vendor_demo@university.edu` | `password123` | ✅ Working |
| Club Admin | `club_demo` | `club_demo@university.edu` | `password123` | ✅ Working |
| Moderator | `mod_demo` | `mod_demo@university.edu` | `password123` | ✅ Working |
| Admin | `admin_demo` | `admin_demo@university.edu` | `password123` | ✅ Working |
# Test Credentials for Campus Marketplace
> ⚠️ **SECURITY WARNING**: These credentials are for **LOCAL DEVELOPMENT AND TESTING ONLY**.
> Never use these credentials, passwords, or patterns in production environments.
> All production credentials must be strong, unique, and securely managed.
## Authentication Testing
All users have been successfully registered and tested for login functionality.
### Test Users
| Role | Username | Email | Password | Status |
| ------------ | -------------- | ----------------------------- | ------------- | ---------- |
| Student | `student_demo` | `student_demo@university.edu` | `password123` | ✅ Working |
| Vendor Admin | `vendor_demo` | `vendor_demo@university.edu` | `password123` | ✅ Working |
| Club Admin | `club_demo` | `club_demo@university.edu` | `password123` | ✅ Working |
| Moderator | `mod_demo` | `mod_demo@university.edu` | `password123` | ✅ Working |
| Admin | `admin_demo` | `admin_demo@university.edu` | `password123` | ✅ Working |
🤖 Prompt for AI Agents
In markdown/TEST_CREDENTIALS.md around lines 1 to 16, add a prominent security
warning at the very top of the document stating that the listed credentials are
for development/testing only and must never be used in production; make it
visually obvious (e.g., a bold/uppercase WARNING or callout block), include
brief guidance to rotate/change these credentials, store real secrets securely
(e.g., environment variables or a secret manager), and remove or replace test
credentials before any production deployment or public release.

@Rahul5977 Rahul5977 closed this Nov 5, 2025
@Rahul5977 Rahul5977 reopened this Nov 5, 2025
@Rahul5977
Copy link
Member Author

Page: Listings/CreateListing.jsx

Key Features

  • Multi-step form wizard guiding users through four structured steps:
  1. Basic Information — Title, description, category, condition
  2. Pricing & Details — Price, original price, negotiability
  3. Image Upload — Drag & drop support, multiple files, preview, reorder, and delete options
  4. Location Details — Hostel, room, and additional information
  • Real-time validation with clear, user-friendly error messages.
  • Image preview functionality, including drag & drop upload and reorder/delete actions.
  • Save as Draft option for partially completed listings.
  • Role-based access control (RBAC) implemented for secure listing creation.
  • Form schema validation powered by Zod for robustness and consistency.

Form Schema (Zod)

const createListingSchema = z.object({
  title: z.string().min(3, "Title must be at least 3 characters"),
  description: z.string().min(10, "Description must be at least 10 characters"),
  price: z.number().min(0, "Price must be positive"),
  category: z.string().min(1, "Category is required"),
  condition: z.enum(["new", "like-new", "good", "fair", "poor"]),
  location: z.object({
    hostel: z.string(),
    room: z.string().optional()
  }),
  images: z.array(z.instanceof(File)).min(1, "At least one image required")
});

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
frontend/src/utils/validation.js (1)

18-27: Security: Password strength validation removed; update JSDoc.

The password validator now only checks for presence, allowing weak passwords like "1" or "a". This significantly degrades security posture and increases the risk of account compromise. If password strength validation has been moved to the backend, consider still enforcing basic client-side requirements for better UX and early feedback.

Additionally, the JSDoc comment at line 18 still states "Validate password strength" but the implementation no longer validates strength—only presence.

If password strength validation is required, apply this diff to restore minimum security requirements:

   /**
-   * Validate password strength
+   * Validate password format and strength
    * @param {string} password - Password to validate
    * @returns {string|null} Error message if invalid, null if valid
    */
   password: (password) => {
     if (!password) {
       return "Password is required";
     }
+    if (password.length < 8) {
+      return "Password must be at least 8 characters long";
+    }
+    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
+      return "Password must contain at least one uppercase letter, one lowercase letter, and one number";
+    }
     return null;
   },

If password validation is intentionally handled server-side only, update the JSDoc to reflect this:

   /**
-   * Validate password strength
+   * Validate password presence (strength validation performed server-side)
    * @param {string} password - Password to validate
    * @returns {string|null} Error message if invalid, null if valid
    */
README.md (3)

3-3: Fix markdown heading syntax error.

The heading has an extra "A" character before the ## marker, which breaks the markdown formatting.

Apply this fix:

-A## 📁 Project Structure
+## 📁 Project Structure

91-91: Remove stray text.

The word "dominos" appears to be leftover text that should be removed.

Apply this fix:

 - 📱 **Mobile-first design**: Built as a progressive web app or native mobile app for quick access.
-- dominos

61-108: Remove duplicate "Project Status" section.

There are two "Project Status" sections (lines 61 and 103). The first one at line 61 appears to be the updated version with current progress, while the second at line 103 contains outdated text about the project being "early-stage" and "not yet initialized."

Remove the duplicate outdated section:

 ---
 
 ## 📦 Project Status
 
-This is an early-stage idea and the repo is not yet initialized.
-We're adding this README so anyone interested can **pick up the project** and start building.
-
----
-
 ## 🤝 How to Contribute
♻️ Duplicate comments (24)
backend/cookies.txt (1)

1-6: CRITICAL: Remove real JWT tokens from version control immediately.

This file contains actual JWT tokens that grant authentication access, which is a critical security vulnerability.

This issue was already flagged in a previous review. Please address it by:

  1. Removing this file using git rm --cached backend/cookies.txt
  2. Adding appropriate patterns to .gitignore
  3. Rotating/invalidating the exposed tokens
  4. Using environment variables or secure secret management instead
frontend/src/pages/HomePage.jsx (1)

6-6: Fix invalid Tailwind CSS class name.

The class bg-linear-gradient-to-br is not valid Tailwind syntax. According to the Tailwind CSS documentation, gradient backgrounds use bg-gradient-to-* format.

Apply this fix:

-    <div className="min-h-screen bg-linear-gradient-to-br from-blue-50 to-indigo-100">
+    <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
markdown/TEST_CREDENTIALS.md (1)

1-16: Add prominent security warning for test credentials.

This documentation should include an explicit warning that these credentials are for development/testing only and must never be used in production environments.

Add a security warning at the top:

 # Test Credentials for Campus Marketplace
 
+> ⚠️ **SECURITY WARNING**: These credentials are for **LOCAL DEVELOPMENT AND TESTING ONLY**. 
+> Never use these credentials, passwords, or patterns in production environments.
+> All production credentials must be strong, unique, and securely managed.
+
 ## Authentication Testing
frontend/src/components/listings/SellerInfoCard.jsx (2)

30-30: Fix CSS class typo: linera should be gradient.

The class name bg-linera-to-br is misspelled and should be bg-gradient-to-br. This will prevent the gradient background from rendering correctly.

Apply this diff:

-        <div className="w-16 h-16 bg-linera-to-br from-blue-600 to-indigo-600 rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg">
+        <div className="w-16 h-16 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg">

90-90: Fix CSS class typo: linear should be gradient.

The class name bg-linear-to-r is misspelled and should be bg-gradient-to-r. This will prevent the button gradient background from rendering correctly.

Apply this diff:

-        className="w-full py-3 px-4 bg-linear-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
+        className="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200"
frontend/src/components/ui/ShareButton.jsx (3)

8-11: Add defensive checks for the listing prop.

The component directly destructures listing.title and listing.price without validating that listing exists. If the component is rendered without a listing prop or with a null/undefined value, it will throw a runtime error.

Add a guard clause at the beginning of the component:

 const ShareButtons = ({ listing }) => {
+  if (!listing) return null;
+
   const shareUrl = window.location.href;
   const shareTitle = listing.title;
   const shareText = `Check out this ${listing.title} for ₹${listing.price}!`;

13-16: Add error handling for clipboard API.

The Clipboard API requires a secure context (HTTPS) and may not be available in all browsers. The current implementation lacks error handling, which could lead to unhandled promise rejections.

Apply this diff to add proper error handling:

 const copyLink = () => {
-    navigator.clipboard.writeText(shareUrl);
-    toast.success("Link copied to clipboard!");
+    navigator.clipboard
+      .writeText(shareUrl)
+      .then(() => {
+        toast.success("Link copied to clipboard!");
+      })
+      .catch(() => {
+        toast.error("Failed to copy link. Please copy manually.");
+      });
   };

32-36: Email sharing redirects the page instead of opening a new window.

Unlike the Facebook and Twitter share methods that open new windows, shareViaEmail assigns to window.location.href, which navigates the current page away. This creates an inconsistent and potentially frustrating user experience.

Consider opening mailto in a way that doesn't disrupt navigation, or inform users with a toast message first:

 const shareViaEmail = () => {
     const subject = encodeURIComponent(`Check out: ${shareTitle}`);
     const body = encodeURIComponent(`${shareText}\n\n${shareUrl}`);
-    window.location.href = `mailto:?subject=${subject}&body=${body}`;
+    window.open(`mailto:?subject=${subject}&body=${body}`, "_self");
   };
frontend/src/components/ui/Pagination.jsx (1)

116-116: Fix invalid Tailwind class syntax.

The class min-w-2.5rem is not valid Tailwind CSS syntax. For arbitrary values, use square brackets.

Apply this diff:

-              "min-w-2.5rem h-10 px-3 rounded-md text-sm font-medium transition-colors",
+              "min-w-[2.5rem] h-10 px-3 rounded-md text-sm font-medium transition-colors",
frontend/src/components/listings/BrowseListings.jsx (1)

78-79: Show the real result count in the filter banner.

The banner displays listings.length, which only shows the current page size rather than the total number of matching listings. Use pagination.totalItems to show the accurate total count across all pages.

Apply this diff:

+  const totalResults = pagination.totalItems || listings.length;
+
   return (
     <div className="container py-8">
…
               <span className="font-medium">
-                  {listings.length}{" "}
-                  {listings.length === 1 ? "listing" : "listings"} found
+                  {totalResults}{" "}
+                  {totalResults === 1 ? "listing" : "listings"} found
               </span>
frontend/src/components/listings/RelatedListings.jsx (1)

49-49: Match loading skeleton count to the limit prop.

The loading state renders 4 skeleton cards but the component accepts a limit prop that could be different. Use the limit prop to ensure the skeleton count matches the expected number of items.

Apply this diff:

-          {[...Array(4)].map((_, i) => (
+          {[...Array(limit)].map((_, i) => (
frontend/src/App.jsx (1)

59-60: Route parameter mismatch will break token-based flows.

The routes use path parameters (:token) but the ResetPassword and VerifyEmail components read tokens from query parameters using useSearchParams(). This mismatch will cause the token to always be null, breaking email verification and password reset functionality.

Solution 1 (Recommended): Update routes to use query parameters.

-          <Route path="/reset-password/:token" element={<ResetPassword />} />
-          <Route path="/verify-email/:token" element={<VerifyEmail />} />
+          <Route path="/reset-password" element={<ResetPassword />} />
+          <Route path="/verify-email" element={<VerifyEmail />} />

The routes would then be called as /reset-password?token=... and /verify-email?token=....

Solution 2: Update the components to use path parameters.

In ResetPassword.jsx and VerifyEmail.jsx, replace:

-const [searchParams] = useSearchParams();
-const token = searchParams.get("token");
+const { token } = useParams();

And add the import:

+import { useParams } from "react-router-dom";
frontend/src/pages/listings/ListingsDetails.jsx (2)

44-45: Stop discarding the listing payload.

listingService.getListingById already returns response.data, so dereferencing .data here leaves listing as undefined. The page will immediately fall into the error state even for valid IDs.

Apply this diff:

       const response = await listingService.getListingById(id);
-      setListing(response.data);
+      setListing(response.data ?? response);

150-156: Add a color mapping for new listings.

The conditionColors object doesn't include the "new" value that can be emitted by the filters, so the condition badge will render with an undefined class. Add the missing key and use a fallback for unknown conditions.

Apply this diff:

   const conditionColors = {
+    new: "bg-green-100 text-green-800",
     "brand-new": "bg-green-100 text-green-800",
     "like-new": "bg-blue-100 text-blue-800",
     good: "bg-yellow-100 text-yellow-800",
     fair: "bg-orange-100 text-orange-800",
     poor: "bg-red-100 text-red-800",
   };
+
+  const conditionClass =
+    conditionColors[listing.condition] || "bg-gray-100 text-gray-800";

Then update the badge to use the fallback:

                     <span
-                      className={`px-3 py-1 rounded-full text-sm font-medium ${
-                        conditionColors[listing.condition]
-                      }`}
+                      className={`px-3 py-1 rounded-full text-sm font-medium ${conditionClass}`}
                     >
frontend/src/components/listings/ImageGallery.jsx (1)

14-14: Create the missing placeholder image or update the fallback path.

The code references /placeholder-image.jpg as a fallback, but this file does not exist in frontend/public/. Either create the placeholder image file or update the fallback to reference an existing asset.

Run this script to check what assets are available:

#!/bin/bash
# List contents of frontend/public directory
ls -la frontend/public/

# Search for any image files that could serve as placeholder
fd -e jpg -e png -e svg . frontend/public/

Consider using an existing asset like /vite.svg or adding a proper placeholder image:

-  const currentImage = images[currentIndex] || "/placeholder-image.jpg";
+  const currentImage = images[currentIndex] || "/vite.svg";
frontend/src/components/listings/ListingCard.jsx (1)

33-33: Restore the missing space in the aspect ratio class.

Line 33 still reads className="relative aspect-4/3overflow-hidden ..." so Tailwind sees a single invalid token and the aspect ratio styling never applies. Please insert the space between aspect-4/3 and overflow-hidden so the skeleton/card layout renders correctly.

-        <div className="relative aspect-4/3overflow-hidden bg-gray-100">
+        <div className="relative aspect-4/3 overflow-hidden bg-gray-100">
frontend/src/components/listings/ListingGrid.jsx (1)

18-18: Fix the invalid aspect ratio utility.

aspect-4/3 isn’t recognized by Tailwind; the skeleton falls back to auto height. Please switch to the bracket form (e.g., aspect-[4/3]) so the placeholders match the card layout.

-            <div className="aspect-4/3 bg-gray-200" />
+            <div className="aspect-[4/3] bg-gray-200" />
frontend/src/hooks/useListings.js (1)

61-90: Keep pagination stable for array responses.

Resetting pagination to { currentPage: 1, itemsPerPage: 12 } and emitting the whole array ignores the user’s chosen page size and breaks the paging UI (exactly the regression flagged earlier). Slice the array locally while preserving the existing pagination state.

-      let listingsData = [];
-      let paginationData = {
-        currentPage: 1,
-        totalPages: 1,
-        totalItems: 0,
-        itemsPerPage: 12,
-      };
+      let listingsData = [];
+      let paginationData = {
+        currentPage: pagination.currentPage,
+        totalPages: pagination.totalPages,
+        totalItems: 0,
+        itemsPerPage: pagination.itemsPerPage,
+      };
@@
-        else if (Array.isArray(response.data)) {
-          listingsData = response.data;
-          paginationData.totalItems = response.data.length;
-        }
+        else if (Array.isArray(response.data)) {
+          const totalItems = response.data.length;
+          const itemsPerPage = pagination.itemsPerPage;
+          const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
+          const currentPage = Math.min(pagination.currentPage, totalPages);
+          const startIndex = (currentPage - 1) * itemsPerPage;
+          const endIndex = startIndex + itemsPerPage;
+
+          listingsData = response.data.slice(startIndex, endIndex);
+          paginationData = {
+            currentPage,
+            totalPages,
+            totalItems,
+            itemsPerPage,
+          };
+        }
frontend/src/pages/auth/Register.jsx (5)

80-99: Use valid Tailwind gradient utilities.

bg-linear-to-br isn’t a Tailwind class (so the gradient never renders). Switch to bg-gradient-to-br for both containers.

-    <div className="min-h-screen flex items-center justify-center bg-linear-to-br from-slate-50 via-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
+    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 py-12 px-4 sm:px-6 lg:px-8">
@@
-            <div className="mx-auto w-16 h-16 bg-linear-to-br from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
+            <div className="mx-auto w-16 h-16 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">

188-199: Add an aria-label to the password toggle.

Screen readers still can’t announce what this button does. Provide a dynamic aria-label so the action is clear.

       <button
         type="button"
         onClick={() => setShowPassword(!showPassword)}
+        aria-label={showPassword ? "Hide password" : "Show password"}
         className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
       >

225-236: Add an aria-label to the confirm-password toggle.

Same accessibility issue here—announce the action for assistive tech.

       <button
         type="button"
         onClick={() => setShowConfirmPassword(!showConfirmPassword)}
+        aria-label={
+          showConfirmPassword ? "Hide confirm password" : "Show confirm password"
+        }
         className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
       >

112-170: Associate helper and error text with inputs.

Inputs still lack aria-describedby / aria-invalid wiring, so assistive tech can’t pick up the inline guidance or errors (raised previously). Please wire the inputs to their messages; here’s the pattern for one field—replicate across name, username, email, password, and confirm password.

                  <input
                     name="username"
                     type="text"
                     value={values.username}
                     onChange={handleChange}
                     placeholder="johndoe"
                     required
+                    aria-invalid={!!errors.username}
+                    aria-describedby={
+                      errors.username ? "username-error" : "username-help"
+                    }
                     className="w-full pl-11 pr-4 py-2.5 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-900 placeholder-gray-400 transition-colors"
                   />
                 </div>
                 {errors.username ? (
-                  <p className="mt-1 text-sm text-red-600">{errors.username}</p>
+                  <p id="username-error" className="mt-1 text-sm text-red-600">
+                    {errors.username}
+                  </p>
                 ) : (
-                  <p className="mt-1 text-xs text-gray-500">
+                  <p id="username-help" className="mt-1 text-xs text-gray-500">
                     Letters, numbers, and underscores only
                   </p>
                 )}

255-283: Fix the submit button gradient class.

bg-linear-to-r is invalid Tailwind; the gradient never shows. Switch to bg-gradient-to-r.

             <button
               type="submit"
               disabled={isSubmitting}
-              className="w-full py-3 px-4 bg-linear-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
+              className="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
             >
frontend/src/services/listingService.js (1)

14-21: Drop the production console.log.

Logging every listings response in production is noisy and can leak data. Remove it or guard it so it only runs in development.

   getAllListings: async (params = {}) => {
     try {
       const response = await api.get("/listings", { params });
-      console.log("API Response:", response.data);
       return response.data;
     } catch (error) {
🧹 Nitpick comments (15)
frontend/src/components/listings/steps/PricingStep.jsx (1)

59-79: Consider handling edge cases in discount calculation.

The discount badge displays when originalPrice > price, but edge cases could produce awkward results:

  • A 1-rupee difference could show as "0% OFF" after rounding
  • Very small discounts (< 1%) might confuse users

Consider adding a minimum discount threshold (e.g., 5%) before showing the badge, or displaying actual savings when the percentage rounds to 0.

Example enhancement:

+      const discountPercent = Math.round(
+        ((formData.originalPrice - formData.price) / formData.originalPrice) * 100
+      );
+
       {formData.originalPrice > 0 &&
         formData.price > 0 &&
-        formData.originalPrice > formData.price && (
+        formData.originalPrice > formData.price &&
+        discountPercent >= 1 && (
           <div className="p-4 bg-green-50 border border-green-200 rounded-lg">
             <div className="flex items-center gap-2 text-green-800">
               <Percent className="h-5 w-5" />
               <span className="font-semibold">
-                {Math.round(
-                  ((formData.originalPrice - formData.price) /
-                    formData.originalPrice) *
-                    100
-                )}
+                {discountPercent}
                 % OFF
               </span>
frontend/src/components/listings/SortDropdown.jsx (1)

22-52: Handle missing option gracefully.

If value doesn't match any option in sortOptions, currentOption will be undefined, causing the trigger button to display "Sort by: undefined". While optional chaining prevents crashes, this creates a poor user experience.

Consider providing a fallback:

   // Get current option label
-  const currentOption = sortOptions.find((opt) => opt.value === value);
+  const currentOption = sortOptions.find((opt) => opt.value === value) || sortOptions[0];

Alternatively, validate the value prop or add a default label:

         <span className="text-sm font-medium text-gray-700">
-          Sort by: {currentOption?.label}
+          Sort by: {currentOption?.label || "Select option"}
         </span>
.gitignore (1)

198-202: Minor redundancy in .env patterns.

Lines 199 and 201 duplicate patterns already covered at lines 112 and 116. The broader patterns at lines 200 and 202 add value, but consider removing the exact duplicates.

Apply this diff to remove redundant entries:

 # Environment files (keep .env files private!)
-.env
 **/.env
-.env.local
 .env.*.local
frontend/Dockerfile (1)

14-18: Consider adding a production build variant.

This Dockerfile runs the Vite dev server, which is appropriate for development. For production deployments, consider creating a multi-stage build that produces optimized static assets and serves them with nginx or another production server.

Example production Dockerfile structure:

# Build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
backend/Dockerfile (1)

17-21: Consider production readiness and security hardening.

This Dockerfile runs the development server as root. For production deployments, consider:

  1. Using a non-root user
  2. Installing production dependencies only (npm ci --only=production)
  3. Setting NODE_ENV=production
  4. Running the production start script instead of dev

Example production additions:

# Install only production dependencies
RUN npm ci --only=production

# Create non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

# Set production environment
ENV NODE_ENV=production

# Start production server
CMD ["npm", "start"]
docker-compose.yml (2)

64-65: Consider making VITE_API_BASE_URL configurable.

While hardcoding http://localhost:3000/api is reasonable for local development, externalizing it as ${VITE_API_BASE_URL:-http://localhost:3000/api} would provide flexibility for different environments or host configurations.

Apply this diff:

-      VITE_API_BASE_URL: http://localhost:3000/api
+      VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:3000/api}

70-71: Consider adding a health condition for frontend dependency.

The frontend depends on the backend but doesn't wait for it to be healthy. Adding condition: service_healthy would ensure the backend is fully ready before the frontend starts, preventing initial API call failures.

Apply this diff:

     depends_on:
-      - backend
+      backend:
+        condition: service_healthy
frontend/src/components/listings/steps/LocationStep.jsx (2)

1-1: Remove unused icon imports.

The icons MapPin, Home, Hash, and FileText are imported but never used in the component. Removing them will keep the code clean and reduce bundle size.

Apply this diff:

-import { MapPin, Home, Hash, FileText } from "lucide-react";

4-11: Consider using the defined hostels array or remove it.

The hostels array is defined but never used. If the hostel field should be a dropdown with predefined options, consider implementing it. Otherwise, remove the unused array to keep the code clean.

If you want to implement a dropdown, apply this diff:

       <div>
         <label className="block text-sm font-semibold text-gray-800 mb-2">
           Hostel
         </label>
-        <input
-          type="text"
-          name="hostel"
-          value={formData.hostel}
-          onChange={onChange}
-          placeholder="(Optional) Hostel name"
-          className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 text-gray-900 placeholder-gray-500 font-medium border-gray-300"
-        />
+        <select
+          name="hostel"
+          value={formData.hostel}
+          onChange={onChange}
+          className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-gray-50 text-gray-900 font-medium border-gray-300"
+        >
+          <option value="">(Optional) Select hostel</option>
+          {hostels.map((hostel) => (
+            <option key={hostel} value={hostel}>
+              {hostel}
+            </option>
+          ))}
+        </select>
       </div>

Or simply remove the unused array:

-  const hostels = [
-    "Hostel A",
-    "Hostel B",
-    "Hostel C",
-    "Hostel D",
-    "Off-Campus",
-    "Faculty Housing",
-  ];
-
frontend/src/schemas/listingSchema.js (6)

19-21: Consider using Zod v4's unified error parameter.

The errorMap approach is the older Zod v3 style. Zod v4 introduced a unified error parameter that provides more consistent error handling.

Apply this diff to use the v4 pattern:

-  condition: z.enum(['brand-new', 'like-new', 'good', 'fair', 'poor'], {
-    errorMap: () => ({ message: 'Please select a condition' }),
-  }),
+  condition: z.enum(['brand-new', 'like-new', 'good', 'fair', 'poor'], {
+    error: () => 'Please select a condition',
+  }),

26-29: Consider using Zod v4's unified error parameter for number validation.

The invalid_type_error option is the older error handling approach. Zod v4's unified error parameter provides more flexible error customization.

Apply this diff to modernize the error handling:

-  price: z
-    .number({ invalid_type_error: 'Price must be a number' })
-    .min(0, 'Price must be positive')
-    .max(1000000, 'Price seems unrealistic'),
+  price: z.number({
+    error: (issue) => {
+      if (issue.code === 'invalid_type') {
+        return 'Price must be a number';
+      }
+    },
+  })
+    .min(0, 'Price must be positive')
+    .max(1000000, 'Price seems unrealistic'),

30-34: Add validation to ensure originalPrice is greater than or equal to price.

When an original price is provided, it should typically be greater than or equal to the current price to properly represent a discount or price history.

You can add a refinement to the pricingSchema after it's defined:

 export const pricingSchema = z.object({
   price: z
     .number({ invalid_type_error: 'Price must be a number' })
     .min(0, 'Price must be positive')
     .max(1000000, 'Price seems unrealistic'),
   originalPrice: z
     .number({ invalid_type_error: 'Original price must be a number' })
     .min(0, 'Original price must be positive')
     .optional()
     .nullable(),
   negotiable: z.boolean().default(true),
-});
+}).refine(
+  (data) => {
+    if (data.originalPrice != null && data.originalPrice < data.price) {
+      return false;
+    }
+    return true;
+  },
+  {
+    message: 'Original price must be greater than or equal to current price',
+    path: ['originalPrice'],
+  }
+);

43-43: Consider using Zod v4's unified error parameter.

The message parameter in z.instanceof uses the older error syntax.

Apply this diff:

-        file: z.instanceof(File, { message: 'Invalid file type' }),
+        file: z.instanceof(File, {
+          error: () => 'Invalid file type',
+        }),

39-49: Add file size and type validation for images.

The current validation only checks that the file is a File instance but doesn't validate file size or MIME type. This could lead to users uploading very large files or non-image files, causing upload failures or poor UX.

Consider adding refinements to validate file size and type:

export const imagesSchema = z.object({
  images: z
    .array(
      z.object({
        file: z.instanceof(File, { message: 'Invalid file type' })
          .refine((file) => file.size <= 5 * 1024 * 1024, {
            message: 'File size must be less than 5MB',
          })
          .refine((file) => file.type.startsWith('image/'), {
            message: 'File must be an image',
          }),
        preview: z.string(),
      })
    )
    .min(1, 'At least one image is required')
    .max(5, 'Maximum 5 images allowed'),
});

54-55: Consider handling empty strings for optional fields.

The hostel and roomNumber fields are marked as optional, but they could still contain empty strings. You might want to transform empty strings to undefined for cleaner data handling.

Apply this diff:

-  hostel: z.string().optional(),
-  roomNumber: z.string().optional(),
+  hostel: z.string().min(1).optional().or(z.literal('')).transform(val => val || undefined),
+  roomNumber: z.string().min(1).optional().or(z.literal('')).transform(val => val || undefined),

Or more simply using a preprocess:

  hostel: z.preprocess((val) => val || undefined, z.string().optional()),
  roomNumber: z.preprocess((val) => val || undefined, z.string().optional()),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c5dda8 and f871fed.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • .env.example (1 hunks)
  • .gitignore (2 hunks)
  • README.md (1 hunks)
  • backend/.dockerignore (1 hunks)
  • backend/Dockerfile (1 hunks)
  • backend/cookies.txt (1 hunks)
  • backend/src/routes/listing.routes.js (1 hunks)
  • docker-compose.yml (1 hunks)
  • frontend/.dockerignore (1 hunks)
  • frontend/Dockerfile (1 hunks)
  • frontend/src/App.jsx (3 hunks)
  • frontend/src/components/auth/ProtectedRoute.jsx (2 hunks)
  • frontend/src/components/listings/BrowseListings.jsx (1 hunks)
  • frontend/src/components/listings/FormProgress.jsx (1 hunks)
  • frontend/src/components/listings/ImageGallery.jsx (1 hunks)
  • frontend/src/components/listings/ImageUpload.jsx (1 hunks)
  • frontend/src/components/listings/LisitingFilters.jsx (1 hunks)
  • frontend/src/components/listings/ListingCard.jsx (1 hunks)
  • frontend/src/components/listings/ListingGrid.jsx (1 hunks)
  • frontend/src/components/listings/RelatedListings.jsx (1 hunks)
  • frontend/src/components/listings/SearchBar.jsx (1 hunks)
  • frontend/src/components/listings/SellerInfoCard.jsx (1 hunks)
  • frontend/src/components/listings/SortDropdown.jsx (1 hunks)
  • frontend/src/components/listings/steps/BasicInfoStep.jsx (1 hunks)
  • frontend/src/components/listings/steps/ImageStep.jsx (1 hunks)
  • frontend/src/components/listings/steps/LocationStep.jsx (1 hunks)
  • frontend/src/components/listings/steps/PricingStep.jsx (1 hunks)
  • frontend/src/components/ui/Breadcrumb.jsx (1 hunks)
  • frontend/src/components/ui/Pagination.jsx (1 hunks)
  • frontend/src/components/ui/ShareButton.jsx (1 hunks)
  • frontend/src/hooks/useListings.js (1 hunks)
  • frontend/src/pages/HomePage.jsx (1 hunks)
  • frontend/src/pages/auth/Login.jsx (3 hunks)
  • frontend/src/pages/auth/Register.jsx (1 hunks)
  • frontend/src/pages/listings/CreateListing.jsx (1 hunks)
  • frontend/src/pages/listings/ListingsDetails.jsx (1 hunks)
  • frontend/src/schemas/listingSchema.js (1 hunks)
  • frontend/src/services/api.js (2 hunks)
  • frontend/src/services/authService.js (2 hunks)
  • frontend/src/services/listingService.js (1 hunks)
  • frontend/src/utils/validation.js (1 hunks)
  • frontend/vite.config.js (1 hunks)
  • markdown/TEST_CREDENTIALS.md (1 hunks)
  • markdown/dbQueries.md (1 hunks)
  • package.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (24)
frontend/src/components/listings/BrowseListings.jsx (6)
frontend/src/hooks/useListings.js (11)
  • useListings (10-161)
  • useListings (10-161)
  • filters (20-29)
  • updateFilters (104-107)
  • clearFilters (112-123)
  • listings (11-11)
  • loading (12-12)
  • error (13-13)
  • pagination (14-19)
  • changePage (128-131)
  • changeItemsPerPage (136-142)
frontend/src/components/listings/SearchBar.jsx (1)
  • SearchBar (9-82)
frontend/src/components/listings/SortDropdown.jsx (1)
  • SortDropdown (9-83)
frontend/src/components/listings/LisitingFilters.jsx (1)
  • ListingFilters (12-243)
frontend/src/components/listings/ListingGrid.jsx (1)
  • ListingGrid (8-77)
frontend/src/components/ui/Pagination.jsx (1)
  • Pagination (8-145)
frontend/src/components/ui/ShareButton.jsx (2)
frontend/src/components/listings/ListingCard.jsx (1)
  • listing (9-19)
frontend/src/pages/listings/ListingsDetails.jsx (1)
  • listing (31-31)
frontend/src/components/listings/ListingGrid.jsx (2)
frontend/src/hooks/useListings.js (3)
  • loading (12-12)
  • error (13-13)
  • listings (11-11)
frontend/src/components/listings/ListingCard.jsx (2)
  • listing (9-19)
  • ListingCard (8-85)
frontend/src/components/listings/steps/BasicInfoStep.jsx (1)
frontend/src/pages/listings/CreateListing.jsx (2)
  • formData (27-44)
  • errors (46-46)
frontend/src/pages/auth/Login.jsx (1)
frontend/src/hooks/useForm.js (5)
  • handleSubmit (36-61)
  • values (8-8)
  • handleChange (16-30)
  • errors (9-9)
  • isSubmitting (10-10)
frontend/src/pages/listings/CreateListing.jsx (8)
frontend/src/components/auth/ProtectedRoute.jsx (1)
  • useAuth (14-14)
frontend/src/components/listings/steps/BasicInfoStep.jsx (1)
  • BasicInfoStep (3-157)
frontend/src/components/listings/steps/PricingStep.jsx (1)
  • PricingStep (3-115)
frontend/src/components/listings/steps/ImageStep.jsx (1)
  • ImageStep (3-40)
frontend/src/components/listings/steps/LocationStep.jsx (1)
  • LocationStep (3-94)
frontend/src/schemas/listingSchema.js (2)
  • stepSchemas (66-71)
  • stepSchemas (66-71)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-117)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/pages/listings/ListingsDetails.jsx (7)
frontend/src/components/listings/RelatedListings.jsx (2)
  • loading (12-12)
  • RelatedListings (10-84)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-117)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/components/ui/Breadcrumb.jsx (1)
  • Breadcrumb (8-35)
frontend/src/components/listings/ImageGallery.jsx (1)
  • ImageGallery (8-154)
frontend/src/components/ui/ShareButton.jsx (1)
  • ShareButtons (8-75)
frontend/src/components/listings/SellerInfoCard.jsx (1)
  • SellerInfoCard (7-108)
frontend/src/components/listings/LisitingFilters.jsx (4)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-117)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/components/ui/Input.jsx (1)
  • Input (4-68)
frontend/src/components/listings/SortDropdown.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/components/listings/SearchBar.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/pages/auth/Register.jsx (1)
frontend/src/hooks/useForm.js (5)
  • handleSubmit (36-61)
  • values (8-8)
  • handleChange (16-30)
  • errors (9-9)
  • isSubmitting (10-10)
backend/src/routes/listing.routes.js (3)
backend/src/middlewares/auth.middleware.js (2)
  • verifyJWT (6-31)
  • verifyJWT (6-31)
backend/src/middlewares/multer.js (4)
  • upload (32-39)
  • upload (32-39)
  • removeLocalFiles (42-52)
  • removeLocalFiles (42-52)
backend/src/utils/upload.js (3)
  • result (16-19)
  • uploadToCloudinary (13-29)
  • uploadToCloudinary (13-29)
frontend/src/components/listings/ListingCard.jsx (2)
frontend/src/pages/listings/ListingsDetails.jsx (2)
  • listing (31-31)
  • conditionColors (150-156)
backend/src/controllers/listing.controller.js (1)
  • images (78-78)
frontend/src/components/listings/steps/LocationStep.jsx (1)
frontend/src/pages/listings/CreateListing.jsx (2)
  • formData (27-44)
  • errors (46-46)
frontend/src/services/listingService.js (3)
frontend/src/services/api.js (2)
  • response (49-53)
  • api (9-15)
backend/src/controllers/listing.controller.js (1)
  • userId (474-474)
frontend/src/pages/listings/CreateListing.jsx (1)
  • formData (27-44)
frontend/src/components/listings/steps/ImageStep.jsx (2)
frontend/src/components/listings/ImageUpload.jsx (1)
  • ImageUpload (10-195)
frontend/src/pages/listings/CreateListing.jsx (2)
  • formData (27-44)
  • errors (46-46)
frontend/src/components/listings/RelatedListings.jsx (2)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-117)
frontend/src/components/listings/ListingCard.jsx (2)
  • listing (9-19)
  • ListingCard (8-85)
frontend/src/services/api.js (1)
frontend/src/services/authService.js (8)
  • response (10-10)
  • response (34-34)
  • response (44-44)
  • response (67-67)
  • response (77-77)
  • response (88-91)
  • response (100-100)
  • response (116-116)
frontend/src/components/listings/steps/PricingStep.jsx (1)
frontend/src/pages/listings/CreateListing.jsx (2)
  • formData (27-44)
  • errors (46-46)
frontend/src/components/ui/Pagination.jsx (1)
frontend/src/utils/cn.js (1)
  • cn (11-13)
frontend/src/App.jsx (4)
frontend/src/pages/auth/ResetPassword.jsx (1)
  • ResetPassword (11-207)
frontend/src/pages/auth/VerifyEmail.jsx (1)
  • VerifyEmail (7-133)
frontend/src/components/layout/AppLayout.jsx (1)
  • AppLayout (7-40)
frontend/src/components/auth/ProtectedRoute.jsx (1)
  • ProtectedRoute (9-46)
frontend/src/hooks/useListings.js (2)
frontend/src/components/listings/BrowseListings.jsx (1)
  • useListings (14-24)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-117)
frontend/src/services/authService.js (1)
frontend/src/services/api.js (1)
  • response (49-53)
frontend/src/components/listings/FormProgress.jsx (1)
frontend/src/pages/listings/CreateListing.jsx (2)
  • steps (49-54)
  • currentStep (22-22)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 14-14: [UnorderedKey] The CLOUDINARY_API_KEY key should go before the CLOUDINARY_CLOUD_NAME key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The CLOUDINARY_API_SECRET key should go before the CLOUDINARY_CLOUD_NAME key

(UnorderedKey)

🪛 Gitleaks (8.28.0)
backend/cookies.txt

[high] 5-5: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


[high] 6-6: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🪛 LanguageTool
markdown/dbQueries.md

[style] ~630-~630: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...t** --- Last Updated: November 6, 2025 Project: Campus Marketplace - IIT...

(MISSING_COMMA_AFTER_YEAR)

markdown/TEST_CREDENTIALS.md

[style] ~221-~221: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...``` --- Last Updated: November 6, 2025 Status: ✅ Docker and Local authen...

(MISSING_COMMA_AFTER_YEAR)

🔇 Additional comments (48)
frontend/src/utils/validation.js (1)

7-7: The review comment is incorrect—no breaking change exists.

The codebase already consistently uses validators.email(...) across all call sites (Register.jsx line 29, Login.jsx line 24, ForgotPassword.jsx line 19). There are no references to the old validators.validateEmail function name, so renaming the validator property does not break any code. The concern about runtime errors is unfounded.

Likely an incorrect or invalid review comment.

frontend/src/components/listings/steps/PricingStep.jsx (1)

3-115: Well-structured pricing form component.

The component effectively handles price input with proper validation error display, optional original price for discount calculation, and clear negotiable selection. The input normalization logic (empty → undefined, parseFloat with NaN handling) is appropriate.

frontend/src/pages/HomePage.jsx (1)

8-87: LGTM on the landing page structure.

The component provides a clear, engaging landing experience with well-organized sections (hero, features grid, CTA) and appropriate navigation links. Responsive design patterns are properly implemented.

markdown/TEST_CREDENTIALS.md (1)

17-223: Comprehensive test documentation.

The document provides clear, well-organized information for authentication testing, including connection details, test users across roles, Docker and local setup instructions, and practical examples. This is valuable for development and onboarding.

frontend/src/components/listings/SortDropdown.jsx (1)

25-83: Well-implemented dropdown component.

The component properly handles outside clicks with event listener cleanup, maintains open/closed state, and provides visual feedback for the selected option. The use of cn utility for conditional classes is appropriate.

markdown/dbQueries.md (2)

633-699: Sample seed data includes hardcoded ObjectIds.

The sample insertMany at the end of the document uses specific ObjectIds for the seller field (e.g., ObjectId("690ba55bd4584dd2215eeb4f")). These IDs must match actual user documents in your database, or the inserts will create orphaned listings.

Ensure these ObjectIds correspond to test users created in your database, or replace them with a note to substitute actual user IDs. Consider adding a comment in the documentation explaining this requirement.

Example clarification:

+// Note: Replace the seller ObjectIds below with actual user IDs from your database
+// You can find user IDs by running: db.users.find({}, { _id: 1, username: 1 })
+
 db.listings.insertMany([
   {

1-632: Excellent MongoDB reference documentation.

This comprehensive guide covers connection setup, user and listing management, analytics queries, maintenance operations, and advanced aggregation pipelines. The organization and examples make it highly practical for development and troubleshooting.

README.md (1)

20-58: Clear and streamlined setup instructions.

The updated Quick Start section provides a straightforward Docker-first approach with explicit prerequisites, step-by-step setup, and clear access URLs. The references to detailed documentation (SETUP.md and SECURITY.md) are helpful.

frontend/src/components/ui/Breadcrumb.jsx (2)

18-32: Using index as key is acceptable here.

While using array indices as keys is generally discouraged in React, it's acceptable in this case because breadcrumb items are static and won't be reordered, added, or removed during the component's lifecycle.


8-35: Clean breadcrumb navigation component.

The component provides clear navigation hierarchy with proper Link usage for navigable items and visual distinction for the current page (last item). The implementation with Home icon and chevron separators follows common UX patterns.

.gitignore (4)

193-196: Good security practice.

Excluding cookie files prevents accidental leakage of session tokens or credentials.


204-206: Verify markdown folder exclusion is intentional.

Excluding all markdown/ directories will prevent documentation from being tracked. If this folder contains project documentation that should be versioned, consider using a more specific pattern or removing this rule.


208-211: LGTM!

Excluding user-uploaded content prevents bloating the repository with binary files and user-generated content.


213-214: LGTM!

Excluding Docker volume directories is correct and aligns with the mongodb_data volume defined in docker-compose.yml.

frontend/Dockerfile (1)

1-12: Good layer ordering for development.

The Dockerfile correctly copies package files before source code, ensuring that dependency installation is cached when only source files change.

frontend/src/services/authService.js (2)

9-26: Improved defensive token storage.

The nested conditional checks ensure that only valid tokens and user data are stored in localStorage, preventing issues when the API response structure is incomplete or unexpected.


42-59: LGTM!

Consistently clearing the refreshToken in both success and error paths ensures proper cleanup and prevents stale tokens from persisting in localStorage.

backend/Dockerfile (2)

3-4: LGTM!

Installing curl is necessary for the healthcheck defined in docker-compose.yml (line 49). The --no-cache flag keeps the image size minimal.


6-15: Good layer ordering.

The Dockerfile correctly separates dependency installation from source code copying, enabling efficient layer caching during development.

frontend/src/components/auth/ProtectedRoute.jsx (1)

1-1: LGTM!

Adding <Outlet /> support enables the ProtectedRoute component to work as a layout wrapper for nested routes, while maintaining backward compatibility with the existing children-based usage. This aligns with React Router v6 best practices.

Also applies to: 45-45

docker-compose.yml (2)

30-38: Security improvement: secrets properly externalized.

The hardcoded secrets flagged in the previous review have been replaced with environment variable references. This is the correct approach for managing sensitive configuration.


48-53: Healthcheck endpoint is correctly implemented.

Verification confirms the /api/healthcheck endpoint exists at backend/src/routes/healthcheck.route.js and responds with a 200 status code. The docker-compose healthcheck configuration is valid.

backend/.dockerignore (2)

6-6: README.md exclusion is reasonable.

Excluding README.md from the Docker image reduces size and is appropriate since documentation typically isn't needed at runtime. If you need the README for reference within the container, you can remove this line.


1-14: LGTM!

The .dockerignore file contains appropriate patterns that prevent development artifacts, logs, environment files, and build configuration from being included in the Docker image, keeping it lean and secure.

frontend/vite.config.js (2)

1-3: LGTM!

The import statement formatting changes are consistent and have no functional impact.


8-14: LGTM!

The server configuration properly supports containerized development with appropriate host binding and polling for Docker volume file watching.

frontend/src/components/listings/steps/ImageStep.jsx (3)

1-3: LGTM!

Component signature and imports are correct and align with the parent form pattern.


15-22: LGTM!

The ImageUpload integration correctly propagates changes via a synthetic event pattern, consistent with other form steps.


25-37: LGTM!

The photo guidelines panel provides clear, actionable advice to users, improving the quality of uploaded images.

frontend/src/components/listings/SellerInfoCard.jsx (3)

1-10: LGTM!

Imports and defensive null check are appropriate.


12-20: LGTM!

Destructuring with appropriate default values for optional properties.


53-85: LGTM!

Contact details section properly handles optional fields and includes appropriate links.

frontend/.dockerignore (1)

1-17: LGTM!

The .dockerignore file contains appropriate exclusions for Docker builds, reducing image size and preventing unnecessary file copying.

frontend/src/components/ui/ShareButton.jsx (2)

18-30: LGTM!

Facebook and Twitter sharing functions properly encode URLs and open in new windows.


38-74: LGTM!

The button rendering is well-structured with proper accessibility attributes and consistent styling.

frontend/src/components/ui/Pagination.jsx (4)

1-16: LGTM!

Component signature and imports are correct.


18-47: LGTM!

The page number generation logic correctly handles truncation with intelligent ellipsis placement for different pagination scenarios.


49-54: LGTM!

Item range calculations are correct with proper boundary handling.


56-90: LGTM!

The items info display and per-page selector are well-implemented with proper accessibility.

package.json (1)

12-17: LGTM with note on docker:clean.

The Docker scripts are well-organized. Note that docker:clean removes volumes with the -v flag, which will delete all database data. Ensure developers are aware of this when running the command.

frontend/src/components/listings/steps/BasicInfoStep.jsx (5)

1-3: LGTM!

Component signature and imports are correct.


4-32: LGTM!

The category and condition options are comprehensive and well-described for a campus marketplace.


36-60: LGTM!

The title field is well-structured with proper error handling and a helpful character counter.


62-88: LGTM!

The description textarea is properly configured with appropriate constraints and user feedback.


90-154: LGTM!

Both the category selector (button grid) and condition selector (radio group) are well-implemented with clear visual feedback and helpful descriptions.

frontend/src/components/listings/SearchBar.jsx (1)

1-84: LGTM! Well-implemented debounced search.

The SearchBar component is well-structured with proper debouncing, state synchronization, cleanup on unmount, and clear functionality. The implementation follows React best practices and handles edge cases appropriately.

frontend/src/components/listings/FormProgress.jsx (1)

1-54: LGTM! Clean progress indicator implementation.

The FormProgress component is well-structured with clear visual states for completed, current, and upcoming steps. The conditional styling and check icon usage make the progress intuitive for users.

frontend/src/schemas/listingSchema.js (1)

60-73: Well-structured schema organization!

The use of .merge() to combine step schemas is idiomatic, and the export structure nicely supports both individual step validation and final consolidated validation. This design works well for the multi-step form use case.

Comment on lines +13 to +15
CLOUDINARY_CLOUD_NAME=your-cloudinary-cloud-name
CLOUDINARY_API_KEY=your-cloudinary-api-key
CLOUDINARY_API_SECRET=your-cloudinary-api-secret
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add missing CLOUDINARY_URL variable.

The docker-compose.yml file (line 39) references CLOUDINARY_URL, but this variable is not included in the .env.example template. Add it to ensure completeness.

Apply this diff:

 CLOUDINARY_CLOUD_NAME=your-cloudinary-cloud-name
 CLOUDINARY_API_KEY=your-cloudinary-api-key
 CLOUDINARY_API_SECRET=your-cloudinary-api-secret
+CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CLOUDINARY_CLOUD_NAME=your-cloudinary-cloud-name
CLOUDINARY_API_KEY=your-cloudinary-api-key
CLOUDINARY_API_SECRET=your-cloudinary-api-secret
CLOUDINARY_CLOUD_NAME=your-cloudinary-cloud-name
CLOUDINARY_API_KEY=your-cloudinary-api-key
CLOUDINARY_API_SECRET=your-cloudinary-api-secret
CLOUDINARY_URL=cloudinary://your-api-key:your-api-secret@your-cloud-name
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 14-14: [UnorderedKey] The CLOUDINARY_API_KEY key should go before the CLOUDINARY_CLOUD_NAME key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The CLOUDINARY_API_SECRET key should go before the CLOUDINARY_CLOUD_NAME key

(UnorderedKey)

🤖 Prompt for AI Agents
.env.example around lines 13 to 15: the .env.example is missing the
CLOUDINARY_URL variable referenced by docker-compose.yml; add a CLOUDINARY_URL
entry to the file (use the Cloudinary URL format
cloudinary://<API_KEY>:<API_SECRET>@<CLOUD_NAME> or a placeholder like
cloudinary://your-api-key:your-api-secret@your-cloud-name) so the example
matches docker-compose and developers know the expected format.

Comment on lines +1 to +194
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, X, Image as ImageIcon, AlertCircle } from 'lucide-react';
import toast from 'react-hot-toast';

/**
* ImageUpload Component
* Drag & drop image upload with preview, reorder, and delete
*/
const ImageUpload = ({ images, onChange, error, maxImages = 5 }) => {
const [isDragging, setIsDragging] = useState(false);

/**
* Handle file drop/selection
*/
const onDrop = useCallback(
(acceptedFiles, rejectedFiles) => {
// Handle rejected files
if (rejectedFiles.length > 0) {
rejectedFiles.forEach((file) => {
file.errors.forEach((err) => {
if (err.code === 'file-too-large') {
toast.error(`${file.file.name}: File is too large (max 5MB)`);
} else if (err.code === 'file-invalid-type') {
toast.error(`${file.file.name}: Invalid file type`);
} else {
toast.error(`${file.file.name}: ${err.message}`);
}
});
});
}

// Check total images limit
if (images.length + acceptedFiles.length > maxImages) {
toast.error(`Maximum ${maxImages} images allowed`);
return;
}

// Add new images with preview
const newImages = acceptedFiles.map((file) => ({
file,
preview: URL.createObjectURL(file),
id: Math.random().toString(36).substr(2, 9),
}));

onChange([...images, ...newImages]);
toast.success(`${acceptedFiles.length} image(s) added`);
},
[images, onChange, maxImages]
);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
'image/*': ['.jpeg', '.jpg', '.png', '.webp'],
},
maxSize: 5 * 1024 * 1024, // 5MB
maxFiles: maxImages,
onDragEnter: () => setIsDragging(true),
onDragLeave: () => setIsDragging(false),
onDropAccepted: () => setIsDragging(false),
onDropRejected: () => setIsDragging(false),
});

/**
* Remove image
*/
const removeImage = (indexToRemove) => {
const newImages = images.filter((_, index) => index !== indexToRemove);
onChange(newImages);
toast.success('Image removed');
};

/**
* Reorder images
*/
const moveImage = (fromIndex, toIndex) => {
const newImages = [...images];
const [movedImage] = newImages.splice(fromIndex, 1);
newImages.splice(toIndex, 0, movedImage);
onChange(newImages);
};

return (
<div className="space-y-4">
{/* Upload Area */}
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
isDragActive || isDragging
? 'border-blue-500 bg-blue-50'
: error
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400 bg-gray-50'
}`}
>
<input {...getInputProps()} />
<div className="flex flex-col items-center justify-center space-y-3">
<div
className={`w-16 h-16 rounded-full flex items-center justify-center ${
isDragActive ? 'bg-blue-100' : 'bg-gray-100'
}`}
>
<Upload
className={`h-8 w-8 ${isDragActive ? 'text-blue-600' : 'text-gray-400'}`}
/>
</div>
<div>
<p className="text-lg font-semibold text-gray-700">
{isDragActive ? 'Drop images here' : 'Drag & drop images here'}
</p>
<p className="text-sm text-gray-500 mt-1">
or click to browse • Max {maxImages} images • Max 5MB each
</p>
<p className="text-xs text-gray-400 mt-2">
Supported formats: JPEG, PNG, WebP
</p>
</div>
</div>
</div>

{/* Error Message */}
{error && (
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg p-3">
<AlertCircle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}

{/* Image Previews */}
{images.length > 0 && (
<div>
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-medium text-gray-700">
{images.length} / {maxImages} images uploaded
</p>
<p className="text-xs text-gray-500">Drag to reorder • First image is cover</p>
</div>

<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{images.map((image, index) => (
<div
key={image.id || index}
draggable
onDragStart={(e) => e.dataTransfer.setData('imageIndex', index)}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const fromIndex = parseInt(e.dataTransfer.getData('imageIndex'));
moveImage(fromIndex, index);
}}
className="relative group aspect-square rounded-lg overflow-hidden border-2 border-gray-200 hover:border-blue-500 transition-all cursor-move"
>
<img
src={image.preview}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>

{/* Cover Badge */}
{index === 0 && (
<div className="absolute top-2 left-2 px-2 py-1 bg-blue-600 text-white text-xs font-semibold rounded">
Cover
</div>
)}

{/* Remove Button */}
<button
type="button"
onClick={() => removeImage(index)}
className="absolute top-2 right-2 p-1.5 bg-red-600 hover:bg-red-700 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
</button>

{/* Overlay on Hover */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<ImageIcon className="h-8 w-8 text-white" />
</div>
</div>
))}
</div>
</div>
)}

{/* Empty State */}
{images.length === 0 && !error && (
<div className="text-center py-8">
<ImageIcon className="h-12 w-12 text-gray-300 mx-auto mb-3" />
<p className="text-sm text-gray-500">No images uploaded yet</p>
</div>
)}
</div>
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Revoke object URLs to avoid leaks.

Each call to URL.createObjectURL pins a blob in memory until you explicitly revoke it. Because we never revoke the previews, every add/remove cycle leaks memory (and a reset from the parent won’t clean them up either). Please track the blob URLs you create and revoke them on removal/reset/unmount so long sessions don’t degrade.

-import { useCallback, useState } from 'react';
+import { useCallback, useState, useRef, useEffect } from 'react';
@@
-  const [isDragging, setIsDragging] = useState(false);
+  const [isDragging, setIsDragging] = useState(false);
+  const previewUrlsRef = useRef(new Set());
@@
-      const newImages = acceptedFiles.map((file) => ({
+      const newImages = acceptedFiles.map((file) => ({
         file,
         preview: URL.createObjectURL(file),
         id: Math.random().toString(36).substr(2, 9),
       }));
+
+      newImages.forEach((image) => previewUrlsRef.current.add(image.preview));
@@
-  const removeImage = (indexToRemove) => {
-    const newImages = images.filter((_, index) => index !== indexToRemove);
+  const removeImage = (indexToRemove) => {
+    const imageToRemove = images[indexToRemove];
+    if (imageToRemove?.preview?.startsWith('blob:')) {
+      URL.revokeObjectURL(imageToRemove.preview);
+      previewUrlsRef.current.delete(imageToRemove.preview);
+    }
+    const newImages = images.filter((_, index) => index !== indexToRemove);
@@
+  useEffect(() => {
+    previewUrlsRef.current.forEach((url) => {
+      if (!images.some((image) => image.preview === url)) {
+        URL.revokeObjectURL(url);
+        previewUrlsRef.current.delete(url);
+      }
+    });
+  }, [images]);
+
+  useEffect(() => {
+    return () => {
+      previewUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
+      previewUrlsRef.current.clear();
+    };
+  }, []);
🤖 Prompt for AI Agents
frontend/src/components/listings/ImageUpload.jsx lines 1-194: the component
creates blob URLs via URL.createObjectURL for image.preview but never revokes
them, leaking memory; update the component to revoke URLs when an image is
removed, when images are replaced/reset, and on component unmount: revoke a
removed image's preview in removeImage before calling onChange, revoke all
previews created by acceptedFiles when you discard or replace them (and after
onChange when appropriate), and add a useEffect cleanup that iterates images'
preview URLs and calls URL.revokeObjectURL for each on unmount or when the
images prop changes to a new list. Ensure you only revoke strings that look like
object URLs and avoid revoking previews still in use by the active images array.

Comment on lines 57 to 74
useEffect(() => {
if (!user) {
toast.error("Please login to create a listing");
navigate("/login", { state: { from: "/listings/create" } });
return;
}

// Check if user has required roles (student, vendor-admin, club-admin)
const canCreateListing = user.roles?.some((role) =>
["student", "vendor-admin", "club-admin", "admin"].includes(role)
);

if (!canCreateListing) {
toast.error("You do not have permission to create listings");
navigate("/");
}
}, [user, navigate]);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate the auth/role check until useAuth finishes loading.

This effect fires on the initial render when loading is still true, so user is undefined and we immediately show an error toast and redirect to /login even for authenticated users. Add a guard for the loading state (and include it in the deps) before performing the role check.

-  useEffect(() => {
-    if (!user) {
+  useEffect(() => {
+    if (loading) {
+      return;
+    }
+
+    if (!user) {
       toast.error("Please login to create a listing");
       navigate("/login", { state: { from: "/listings/create" } });
       return;
     }
@@
-  }, [user, navigate]);
+  }, [user, loading, navigate]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!user) {
toast.error("Please login to create a listing");
navigate("/login", { state: { from: "/listings/create" } });
return;
}
// Check if user has required roles (student, vendor-admin, club-admin)
const canCreateListing = user.roles?.some((role) =>
["student", "vendor-admin", "club-admin", "admin"].includes(role)
);
if (!canCreateListing) {
toast.error("You do not have permission to create listings");
navigate("/");
}
}, [user, navigate]);
useEffect(() => {
if (loading) {
return;
}
if (!user) {
toast.error("Please login to create a listing");
navigate("/login", { state: { from: "/listings/create" } });
return;
}
// Check if user has required roles (student, vendor-admin, club-admin)
const canCreateListing = user.roles?.some((role) =>
["student", "vendor-admin", "club-admin", "admin"].includes(role)
);
if (!canCreateListing) {
toast.error("You do not have permission to create listings");
navigate("/");
}
}, [user, loading, navigate]);
🤖 Prompt for AI Agents
In frontend/src/pages/listings/CreateListing.jsx around lines 57 to 74, the
effect runs while auth is still loading so user is undefined and triggers
erroneous toasts/redirects; update the effect to bail out when the auth loading
flag is true (add loading to the dependency array) and only perform the
user/role checks once loading is false, then run the existing logic to toast and
navigate if user is absent or lacks roles.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/controllers/listing.controller.js (1)

190-198: Throw the ApiError when the listing is missing.

Returning new ApiError(...) doesn’t send a response; the request just hangs. Throw the error (or send a 404 response) so the async handler can propagate it.

-  if (!listing) {
-    return new ApiError(404, "Listing not found.");
-  }
+  if (!listing) {
+    throw new ApiError(404, "Listing not found.");
+  }
♻️ Duplicate comments (6)
frontend/src/components/listings/ListingCard.jsx (1)

22-28: Missing "new" condition in color mapping (duplicate issue).

The conditionColors object still lacks a "new" key. According to the form schema in the PR objectives, "new" is a valid condition value. When a listing has condition "new", it will fall back to the "good" styling (yellow) due to line 99's fallback, which is semantically incorrect for a brand new item.

Apply this diff to add the missing "new" entry:

 const conditionColors = {
+  new: "bg-green-100 text-green-800",
   "brand-new": "bg-green-100 text-green-800",
   "like-new": "bg-blue-100 text-blue-800",
   good: "bg-yellow-100 text-yellow-800",
   fair: "bg-orange-100 text-orange-800",
   poor: "bg-red-100 text-red-800",
 };
frontend/src/pages/listings/ListingsDetails.jsx (2)

44-52: Still discarding the listing payload.

listingService.getListingById already unwraps response.data, so .data is undefined and the page always falls into the error state. Please reuse the service return value directly.

-      const response = await listingService.getListingById(id);
-      setListing(response.data);
+      const listingData = await listingService.getListingById(id);
+      setListing(listingData);

150-207: Add missing badge mappings (again) and fallbacks.

conditionColors still lacks "new" and statusColors never covers the default "active" status, so both badges render with an undefined class. Add the missing keys and guard with a fallback class.

   const conditionColors = {
+    new: "bg-green-100 text-green-800",
     "brand-new": "bg-green-100 text-green-800",
…
   const statusColors = {
+    active: "bg-green-100 text-green-800",
     available: "bg-green-100 text-green-800",
…
-                    <span
-                      className={`px-3 py-1 rounded-full text-sm font-medium ${
-                        statusColors[listing.status]
-                      }`}
-                    >
+                    const statusClass =
+                      statusColors[listing.status] ||
+                      "bg-gray-100 text-gray-800";
+                    const conditionClass =
+                      conditionColors[listing.condition] ||
+                      "bg-gray-100 text-gray-800";
+
+                    <span
+                      className={`px-3 py-1 rounded-full text-sm font-medium ${statusClass}`}
+                    >-                    <span
-                      className={`px-3 py-1 rounded-full text-sm font-medium ${
-                        conditionColors[listing.condition]
-                      }`}
-                    >
+                    <span
+                      className={`px-3 py-1 rounded-full text-sm font-medium ${conditionClass}`}
+                    >
frontend/src/services/listingService.js (1)

13-21: Drop the production console log.

Logging every listings response in production leaks payloads and clutters the console. Please remove (or guard) the statement.

       const response = await api.get("/listings", { params });
-      console.log("API Response:", response.data);
       return response.data;
frontend/src/App.jsx (1)

60-62: Align token routing with the pages (still broken).

ResetPassword/VerifyEmail read tokens from the query string. Keeping :token path params means token is always null, so both flows fail. Drop the path params (or update the pages to use useParams) to stay consistent.

-          <Route path="/reset-password/:token" element={<ResetPassword />} />
-          <Route path="/verify-email/:token" element={<VerifyEmail />} />
+          <Route path="/reset-password" element={<ResetPassword />} />
+          <Route path="/verify-email" element={<VerifyEmail />} />
frontend/src/pages/listings/CreateListing.jsx (1)

56-75: Include loading in the auth gate effect deps.

The guard rightly bails while auth is loading, but because loading isn’t in the dependency array the effect never re-runs when it flips to false. Add it so the permission checks actually execute.

-  }, [user, navigate]);
+  }, [user, loading, navigate]);
🧹 Nitpick comments (7)
markdown/dbQueries.md (3)

143-146: Consider adding connection timeouts and retry guidance.

The documentation includes important cleanup and maintenance operations, but lacks guidance on connection timeouts, retry strategies, and safety measures when executing bulk operations. For production environments, consider adding a section on:

  • Bulk operation safety (backups before deletions, transaction usage)
  • Connection timeout recommendations
  • Error handling best practices

Also applies to: 342-347, 400-405, 427-433


455-458: Add guidance on text index creation timing.

The text index example is helpful, but it should note that text search queries will fail if the index doesn't exist. Consider adding a comment recommending this be run during database setup or migrations.

 // Create text index for search
+// ⚠️ Run once during initial setup; text search queries will fail without this
 db.listings.createIndex({
   title: "text",
   description: "text",
 });

630-630: Fix minor formatting issues flagged by style guides.

Two minor style suggestions from linting:

  1. Line 630: Add comma after year in date format (style guide convention for month-day-year dates)
  2. Line 688: Hyphenate compound adjective "1-year-old" → "1-year-old"
-**Last Updated**: November 6, 2025  
+**Last Updated**: November 6, 2025,

-    description: "Boat Rockerz wireless earphones, 1 year old, working perfectly.",
+    description: "Boat Rockerz wireless earphones, 1-year-old, working perfectly.",

Also applies to: 688-688

frontend/src/components/listings/ImageManager.jsx (1)

49-49: Replace deprecated substr() with substring().

The substr() method is deprecated. Use substring() instead for future compatibility.

Apply this diff:

-      id: Math.random().toString(36).substr(2, 9),
+      id: Math.random().toString(36).substring(2, 11),
backend/src/routes/listing.routes.js (2)

47-49: Avoid dynamic imports for static dependencies.

The dynamic imports for uploadToCloudinary and removeLocalFiles add unnecessary overhead since these utilities are known at build time. Static imports at the top of the file would be more efficient and conventional.

Apply this diff at the top of the file:

 import { upload } from "../middlewares/multer.js";
+import { uploadToCloudinary } from "../utils/upload.js";
+import { removeLocalFiles } from "../middlewares/multer.js";

Then remove the dynamic imports and use the statically imported functions:

-      // Import uploadToCloudinary and removeLocalFiles
-      const { uploadToCloudinary } = await import("../utils/upload.js");
-      const { removeLocalFiles } = await import("../middlewares/multer.js");
-
       // Upload to Cloudinary

And in the error handler:

       console.error("Image upload error:", error);
       if (req.file) {
-        const { removeLocalFiles } = await import("../middlewares/multer.js");
         removeLocalFiles([req.file]);
       }

Also applies to: 70-70


37-80: Consider extracting inline handler to controller for consistency.

While the inline async handler works correctly, extracting it to a dedicated controller function (e.g., uploadListingImage) would maintain consistency with the rest of the codebase where all route handlers are defined in controllers.

This would make the route definition cleaner:

listingRouter.post("/upload-image", verifyJWT, upload.single("image"), uploadListingImage);

And the handler would live in backend/src/controllers/listing.controller.js alongside other listing operations.

frontend/src/pages/listings/EditListing.jsx (1)

230-247: Consider parallel image uploads for better performance.

Images are uploaded sequentially, which could be slow when multiple images need to be uploaded. Using Promise.all would upload them in parallel and improve user experience.

Apply this diff:

  const uploadNewImages = async () => {
-    const uploadedUrls = [];
-
-    for (const image of formData.newImages) {
-      try {
-        const formDataToSend = new FormData();
-        formDataToSend.append('image', image.file);
-
-        const response = await listingService.uploadImage(formDataToSend);
-        uploadedUrls.push(response.data.data.url);
-      } catch (error) {
-        console.error('Error uploading image:', error);
-        throw new Error('Failed to upload images');
-      }
-    }
-
-    return uploadedUrls;
+    try {
+      const uploadPromises = formData.newImages.map(async (image) => {
+        const formDataToSend = new FormData();
+        formDataToSend.append('image', image.file);
+        const response = await listingService.uploadImage(formDataToSend);
+        return response.data.data.url;
+      });
+      
+      return await Promise.all(uploadPromises);
+    } catch (error) {
+      console.error('Error uploading image:', error);
+      throw new Error('Failed to upload images');
+    }
  };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f871fed and a4e4f3f.

📒 Files selected for processing (14)
  • backend/src/controllers/listing.controller.js (7 hunks)
  • backend/src/models/index.js (1 hunks)
  • backend/src/models/listing.model.js (1 hunks)
  • backend/src/routes/listing.routes.js (3 hunks)
  • frontend/src/App.jsx (3 hunks)
  • frontend/src/components/listings/ImageManager.jsx (1 hunks)
  • frontend/src/components/listings/ListingCard.jsx (1 hunks)
  • frontend/src/components/listings/PriceHistory.jsx (1 hunks)
  • frontend/src/hooks/useListings.js (1 hunks)
  • frontend/src/pages/listings/CreateListing.jsx (1 hunks)
  • frontend/src/pages/listings/EditListing.jsx (1 hunks)
  • frontend/src/pages/listings/ListingsDetails.jsx (1 hunks)
  • frontend/src/services/listingService.js (1 hunks)
  • markdown/dbQueries.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/hooks/useListings.js
🧰 Additional context used
🧬 Code graph analysis (10)
frontend/src/components/listings/PriceHistory.jsx (1)
backend/src/controllers/listing.controller.js (1)
  • history (574-577)
frontend/src/pages/listings/ListingsDetails.jsx (7)
frontend/src/components/listings/RelatedListings.jsx (2)
  • loading (12-12)
  • RelatedListings (10-84)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-141)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/components/ui/Breadcrumb.jsx (1)
  • Breadcrumb (8-35)
frontend/src/components/listings/ImageGallery.jsx (1)
  • ImageGallery (8-154)
frontend/src/components/ui/ShareButton.jsx (1)
  • ShareButtons (8-75)
frontend/src/components/listings/SellerInfoCard.jsx (1)
  • SellerInfoCard (7-108)
frontend/src/pages/listings/EditListing.jsx (7)
frontend/src/components/listings/steps/BasicInfoStep.jsx (1)
  • BasicInfoStep (3-157)
frontend/src/components/listings/steps/PricingStep.jsx (1)
  • PricingStep (3-115)
frontend/src/components/listings/steps/LocationStep.jsx (1)
  • LocationStep (3-94)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-141)
frontend/src/components/listings/ImageManager.jsx (2)
  • totalImages (21-21)
  • ImageManager (10-270)
frontend/src/components/listings/FormProgress.jsx (1)
  • FormProgress (7-52)
frontend/src/components/listings/PriceHistory.jsx (1)
  • PriceHistory (27-128)
frontend/src/services/listingService.js (4)
backend/src/controllers/listing.controller.js (1)
  • userId (484-484)
frontend/src/services/api.js (1)
  • api (9-15)
frontend/src/pages/listings/CreateListing.jsx (1)
  • formData (27-44)
frontend/src/pages/listings/EditListing.jsx (1)
  • formData (33-47)
frontend/src/App.jsx (4)
frontend/src/pages/auth/ResetPassword.jsx (1)
  • ResetPassword (11-207)
frontend/src/pages/auth/VerifyEmail.jsx (1)
  • VerifyEmail (7-133)
frontend/src/pages/listings/ListingsDetails.jsx (1)
  • ListingDetail (26-299)
frontend/src/components/auth/ProtectedRoute.jsx (1)
  • ProtectedRoute (9-46)
backend/src/routes/listing.routes.js (4)
backend/src/controllers/listing.controller.js (21)
  • getCategories (549-560)
  • getCategories (549-560)
  • req (28-42)
  • req (135-146)
  • req (182-182)
  • req (201-201)
  • req (225-241)
  • req (339-339)
  • req (340-340)
  • req (406-406)
  • req (407-413)
  • req (445-451)
  • req (563-563)
  • req (585-585)
  • req (586-586)
  • result (82-85)
  • result (268-271)
  • getPriceHistory (562-582)
  • getPriceHistory (562-582)
  • deleteListingImage (584-612)
  • deleteListingImage (584-612)
backend/src/middlewares/auth.middleware.js (4)
  • verifyJWT (6-31)
  • verifyJWT (6-31)
  • verifyListingOwnershipOrAdmin (78-108)
  • verifyListingOwnershipOrAdmin (78-108)
backend/src/middlewares/multer.js (4)
  • upload (32-39)
  • upload (32-39)
  • removeLocalFiles (42-52)
  • removeLocalFiles (42-52)
backend/src/utils/upload.js (3)
  • result (16-19)
  • uploadToCloudinary (13-29)
  • uploadToCloudinary (13-29)
backend/src/controllers/listing.controller.js (3)
backend/src/utils/asyncHandler.js (2)
  • asyncHandler (1-5)
  • asyncHandler (1-5)
backend/src/middlewares/multer.js (2)
  • removeLocalFiles (42-52)
  • removeLocalFiles (42-52)
backend/src/models/index.js (2)
  • LISTING_CATEGORIES (22-32)
  • LISTING_CATEGORIES (22-32)
frontend/src/pages/listings/CreateListing.jsx (9)
frontend/src/pages/listings/EditListing.jsx (10)
  • navigate (23-23)
  • currentStep (27-27)
  • formData (33-47)
  • errors (49-49)
  • steps (52-57)
  • loading (25-25)
  • handleChange (122-135)
  • validateStep (140-194)
  • handleSubmit (252-299)
  • CurrentStepComponent (337-337)
frontend/src/components/listings/steps/BasicInfoStep.jsx (1)
  • BasicInfoStep (3-157)
frontend/src/components/listings/steps/PricingStep.jsx (1)
  • PricingStep (3-115)
frontend/src/components/listings/steps/ImageStep.jsx (1)
  • ImageStep (3-40)
frontend/src/components/listings/steps/LocationStep.jsx (1)
  • LocationStep (3-94)
frontend/src/schemas/listingSchema.js (2)
  • stepSchemas (66-71)
  • stepSchemas (66-71)
frontend/src/services/listingService.js (10)
  • response (15-15)
  • response (30-30)
  • response (41-41)
  • response (52-54)
  • response (74-74)
  • response (85-85)
  • response (95-95)
  • response (104-104)
  • response (114-114)
  • listingService (7-141)
frontend/src/components/listings/FormProgress.jsx (1)
  • FormProgress (7-52)
frontend/src/components/ui/Button.jsx (1)
  • Button (22-61)
frontend/src/components/listings/ListingCard.jsx (2)
backend/src/controllers/listing.controller.js (8)
  • listing (101-118)
  • listing (183-188)
  • listing (204-204)
  • listing (343-343)
  • listing (534-534)
  • listing (565-567)
  • listing (588-588)
  • images (78-78)
frontend/src/pages/listings/ListingsDetails.jsx (2)
  • listing (31-31)
  • conditionColors (150-156)
backend/src/models/index.js (1)
backend/src/models/users.model.js (1)
  • HOSTELS (12-12)
🪛 LanguageTool
markdown/dbQueries.md

[style] ~630-~630: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ...t** --- Last Updated: November 6, 2025 Project: Campus Marketplace - IIT...

(MISSING_COMMA_AFTER_YEAR)


[uncategorized] ~688-~688: It seems that hyphens are missing.
Context: ...tion: "Boat Rockerz wireless earphones, 1 year old, working perfectly.", price: 900, ...

(YEAR_OLD_HYPHEN)

🔇 Additional comments (7)
markdown/dbQueries.md (2)

633-699: Excellent seed data examples.

The sample data at the end provides realistic, well-formatted examples with multiple item types. The inclusion of actual ObjectIds (instead of just placeholders) and realistic details makes this practical for testing and demonstrations.


1-25: Strong documentation structure with clear practical guidance.

The documentation is well-organized, comprehensive, and includes both Docker and local environment instructions. The organization with emoji headers, clear code examples, and practical tips makes this an excellent reference for developers, DBAs, and DevOps teams.

frontend/src/components/listings/ImageManager.jsx (1)

26-54: LGTM with minor suggestions.

The file validation and error handling logic is well-implemented with clear user feedback via toast messages. The max images enforcement is appropriate.

backend/src/routes/listing.routes.js (1)

34-34: LGTM! Route ordering prevents conflicts.

The new routes are well-positioned:

  • /categories as a public route before parameterized routes
  • /:id/price-history before /:id to avoid conflicts
  • /:id/images DELETE with proper authentication and authorization

The middleware chains correctly enforce JWT verification and ownership checks.

Also applies to: 95-95, 98-103

frontend/src/pages/listings/EditListing.jsx (3)

62-117: LGTM! Well-structured data fetching and validation.

The component properly:

  • Fetches listing data and verifies ownership
  • Handles loading and error states gracefully
  • Validates each step with clear error messages
  • Populates the form with existing data

The authorization check, error handling, and step validation logic are well-implemented.

Also applies to: 140-194


304-322: Comprehensive change detection.

The hasChanges() function thoroughly compares all fields to detect modifications, including image changes. This properly enables/disables the save button and shows the unsaved changes warning.


78-78: Defensive check needed for user.roles.

The optional chaining on user?.roles?.includes('admin') is good, but if user.roles is undefined rather than an array, the includes call will still throw. Consider adding a more defensive check.

Apply this diff:

-      if (listing.seller._id !== user?._id && !user?.roles?.includes('admin')) {
+      if (listing.seller._id !== user?._id && !user?.roles?.some(role => role === 'admin')) {

Or more defensively:

-      if (listing.seller._id !== user?._id && !user?.roles?.includes('admin')) {
+      const isAdmin = user?.roles?.includes('admin') ?? false;
+      if (listing.seller._id !== user?._id && !isAdmin) {

Likely an incorrect or invalid review comment.

Comment on lines +585 to +605
const { id } = req.params;
const { imageUrl } = req.body;

try {
// 4. Update listing status
listing.status = "sold";
listing.isAvailable = false;
listing.soldAt = new Date();
listing.updatedAt = new Date();
const listing = await Listing.findById(id);

await listing.save();
if (!listing) {
throw new ApiError(404, "Listing not found");
}

return res.status(200).json({
message: "Listing marked as sold successfully.",
listing: {
id: listing._id,
title: listing.title,
status: listing.status,
soldAt: listing.soldAt,
},
});
} catch (err) {
return res
.status(500)
.json({ error: "Failed to mark listing as sold.", details: err.message });
// Check ownership
if (listing.seller.toString() !== req.user._id.toString()) {
throw new ApiError(
403,
"You do not have permission to modify this listing"
);
}
}); // Mark listing as sold

// FUTURE ENHANCEMENTS
// Remove image from array
listing.images = listing.images.filter((img) => img !== imageUrl);
await listing.save();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix owner check and image removal in deleteListingImage.

listing.seller is undefined (the field is owner), so this throws, and filtering by object equality never removes the image. Use owner for the auth check and match on img.url.

-  if (listing.seller.toString() !== req.user._id.toString()) {
+  if (listing.owner.toString() !== req.user._id.toString()) {
     throw new ApiError(
       403,
       "You do not have permission to modify this listing"
     );
   }
…
-  listing.images = listing.images.filter((img) => img !== imageUrl);
+  listing.images = listing.images.filter((img) => img.url !== imageUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { id } = req.params;
const { imageUrl } = req.body;
try {
// 4. Update listing status
listing.status = "sold";
listing.isAvailable = false;
listing.soldAt = new Date();
listing.updatedAt = new Date();
const listing = await Listing.findById(id);
await listing.save();
if (!listing) {
throw new ApiError(404, "Listing not found");
}
return res.status(200).json({
message: "Listing marked as sold successfully.",
listing: {
id: listing._id,
title: listing.title,
status: listing.status,
soldAt: listing.soldAt,
},
});
} catch (err) {
return res
.status(500)
.json({ error: "Failed to mark listing as sold.", details: err.message });
// Check ownership
if (listing.seller.toString() !== req.user._id.toString()) {
throw new ApiError(
403,
"You do not have permission to modify this listing"
);
}
}); // Mark listing as sold
// FUTURE ENHANCEMENTS
// Remove image from array
listing.images = listing.images.filter((img) => img !== imageUrl);
await listing.save();
const { id } = req.params;
const { imageUrl } = req.body;
const listing = await Listing.findById(id);
if (!listing) {
throw new ApiError(404, "Listing not found");
}
// Check ownership
if (listing.owner.toString() !== req.user._id.toString()) {
throw new ApiError(
403,
"You do not have permission to modify this listing"
);
}
// Remove image from array
listing.images = listing.images.filter((img) => img.url !== imageUrl);
await listing.save();
🤖 Prompt for AI Agents
In backend/src/controllers/listing.controller.js around lines 585 to 605, the
owner check and image removal are incorrect: replace the undefined
listing.seller check with listing.owner and compare IDs as strings
(listing.owner.toString() !== req.user._id.toString()), and when removing the
image filter the images array by the image object's url property (e.g.,
listing.images = listing.images.filter(img => img.url !== imageUrl)), handling
possible null/undefined images array before filtering, then save the listing.

Comment on lines +46 to +53
const newImageObjects = acceptedFiles.map((file) => ({
file,
preview: URL.createObjectURL(file),
id: Math.random().toString(36).substr(2, 9),
}));

onNewImagesChange([...newImages, ...newImageObjects]);
toast.success(`${acceptedFiles.length} image(s) added`);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: Object URLs are not revoked.

Object URLs created with URL.createObjectURL() on line 48 are never revoked, which can lead to memory leaks, especially when users upload and remove multiple images.

Add cleanup logic using useEffect. At the component level, add:

useEffect(() => {
  // Cleanup function to revoke object URLs when component unmounts or newImages change
  return () => {
    newImages.forEach((image) => {
      if (image.preview) {
        URL.revokeObjectURL(image.preview);
      }
    });
  };
}, [newImages]);

Also consider revoking URLs immediately in removeNewImage:

  const removeNewImage = (indexToRemove) => {
+   const imageToRemove = newImages[indexToRemove];
+   if (imageToRemove?.preview) {
+     URL.revokeObjectURL(imageToRemove.preview);
+   }
    const filtered = newImages.filter((_, index) => index !== indexToRemove);
    onNewImagesChange(filtered);
    toast.success('Image removed');
  };
🤖 Prompt for AI Agents
In frontend/src/components/listings/ImageManager.jsx around lines 46 to 53,
object URLs created with URL.createObjectURL are not revoked causing a memory
leak; add a useEffect that returns a cleanup function which iterates newImages
and calls URL.revokeObjectURL for each image.preview when newImages changes or
the component unmounts, and update removeNewImage to call
URL.revokeObjectURL(image.preview) immediately before removing the image from
state or calling onNewImagesChange so previews are released as soon as they are
dropped.


// Extract image URL safely
const getImageUrl = () => {
if (!images || images.length === 0) return "/placeholder-image.jpg";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the placeholder image exists.

The code references /placeholder-image.jpg as a fallback in multiple places. Ensure this file exists in your public/ directory to avoid broken image displays when listings have no images or when images fail to load.

Also applies to: 45-45, 76-76


🏁 Script executed:

#!/bin/bash
# Check if placeholder image exists
fd -t f "placeholder-image.jpg" frontend/public

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if frontend/public directory exists and list its contents
echo "=== Checking frontend/public directory ===" 
fd -t f "." frontend/public 2>&1 | head -20

echo ""
echo "=== Searching for any placeholder-related files ==="
fd -i "placeholder" . 2>&1 | grep -v node_modules | head -20

echo ""
echo "=== Listing frontend directory structure ==="
fd -t d "^(public|static|assets)$" frontend

Length of output: 204


🏁 Script executed:

#!/bin/bash
# Check assets directory
echo "=== Contents of frontend/src/assets ==="
fd -t f "." frontend/src/assets

echo ""
echo "=== Inspect ListingCard.jsx (lines 25-80 for context) ==="
sed -n '25,80p' frontend/src/components/listings/ListingCard.jsx

Length of output: 2013


Create the missing placeholder image file.

The code references /placeholder-image.jpg as a fallback at three locations (lines 32, 45, 76), but this file does not exist in frontend/public/. This will result in broken images when listings have no images, invalid image objects, or when images fail to load. Create frontend/public/placeholder-image.jpg or update the fallback path to point to an existing asset.

🤖 Prompt for AI Agents
In frontend/src/components/listings/ListingCard.jsx around lines 32, 45 and 76
the component falls back to "/placeholder-image.jpg" when images are missing but
that file is not present in frontend/public/, causing broken images; either add
a placeholder image file at frontend/public/placeholder-image.jpg (ensure it's
web‑optimized and named exactly) or update the fallback references at those
lines to point to an existing asset path in public (e.g.,
"/assets/placeholder.jpg") and verify the path is correct and loadable in the
built app.

<Link to={`/listings/${_id}`} className="block">
<div className="bg-white rounded-xl shadow-md overflow-hidden hover:shadow-xl transition-all duration-300 group h-full">
{/* Image */}
<div className="relative aspect-4/3 overflow-hidden bg-gray-100">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the custom aspect ratio class.

The class aspect-4/3 is not standard Tailwind CSS syntax. Standard Tailwind uses aspect-[4/3] with square brackets for arbitrary aspect ratios. Unless aspect-4/3 is explicitly defined in your tailwind.config.js, this class will be ignored and the aspect ratio won't apply.

Please verify one of the following:

  1. The class is defined in tailwind.config.js under theme.extend.aspectRatio
  2. Use the standard arbitrary value syntax instead

If not already configured, apply this diff:

-        <div className="relative aspect-4/3 overflow-hidden bg-gray-100">
+        <div className="relative aspect-[4/3] overflow-hidden bg-gray-100">

Run this script to check if the custom class is configured:


🏁 Script executed:

#!/bin/bash
# Check if aspect-4/3 is configured in Tailwind config
fd -t f "tailwind.config" | xargs rg -l "aspect.*4.*3"

Length of output: 248



---

🏁 Script executed:

```shell
#!/bin/bash
# Find tailwind config file and check for aspect ratio configuration
echo "=== Finding Tailwind config files ==="
fd -t f "tailwind.config\.(js|ts|cjs|mjs)" 

echo -e "\n=== Checking for aspect ratio configuration ==="
fd -t f "tailwind.config\.(js|ts|cjs|mjs)" | xargs cat 2>/dev/null | grep -A 20 -B 2 "aspectRatio\|aspect"

Length of output: 86



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for Tailwind config in package.json or other config files
echo "=== Checking package.json for tailwind config ==="
fd -t f "package.json" -x grep -l "tailwind" {} \;

echo -e "\n=== Looking for any tailwind references in config files ==="
fd -t f "\.(config|postcss)" | head -20

echo -e "\n=== Checking all files in root for tailwind ==="
ls -la | grep -i tailwind

echo -e "\n=== Searching project for any tailwind config references ==="
rg -l "theme.*extend.*aspectRatio" --type=js --type=ts --type=json 2>/dev/null | head -10

Length of output: 295



---

🏁 Script executed:

```shell
#!/bin/bash
# Search specifically in frontend directory
echo "=== Searching frontend directory for tailwind config ==="
find frontend -maxdepth 2 -name "tailwind.config*" -o -name "postcss.config*"

echo -e "\n=== Check frontend package.json for tailwind config ==="
cat frontend/package.json | grep -A 5 -B 5 "tailwind"

echo -e "\n=== Check for any config in frontend root ==="
ls -la frontend/ | grep -E "config|tailwind|postcss"

echo -e "\n=== Search entire codebase for aspect-4/3 definition ==="
rg "4.*3|4/3" frontend/tailwind* frontend/postcss* 2>/dev/null || echo "No tailwind/postcss config files found"

Length of output: 1249


Fix the invalid aspect ratio class syntax.

The class aspect-4/3 is not configured and will not work with Tailwind CSS's default configuration. Tailwind's standard aspect utilities are aspect-auto, aspect-square, and aspect-video. The arbitrary value syntax aspect-[4/3] is the correct approach and requires no configuration changes.

Apply this fix:

-        <div className="relative aspect-4/3 overflow-hidden bg-gray-100">
+        <div className="relative aspect-[4/3] overflow-hidden bg-gray-100">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="relative aspect-4/3 overflow-hidden bg-gray-100">
<div className="relative aspect-[4/3] overflow-hidden bg-gray-100">
🤖 Prompt for AI Agents
In frontend/src/components/listings/ListingCard.jsx around line 70, the Tailwind
class "aspect-4/3" is invalid; replace it with the arbitrary-value syntax
"aspect-[4/3]" so the aspect ratio renders correctly with default Tailwind.
Update the class string accordingly to use aspect-[4/3] instead of aspect-4/3.

Comment on lines +49 to +124
<div className="space-y-2">
{history.map((entry, index) => {
const previousPrice =
index < history.length - 1 ? history[index + 1].price : null;
const priceChange = getPriceChange(entry.price, previousPrice);

return (
<div
key={entry._id || index}
className="flex items-center justify-between p-4 bg-white border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
>
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
!priceChange
? "bg-blue-100"
: priceChange.change > 0
? "bg-red-100"
: priceChange.change < 0
? "bg-green-100"
: "bg-gray-100"
}`}
>
{!priceChange ? (
<Minus className="h-5 w-5 text-blue-600" />
) : priceChange.change > 0 ? (
<TrendingUp className="h-5 w-5 text-red-600" />
) : priceChange.change < 0 ? (
<TrendingDown className="h-5 w-5 text-green-600" />
) : (
<Minus className="h-5 w-5 text-gray-600" />
)}
</div>

<div>
<div className="flex items-center gap-2">
<IndianRupee className="h-4 w-4 text-gray-600" />
<span className="text-lg font-bold text-gray-900">
{entry.price.toLocaleString()}
</span>
{priceChange && (
<span
className={`text-xs font-medium px-2 py-1 rounded ${
priceChange.change > 0
? "bg-red-100 text-red-700"
: "bg-green-100 text-green-700"
}`}
>
{priceChange.change > 0 ? "+" : ""}
{priceChange.percentChange}%
</span>
)}
</div>
<div className="flex items-center gap-1 text-xs text-gray-500 mt-1">
<Calendar className="h-3 w-3" />
<span>{formatDate(entry.changedAt)}</span>
</div>
</div>
</div>

{priceChange && (
<div className="text-right">
<p className="text-xs text-gray-500">Changed by</p>
<p
className={`text-sm font-semibold ${
priceChange.change > 0 ? "text-red-600" : "text-green-600"
}`}
>
{priceChange.change > 0 ? "+" : ""}₹
{Math.abs(priceChange.change).toLocaleString()}
</p>
</div>
)}
</div>
);
})}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix price-change ordering before computing deltas.

The backend returns priceHistory in insertion order (oldest first). Iterating the raw array and comparing against history[index + 1] flips the delta sign, so price drops render as increases (and vice versa). Sort newest-first (or reverse) before mapping, then compare against the next element of that sorted array.

-      <div className="space-y-2">
-        {history.map((entry, index) => {
-          const previousPrice =
-            index < history.length - 1 ? history[index + 1].price : null;
+      const sortedHistory = [...history].sort(
+        (a, b) => new Date(b.changedAt) - new Date(a.changedAt)
+      );
+
+      <div className="space-y-2">
+        {sortedHistory.map((entry, index) => {
+          const previousPrice =
+            index < sortedHistory.length - 1
+              ? sortedHistory[index + 1].price
+              : null;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/components/listings/PriceHistory.jsx around lines 49 to 124, the
component iterates the backend's insertion-ordered (oldest-first) history and
compares entry to history[index + 1], which flips delta signs; create a sorted
copy newest-first before mapping (e.g. const sorted =
[...history].slice().reverse() or sort by changedAt descending) and iterate
sorted instead, computing previousPrice from sorted[index + 1]; do not mutate
the original history prop.

Comment on lines +258 to +263
// Upload new images if any
let newImageUrls = [];
if (formData.newImages.length > 0) {
toast.loading('Uploading new images...');
newImageUrls = await uploadNewImages();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Store toast IDs for targeted dismissal.

The toast.loading() calls on lines 261 and 286 don't store their IDs, so toast.dismiss() on line 285 and 289 may dismiss unrelated toasts. Store and dismiss specific toast IDs for better UX.

Apply this diff:

      // Upload new images if any
      let newImageUrls = [];
      if (formData.newImages.length > 0) {
-        toast.loading('Uploading new images...');
+        const uploadToastId = toast.loading('Uploading new images...');
         newImageUrls = await uploadNewImages();
+        toast.dismiss(uploadToastId);
      }

      // Combine existing and new images
      const allImages = [...formData.existingImages, ...newImageUrls];

      // Prepare update data
      const updateData = {
        title: formData.title,
        description: formData.description,
        category: formData.category,
        condition: formData.condition,
        price: formData.price,
        originalPrice: formData.originalPrice,
        negotiable: formData.negotiable,
        images: allImages,
        location: formData.location,
        hostel: formData.hostel,
        roomNumber: formData.roomNumber,
        additionalNotes: formData.additionalNotes,
      };

      // Update listing
-      toast.dismiss();
-      toast.loading('Updating listing...');
+      const updateToastId = toast.loading('Updating listing...');
       await listingService.updateListing(id, updateData);

-      toast.dismiss();
+      toast.dismiss(updateToastId);
       toast.success('Listing updated successfully!');

Also applies to: 285-286

🤖 Prompt for AI Agents
In frontend/src/pages/listings/EditListing.jsx around lines 258-263 and 285-286
the toast.loading() calls are invoked without storing their returned IDs, so
later toast.dismiss() calls may close unrelated toasts; capture the IDs returned
from toast.loading (e.g., const uploadToastId = toast.loading(...)) when showing
each loading toast and then call toast.dismiss(uploadToastId) (and the
corresponding id for the later toast) to dismiss those specific toasts rather
than calling toast.dismiss() with no args.

Comment on lines +142 to +287
const isOwner = user?._id === listing.seller?._id;

const breadcrumbItems = [
{ label: "Listings", link: "/listings" },
{ label: listing.category, link: `/listings?category=${listing.category}` },
{ label: listing.title },
];

const conditionColors = {
"brand-new": "bg-green-100 text-green-800",
"like-new": "bg-blue-100 text-blue-800",
good: "bg-yellow-100 text-yellow-800",
fair: "bg-orange-100 text-orange-800",
poor: "bg-red-100 text-red-800",
};

const statusColors = {
available: "bg-green-100 text-green-800",
sold: "bg-gray-100 text-gray-800",
reserved: "bg-yellow-100 text-yellow-800",
};

return (
<div className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
{/* Breadcrumb */}
<Breadcrumb items={breadcrumbItems} />

{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column - Images & Details */}
<div className="lg:col-span-2 space-y-6">
{/* Image Gallery */}
<ImageGallery images={listing.images} title={listing.title} />

{/* Listing Info Card */}
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-100">
{/* Title & Price */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900 mb-3">
{listing.title}
</h1>
<div className="flex items-center gap-3 flex-wrap">
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
statusColors[listing.status]
}`}
>
{listing.status.charAt(0).toUpperCase() +
listing.status.slice(1)}
</span>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
conditionColors[listing.condition]
}`}
>
{listing.condition
.split("-")
.map(
(word) => word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</span>
</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-blue-600">
₹{listing.price.toLocaleString()}
</div>
</div>
</div>

{/* Meta Information */}
<div className="flex flex-wrap gap-6 py-4 border-y border-gray-200 mb-6">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Tag className="h-5 w-5 text-gray-400" />
<span>{listing.category}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<MapPin className="h-5 w-5 text-gray-400" />
<span>{listing.location}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-5 w-5 text-gray-400" />
<span>
{new Date(listing.createdAt).toLocaleDateString()}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Package className="h-5 w-5 text-gray-400" />
<span>ID: {listing._id.slice(-8).toUpperCase()}</span>
</div>
</div>

{/* Description */}
<div className="mb-6">
<h2 className="text-xl font-bold text-gray-900 mb-3">
Description
</h2>
<p className="text-gray-700 whitespace-pre-line leading-relaxed">
{listing.description}
</p>
</div>

{/* Share Buttons */}
<div className="pt-6 border-t border-gray-200">
<ShareButtons listing={listing} />
</div>
</div>

{/* Owner Actions */}
{isOwner && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">
Manage Your Listing
</h3>
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={() => navigate(`/listings/${id}/edit`)}
className="flex-1 flex items-center justify-center gap-2 py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
>
<Edit className="h-5 w-5" />
Edit Listing
</button>
<Button
onClick={handleDelete}
disabled={deleting}
className="flex-1 flex items-center justify-center gap-2 py-3 px-4 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Trash2 className="h-5 w-5" />
{deleting ? "Deleting..." : "Delete Listing"}
</Button>
</div>
</div>
)}
</div>

{/* Right Column - Seller Info */}
<div className="lg:col-span-1">
<div className="sticky top-8">
<SellerInfoCard
seller={listing.seller}
onContact={handleContact}
/>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Wire seller data from owner instead of seller.

The document exposes the owner details under listing.owner. Referencing listing.seller leaves isOwner false, breaks the contact flow, and renders an empty seller card.

-  const isOwner = user?._id === listing.seller?._id;
+  const seller = listing.owner;
+  const isOwner = user?._id === seller?._id;-      `Hi ${listing.seller?.name},\n\nI'm interested in your listing "${listing.title}".\n\nPlease let me know if it's still available.\n\nThanks!`
+      `Hi ${seller?.name},\n\nI'm interested in your listing "${listing.title}".\n\nPlease let me know if it's still available.\n\nThanks!`-    window.location.href = `mailto:${listing.seller?.email}?subject=${subject}&body=${body}`;
+    window.location.href = `mailto:${seller?.email}?subject=${subject}&body=${body}`;-              <SellerInfoCard
-                seller={listing.seller}
-                onContact={handleContact}
-              />
+              <SellerInfoCard seller={seller} onContact={handleContact} />

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/src/pages/listings/ListingsDetails.jsx around lines 142 to 287, the
component incorrectly references listing.seller (causing isOwner to be wrong and
SellerInfoCard to render empty); replace all uses of listing.seller with
listing.owner within this range (including the isOwner check and the prop passed
to SellerInfoCard), keep optional chaining/null checks (e.g., user?._id ===
listing.owner?._id) to avoid runtime errors, and ensure any other seller-related
references in this block now point to listing.owner so contact/manage flows work
correctly.

Comment on lines +175 to +177
<ImageGallery images={listing.images} title={listing.title} />

{/* Listing Info Card */}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Map gallery images to URLs before rendering.

ImageGallery expects an array of URL strings, but listing.images contains objects. Passing the objects yields src="[object Object]" and the gallery never renders.

-            <ImageGallery images={listing.images} title={listing.title} />
+            <ImageGallery
+              images={(listing.images || []).map((image) => image.url)}
+              title={listing.title}
+            />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ImageGallery images={listing.images} title={listing.title} />
{/* Listing Info Card */}
<ImageGallery
images={(listing.images || []).map((image) => image.url)}
title={listing.title}
/>
{/* Listing Info Card */}
🤖 Prompt for AI Agents
In frontend/src/pages/listings/ListingsDetails.jsx around lines 175 to 177,
listing.images is an array of image objects but ImageGallery expects an array of
URL strings; map listing.images to extract the URL (e.g., listing.images.map(img
=> img.url || img.src || img.path)) and pass that array (or an empty array if
undefined) to ImageGallery so it receives strings instead of objects.

Comment on lines +220 to +228
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<MapPin className="h-5 w-5 text-gray-400" />
<span>{listing.location}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="h-5 w-5 text-gray-400" />
<span>
{new Date(listing.createdAt).toLocaleDateString()}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Render human-readable location info.

listing.location is an object, so interpolating it prints [object Object]. Surface the meaningful properties (e.g., hostel, pickup point) instead of the raw object.

-                <div className="flex items-center gap-2 text-sm text-gray-600">
-                  <MapPin className="h-5 w-5 text-gray-400" />
-                  <span>{listing.location}</span>
-                </div>
+                <div className="flex items-center gap-2 text-sm text-gray-600">
+                  <MapPin className="h-5 w-5 text-gray-400" />
+                  <span>
+                    {listing.location?.hostel}
+                    {listing.location?.pickupPoint
+                      ? ` • ${listing.location.pickupPoint}`
+                      : ""}
+                  </span>
+                </div>
🤖 Prompt for AI Agents
In frontend/src/pages/listings/ListingsDetails.jsx around lines 220 to 228, the
UI is interpolating listing.location which is an object and renders “[object
Object]”; replace the direct object interpolation with a human-readable string
built from the location object's meaningful properties (for example
listing.location.hostel and listing.location.pickupPoint or similar keys).
Update the JSX to safely access those properties (use optional chaining or null
checks), format them into a single display string (e.g., "Hostel Name — Pickup
Point" or join available fields with a comma), and provide a sensible fallback
like "Location unavailable" when fields are missing.


```bash
# Connect to MongoDB in Docker
docker exec -it campus-marketplace-mongo mongosh campus-marketplace
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Docker container names match docker-compose configuration.

The file references container names campus-marketplace-mongo and campus-marketplace-mongodb. Confirm these align with the actual service names in the docker-compose configuration to prevent connection errors.

Also applies to: 12-12


🏁 Script executed:

#!/bin/bash
# Verify Docker container/service names in docker-compose
fd -e 'docker-compose.yml' -e 'docker-compose.yaml' --exec grep -E 'services:|container_name:' {}

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Broader search for docker-compose files with any naming pattern
fd -type f -iname '*docker*compose*' | head -20

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Search for files containing "campus-marketplace" to locate configuration
rg -l 'campus-marketplace' --type yaml --type json --type toml -g '!node_modules' -g '!.git'

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Look for common docker/compose config locations and patterns
fd -type f \( -name '*.compose*' -o -name 'compose.*' -o -name 'docker-compose*' -o -name 'Dockerfile*' \)

Length of output: 170



🏁 Script executed:

#!/bin/bash
# Read docker-compose.yml to verify service and container names
cat docker-compose.yml

Length of output: 2048



🏁 Script executed:

#!/bin/bash
# Check lines 9-15 of markdown/dbQueries.md to see what's on line 12
sed -n '9,15p' markdown/dbQueries.md

Length of output: 211


Fix incorrect container name on line 12.

Line 12 references non-existent container campus-marketplace-mongodb, which should be campus-marketplace-mongo to match the docker-compose configuration. The command will fail as written.

Change:

docker exec campus-marketplace-mongodb mongosh campus-marketplace --eval 'QUERY_HERE'

To:

docker exec campus-marketplace-mongo mongosh campus-marketplace --eval 'QUERY_HERE'
🤖 Prompt for AI Agents
In markdown/dbQueries.md around line 9, the docker exec command uses the wrong
container name; update any occurrences of "campus-marketplace-mongodb" to
"campus-marketplace-mongo" so the command matches the docker-compose service
name (e.g., replace docker exec campus-marketplace-mongodb ... with docker exec
campus-marketplace-mongo ...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant