diff --git a/client/src/api/searchService.js b/client/src/api/searchService.js
new file mode 100644
index 0000000..88e54f6
--- /dev/null
+++ b/client/src/api/searchService.js
@@ -0,0 +1,72 @@
+import axiosInstance from './axiosInstance';
+
+// Search for users with various filters
+export const searchUsers = async (params) => {
+ try {
+ // Log the params being sent to the API
+ console.log('Sending search params to API:', params);
+
+ // Make sure we're using the correct endpoint
+ const response = await axiosInstance.get('/search/users', {
+ params,
+ // Add timeout to prevent long-hanging requests
+ timeout: 10000
+ });
+
+ // Log the response for debugging
+ console.log('Search API response:', response.data);
+
+ return response.data;
+ } catch (error) {
+ console.error('Error searching users:', error.response || error);
+ // Return a structured error object instead of throwing
+ return {
+ success: false,
+ message: error.response?.data?.message || 'Error searching users',
+ error: error.message
+ };
+ }
+};
+
+// Search for jobs with various filters
+export const searchJobs = async (params) => {
+ try {
+ // Create a copy of params to avoid modifying the original
+ const searchParams = { ...params };
+
+ // Fix the query format for Solr
+ // If query is "*:*", leave it as is, otherwise format it properly
+ if (searchParams.query && searchParams.query !== "*:*") {
+ // Remove special characters that might cause parsing issues
+ const sanitizedQuery = searchParams.query.replace(/:/g, " ");
+ searchParams.query = sanitizedQuery;
+ }
+
+ // Log the params being sent to the API for debugging
+ console.log('Sending job search params to API:', searchParams);
+
+ const response = await axiosInstance.get('/search/jobs', {
+ params: searchParams,
+ timeout: 10000 // Add timeout to prevent long-hanging requests
+ });
+
+ console.log('Search jobs API response:', response.data);
+
+ return {
+ success: true,
+ jobs: response.data.jobs || [],
+ total: response.data.total || 0,
+ ...response.data
+ };
+ } catch (error) {
+ console.error('Error searching jobs:', error.response || error);
+ return {
+ success: false,
+ message: error.response?.data?.message || 'Error searching jobs',
+ error: error.message,
+ jobs: []
+ };
+ }
+};
+
+
diff --git a/client/src/components/MarketPlace/MarketPlace.jsx b/client/src/components/MarketPlace/MarketPlace.jsx
index 0ade2b9..4266f8f 100644
--- a/client/src/components/MarketPlace/MarketPlace.jsx
+++ b/client/src/components/MarketPlace/MarketPlace.jsx
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react";
-import { FaSearch, FaSort } from "react-icons/fa";
+import { FaSearch, FaSort, FaFilter } from "react-icons/fa";
import axiosInstance from "../../api/axiosInstance";
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectRole } from "../../redux/Features/user/authSlice";
+import { searchJobs } from "../../api/searchService"; // Import the search service
const Marketplace = () => {
const userRole = useSelector(selectRole);
@@ -13,79 +14,203 @@ const Marketplace = () => {
const [budgetMin, setBudgetMin] = useState("");
const [budgetMax, setBudgetMax] = useState("");
const [jobs, setJobs] = useState([]);
- const [categories, setCategories] = useState([]);
+ // Start with default categories
+ const [categories, setCategories] = useState([
+ "Web Development",
+ "Mobile Development",
+ "Design",
+ "Marketing",
+ "Writing",
+ "Admin Support"
+ ]); // Default categories
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [sortBy, setSortBy] = useState("latest");
+ const [page, setPage] = useState(1);
+ const [limit] = useState(20);
+
+ // New state to track form values separately from search params
+ const [formValues, setFormValues] = useState({
+ searchTerm: "",
+ category: "",
+ budgetMin: "",
+ budgetMax: "",
+ sortBy: "latest"
+ });
+ // Modified to use searchJobs from searchService
const fetchJobs = async () => {
+ setLoading(true);
try {
- const response = await axiosInstance.get("/jobs/marketplace");
- // Sort jobs by createdAt date
- const sortedJobs = response.data.sort((a, b) =>
- new Date(b.createdAt) - new Date(a.createdAt)
- );
- setJobs(sortedJobs);
+ // Build search params
+ const searchParams = {
+ query: searchTerm || "*:*",
+ limit,
+ page,
+ };
+
+ // Add filters if they exist
+ if (selectedCategory) searchParams.categories = selectedCategory;
+ if (budgetMin) searchParams.minBudget = parseInt(budgetMin);
+ if (budgetMax) searchParams.maxBudget = parseInt(budgetMax);
+
+ // Add sort parameter - using fields that exist in Solr schema
+ switch (sortBy) {
+ case "latest":
+ searchParams.sort = "_version_ desc"; // Use _version_ as a proxy for creation time
+ break;
+ case "oldest":
+ searchParams.sort = "_version_ asc";
+ break;
+ case "budget-high":
+ searchParams.sort = "budget_max desc";
+ break;
+ case "budget-low":
+ searchParams.sort = "budget_min asc";
+ break;
+ default:
+ searchParams.sort = "_version_ desc";
+ }
+
+ // Use the searchJobs function from searchService
+ const response = await searchJobs(searchParams);
+
+ if (response.success === false) {
+ throw new Error(response.message);
+ }
+
+ setJobs(response.jobs || []);
setLoading(false);
-
- const allCategories = new Set();
- response.data.forEach((job) => {
- job.categories.forEach((category) => allCategories.add(category));
- });
- setCategories([...allCategories]);
} catch (err) {
- setError("Error fetching jobs");
+ setError("Error fetching jobs: " + (err.message || "Unknown error"));
setLoading(false);
}
};
+ // Fetch categories by extracting unique categories from jobs and combining with defaults
+ const fetchCategories = async () => {
+ try {
+ const response = await axiosInstance.get("/jobs/jobs/filtered");
+ if (response.data && Array.isArray(response.data.jobs)) {
+ // Start with a Set containing the default categories
+ const allCategories = new Set(categories);
+
+ // Add any new categories from jobs
+ response.data.jobs.forEach((job) => {
+ if (job.categories && Array.isArray(job.categories)) {
+ job.categories.forEach((category) => allCategories.add(category));
+ }
+ });
+
+ // Update categories state with the combined unique categories
+ setCategories(Array.from(allCategories));
+ }
+ } catch (err) {
+ console.error("Error fetching categories:", err);
+ // Keep using default categories if there's an error
+ }
+ };
+
useEffect(() => {
- fetchJobs();
- }, []);
+ fetchCategories(); // Fetch categories once when component mounts
+ fetchJobs(); // Initial fetch of jobs
+ }, [page]); // Only re-fetch when page changes
- const isNewJob = (createdAt) => {
- const jobDate = new Date(createdAt);
- const sevenDaysAgo = new Date();
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
- return jobDate >= sevenDaysAgo;
+ // Handle form input changes
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormValues({
+ ...formValues,
+ [name]: value
+ });
};
- const sortJobs = (jobs) => {
- switch (sortBy) {
+ // Handle search button click - updated to use form values directly
+ const handleSearch = () => {
+ // Update the search parameters directly from formValues
+ const searchParams = {
+ query: formValues.searchTerm || "*:*",
+ limit,
+ page: 1, // Reset to first page
+ };
+
+ // Add filters if they exist
+ if (formValues.category) searchParams.categories = formValues.category;
+ if (formValues.budgetMin) searchParams.minBudget = parseInt(formValues.budgetMin);
+ if (formValues.budgetMax) searchParams.maxBudget = parseInt(formValues.budgetMax);
+
+ // Add sort parameter - using fields that exist in Solr schema
+ switch (formValues.sortBy) {
case "latest":
- return [...jobs].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+ searchParams.sort = "_version_ desc";
+ break;
case "oldest":
- return [...jobs].sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
+ searchParams.sort = "_version_ asc";
+ break;
case "budget-high":
- return [...jobs].sort((a, b) => (b.budget?.max || 0) - (a.budget?.max || 0));
+ searchParams.sort = "budget_max desc";
+ break;
case "budget-low":
- return [...jobs].sort((a, b) => (a.budget?.min || 0) - (b.budget?.min || 0));
+ searchParams.sort = "budget_min asc";
+ break;
default:
- return jobs;
+ searchParams.sort = "_version_ desc";
}
+
+ // Set loading state
+ setLoading(true);
+
+ // Call searchJobs directly with the form values
+ searchJobs(searchParams)
+ .then(response => {
+ if (response.success === false) {
+ throw new Error(response.message);
+ }
+
+ // Update state with search results
+ setJobs(response.jobs || []);
+ setSearchTerm(formValues.searchTerm);
+ setSelectedCategory(formValues.category);
+ setBudgetMin(formValues.budgetMin);
+ setBudgetMax(formValues.budgetMax);
+ setSortBy(formValues.sortBy);
+ setPage(1);
+ setLoading(false);
+ })
+ .catch(err => {
+ setError("Error fetching jobs: " + (err.message || "Unknown error"));
+ setLoading(false);
+ });
};
- const filteredJobs = sortJobs(jobs.filter((job) => {
- const jobMin = job.budget?.min || 0;
- const jobMax = job.budget?.max || 0;
- const min = budgetMin ? parseInt(budgetMin) : 0;
- const max = budgetMax ? parseInt(budgetMax) : Infinity;
+ const isNewJob = (createdAt) => {
+ if (!createdAt) return false;
+
+ // Handle array format
+ const dateStr = Array.isArray(createdAt) ? createdAt[0] : createdAt;
+
+ const jobDate = new Date(dateStr);
+ const sevenDaysAgo = new Date();
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
+ return jobDate >= sevenDaysAgo;
+ };
- return (
- (job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
- job.description.toLowerCase().includes(searchTerm.toLowerCase())) &&
- (selectedCategory ? job.categories.includes(selectedCategory) : true) &&
- ((jobMin <= min && jobMin <= max) || min === 0) &&
- ((jobMax >= min && jobMax <= max) || max === Infinity)
- );
- }));
+ // For pagination
+ const handleNextPage = () => {
+ setPage(prev => prev + 1);
+ };
+
+ const handlePrevPage = () => {
+ setPage(prev => Math.max(1, prev - 1));
+ };
+ // Use jobs directly instead of filteredJobs
return (
-
+
{/* Background Animation */}
-
-
-
+
{/* Main Content */}
@@ -93,7 +218,7 @@ const Marketplace = () => {
Marketplace
@@ -103,28 +228,28 @@ const Marketplace = () => {
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
- className="space-y-6"
+ className="bg-gray-800/50 backdrop-blur-sm p-6 rounded-xl mb-8 shadow-xl border border-gray-700/50"
>
- {/* Search Bar */}
-
-
+
+ {/* Search Input */}
+
setSearchTerm(e.target.value)}
+ className="w-full bg-gray-900/50 border border-gray-600/30 rounded-lg px-6 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-white pl-12 transition-all duration-300"
+ value={formValues.searchTerm}
+ onChange={handleInputChange}
/>
-
- {/* Filters */}
-
+ {/* Category Select */}
-
-
+ {/* Budget Inputs */}
setBudgetMin(e.target.value)}
+ className="w-full bg-gray-900/50 border border-gray-600/30 rounded-lg px-6 py-3 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all duration-300"
+ value={formValues.budgetMin}
+ onChange={handleInputChange}
/>
setBudgetMax(e.target.value)}
+ className="w-full bg-gray-900/50 border border-gray-600/30 rounded-lg px-6 py-3 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all duration-300"
+ value={formValues.budgetMax}
+ onChange={handleInputChange}
/>
+
+
+ {/* Sort Select */}
+
+
+ {/* Search Button */}
+
+
- {/* Job Listings */}
- {loading ? (
-
-
+ {/* Loading State */}
+ {loading && (
+
- ) : error ? (
-
- {error}
+ )}
+
+ {/* Error State */}
+ {error && (
+
- ) : (
-
- {filteredJobs.map((job, index) => (
+ )}
+
+ {/* Jobs Grid */}
+ {!loading && !error && jobs.length > 0 ? (
+
+ {jobs.map((job) => (
-
- {/* New Badge */}
- {isNewJob(job.createdAt) && (
-
-
- New
+
+
+
+ {Array.isArray(job.title) ? job.title[0] : job.title}
+
+ {isNewJob(job.created_at) && (
+
+ NEW
-
- )}
-
- {/* Job Title */}
-
- {job.title}
-
-
- {/* Job Description */}
-
- {job.description}
+ )}
+
+
+ {Array.isArray(job.description) ? job.description[0] : job.description}
-
- {/* Budget Section */}
-
-
- Budget:{" "}
-
- ₹{(job.budget?.min || 0).toLocaleString()} - ₹{(job.budget?.max || 0).toLocaleString()}
+
+ {Array.isArray(job.categories) && job.categories.map((category, index) => (
+
+ {category}
-
-
-
- {/* Skills Required */}
-
-
Skills Required:
-
- {job.skillsRequired.map((skill, index) => (
-
- {skill}
-
- ))}
-
+ ))}
-
- {/* Categories */}
-
-
Categories:
-
- {job.categories.map((category, index) => (
-
- {category}
-
- ))}
+
+
+ ${Array.isArray(job.budget_min) ? job.budget_min[0] : job.budget_min} -
+ ${Array.isArray(job.budget_max) ? job.budget_max[0] : job.budget_max}
+
+ View Details →
+
-
- {/* Bid Now/View Details Button */}
-
- {userRole === "enterprise" ? "View Details" : "Bid Now"}
-
))}
-
+
+ ) : (
+ !loading && !error && (
+
+
No jobs found matching your criteria
+
+ )
+ )}
+
+ {/* Pagination */}
+ {!loading && !error && jobs.length > 0 && (
+
+
+
+ Page {page}
+
+
+
)}
diff --git a/client/src/components/admin/AdminAnalytics.jsx b/client/src/components/admin/AdminAnalytics.jsx
index 7f4a029..7129f25 100644
--- a/client/src/components/admin/AdminAnalytics.jsx
+++ b/client/src/components/admin/AdminAnalytics.jsx
@@ -772,34 +772,18 @@ const AdminAnalytics = () => {
cy="50%"
innerRadius={60}
outerRadius={80}
- fill="#8884d8"
+ fill={darkThemeColors.primary.main}
+ paddingAngle={5}
dataKey="value"
- nameKey="name"
- label={({ name, percent }) =>
- `${name}: ${(percent * 100).toFixed(0)}%`
- }
- labelLine={false}
>
{userStats.roleDistribution.map((entry, index) => (
-
|
+
|
))}
-
[`${value} Users`, name]}
- contentStyle={{
- backgroundColor: darkThemeColors.background.lighter,
- border: `1px solid ${darkThemeColors.divider}`,
- color: darkThemeColors.text.primary,
- }}
- />
+ } />