Skip to content

Commit

Permalink
fixed the explore experts page with search and filter working
Browse files Browse the repository at this point in the history
  • Loading branch information
teetangh committed Dec 12, 2024
1 parent c0f6933 commit acd877a
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 93 deletions.
8 changes: 5 additions & 3 deletions app/explore/experts/components/ConsultantCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export function ConsultantCard({ consultant, metadata }: ConsultantCardProps) {
)}
</div>
<div className="text-sm space-y-2">
<p className="text-black dark:text-black">{consultant.description}</p>
<p className="text-black dark:text-black">
{consultant.description}
</p>
<div className="flex flex-wrap gap-2">
<span className="text-black dark:text-black">
Experience: {consultant.experience}
Expand Down Expand Up @@ -108,8 +110,8 @@ export function ConsultantCard({ consultant, metadata }: ConsultantCardProps) {
<div className="flex items-center space-x-2 text-black dark:text-black">
<StarIcon className="w-4 h-4" />
<span>
{consultant.rating.toFixed(1)} ({consultant.reviews?.length || 0}{" "}
reviews)
{consultant.rating.toFixed(1)} (
{consultant.reviews?.length || 0} reviews)
</span>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/explore/experts/components/FeaturedExperts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export function FeaturedExperts() {
Top Consultants
</h2>
<p className="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
Discover the best of the best. Our top consultants are ready to help
you with your business needs.
Discover the best of the best. Our top consultants are ready to
help you with your business needs.
</p>
</div>
</div>
Expand Down
59 changes: 12 additions & 47 deletions app/explore/experts/components/FiltersSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
Expand All @@ -9,7 +8,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Domain, SubDomain, Tag } from "@prisma/client";
import { useState, useEffect } from "react";
import { useState } from "react";

interface FiltersSectionProps {
metadata: {
Expand All @@ -25,8 +24,6 @@ interface FiltersSectionProps {
setSelectedTags: (tags: string[]) => void;
experienceYears: number;
setExperienceYears: (years: number) => void;
pricing: number;
setPricing: (price: number) => void;
}

function XIcon(props: React.SVGProps<SVGSVGElement>) {
Expand Down Expand Up @@ -59,18 +56,10 @@ export function FiltersSection({
setSelectedTags,
experienceYears,
setExperienceYears,
pricing,
setPricing,
}: FiltersSectionProps) {
const [searchTerm, setSearchTerm] = useState("");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

// Reset subdomain and tags when domain changes
useEffect(() => {
setSelectedSubdomain(null);
setSelectedTags([]);
}, [selectedDomain, setSelectedSubdomain, setSelectedTags]);

const handleDomainChange = (value: string) => {
setSelectedDomain(value);
setSelectedSubdomain(null);
Expand All @@ -96,15 +85,16 @@ export function FiltersSection({
};

// Filter tags based on selected domain and search term
const filteredTags = metadata?.tags.filter((tag) => {
if (selectedDomain && tag.domainId !== selectedDomain) return false;
if (!searchTerm) return true;
if (selectedTags.includes(tag.name)) return false;
return tag.name.toLowerCase().includes(searchTerm.toLowerCase());
}) || [];
const filteredTags =
metadata?.tags.filter((tag) => {
if (selectedDomain && tag.domainId !== selectedDomain) return false;
if (!searchTerm) return true;
if (selectedTags.includes(tag.name)) return false;
return tag.name.toLowerCase().includes(searchTerm.toLowerCase());
}) || [];

return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="border border-gray-200 rounded-lg p-4 flex flex-col justify-between dark:border-gray-800">
<div>
<label
Expand Down Expand Up @@ -176,7 +166,9 @@ export function FiltersSection({
<input
className="bg-white border border-gray-300 text-black text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
id="tags"
placeholder={selectedDomain ? "Search tags..." : "Select a domain first"}
placeholder={
selectedDomain ? "Search tags..." : "Select a domain first"
}
type="text"
value={searchTerm}
onChange={handleInputChange}
Expand Down Expand Up @@ -243,33 +235,6 @@ export function FiltersSection({
</div>
</div>
</div>
<div className="border border-gray-200 rounded-lg p-4 flex flex-col justify-between dark:border-gray-800">
<div>
<label
className="block mb-2 text-sm font-medium text-black"
htmlFor="pricing"
>
Pricing (per hour)
</label>
<input
type="range"
min="0"
max="1000"
step="10"
value={pricing}
onChange={(e) => setPricing(Number(e.target.value))}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<div className="flex justify-between mt-2 text-sm text-gray-600">
<span>$0</span>
<span>$500</span>
<span>$1000+</span>
</div>
<div className="text-center mt-2 font-semibold text-lg">
${pricing === 1000 ? "1000+" : pricing}
</div>
</div>
</div>
</div>
);
}
33 changes: 30 additions & 3 deletions app/explore/experts/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
"use client";

import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

export type SortOption = "nameAsc" | "nameDesc" | "reviewCount" | "rating";

interface SearchBarProps {
onSearch: (value: string) => void;
onSort: (option: SortOption) => void;
sortBy: SortOption;
}

export function SearchBar({ onSearch }: SearchBarProps) {
export function SearchBar({ onSearch, onSort, sortBy }: SearchBarProps) {
return (
<div className="border border-gray-200 rounded-lg grid items-center p-2 dark:border-gray-800">
<div className="border border-gray-200 rounded-lg grid grid-cols-[1fr,auto] items-center p-2 gap-4 dark:border-gray-800">
<Input
className="appearance-none w-full border-0 focus:outline-none placeholder-gray-500 dark:placeholder-gray-400"
className="appearance-none border-0 focus:outline-none placeholder-gray-500 dark:placeholder-gray-400"
placeholder="Search for experts"
type="search"
onChange={(e) => onSearch(e.target.value)}
/>
<div className="w-48">
<Select
value={sortBy}
onValueChange={(value) => onSort(value as SortOption)}
>
<SelectTrigger>
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="nameAsc">Name (A-Z)</SelectItem>
<SelectItem value="nameDesc">Name (Z-A)</SelectItem>
<SelectItem value="reviewCount">Most Reviews</SelectItem>
<SelectItem value="rating">Highest Rating</SelectItem>
</SelectContent>
</Select>
</div>
</div>
);
}
3 changes: 2 additions & 1 deletion app/explore/experts/components/Testimonials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export function Testimonials() {
</span>
</h2>
<p className="mx-auto max-w-[600px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
Hear from our valued customers about their experience with our experts.
Hear from our valued customers about their experience with our
experts.
</p>
</div>
<div className="mx-auto w-full max-w-sm space-y-2">
Expand Down
94 changes: 57 additions & 37 deletions app/explore/experts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FiltersSection } from "./components/FiltersSection";
import { ConsultantCard } from "./components/ConsultantCard";
import { FeaturedExperts } from "./components/FeaturedExperts";
import { Testimonials } from "./components/Testimonials";
import { SearchBar } from "./components/SearchBar";
import { SearchBar, SortOption } from "./components/SearchBar";

interface MetaData {
domains: Domain[];
Expand All @@ -31,9 +31,11 @@ function FindExperts() {
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [experienceYears, setExperienceYears] = useState(0);
const [pricing, setPricing] = useState(0);
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
const [selectedSubdomain, setSelectedSubdomain] = useState<string | null>(null);
const [selectedSubdomain, setSelectedSubdomain] = useState<string | null>(
null,
);
const [sortBy, setSortBy] = useState<SortOption>("nameAsc");

useEffect(() => {
async function fetchData() {
Expand Down Expand Up @@ -67,20 +69,29 @@ function FindExperts() {
if (consultant.user.email?.toLowerCase().includes(searchLower)) return true;

// Search in consultant details
if (consultant.description?.toLowerCase().includes(searchLower)) return true;
if (consultant.specialization?.toLowerCase().includes(searchLower)) return true;
if (consultant.qualifications?.toLowerCase().includes(searchLower)) return true;
if (consultant.description?.toLowerCase().includes(searchLower))
return true;
if (consultant.specialization?.toLowerCase().includes(searchLower))
return true;
if (consultant.qualifications?.toLowerCase().includes(searchLower))
return true;

// Search in domain and subdomain names
if (consultant.domain.name.toLowerCase().includes(searchLower)) return true;
if (consultant.subDomains.some(sd =>
sd.name.toLowerCase().includes(searchLower)
)) return true;
if (
consultant.subDomains.some((sd) =>
sd.name.toLowerCase().includes(searchLower),
)
)
return true;

// Search in tags
if (consultant.tags.some(tag =>
tag.name.toLowerCase().includes(searchLower)
)) return true;
if (
consultant.tags.some((tag) =>
tag.name.toLowerCase().includes(searchLower),
)
)
return true;

return false;
};
Expand All @@ -98,7 +109,7 @@ function FindExperts() {
const filterByTags = (consultant: TConsultantProfile) => {
if (selectedTags.length === 0) return true;
return selectedTags.every((tagName) =>
consultant.tags.some((t) => t.name === tagName)
consultant.tags.some((t) => t.name === tagName),
);
};

Expand All @@ -114,48 +125,58 @@ function FindExperts() {
return years >= experienceYears;
};

const filterByPrice = (consultant: TConsultantProfile) => {
if (pricing === 0) return true;
// Check if any subscription plan's price is less than or equal to the filter price
return consultant.subscriptionPlans.some(
(plan) => plan.price / 100 <= pricing
);
// Sort function
const sortConsultants = (consultants: TConsultantProfile[]) => {
return [...consultants].sort((a, b) => {
switch (sortBy) {
case "nameAsc":
return (a.user.name || "").localeCompare(b.user.name || "");
case "nameDesc":
return (b.user.name || "").localeCompare(a.user.name || "");
case "reviewCount":
return (b.reviews?.length || 0) - (a.reviews?.length || 0);
case "rating":
return (b.rating || 0) - (a.rating || 0);
default:
return 0;
}
});
};

// Apply all filters using useMemo to optimize performance
const filteredConsultants = useMemo(() => {
return consultants.filter(
// Apply all filters and sorting using useMemo
const filteredAndSortedConsultants = useMemo(() => {
const filtered = consultants.filter(
(consultant) =>
filterBySearch(consultant) &&
filterByDomain(consultant) &&
filterBySubdomain(consultant) &&
filterByTags(consultant) &&
filterByExperience(consultant) &&
filterByPrice(consultant)
filterByExperience(consultant),
);
return sortConsultants(filtered);
}, [
consultants,
searchTerm,
selectedDomain,
selectedSubdomain,
selectedTags,
experienceYears,
pricing,
sortBy,
]);

// Group consultants by domain
const groupedConsultants = useMemo(() => {
const grouped = new Map<string, TConsultantProfile[]>();
filteredConsultants.forEach((consultant) => {

filteredAndSortedConsultants.forEach((consultant) => {
if (!grouped.has(consultant.domain.id)) {
grouped.set(consultant.domain.id, []);
}
grouped.get(consultant.domain.id)?.push(consultant);
});

return grouped;
}, [filteredConsultants]);
}, [filteredAndSortedConsultants]);

if (isLoading) {
return (
Expand All @@ -172,8 +193,8 @@ function FindExperts() {
Find an Expert
</h1>
<p className="text-gray-500 grid-rows-2 dark:text-gray-400">
Search for experts in various fields. Enter keywords to find experts in
specific areas.
Search for experts in various fields. Enter keywords to find experts
in specific areas.
</p>
</div>

Expand All @@ -187,11 +208,9 @@ function FindExperts() {
setSelectedTags={setSelectedTags}
experienceYears={experienceYears}
setExperienceYears={setExperienceYears}
pricing={pricing}
setPricing={setPricing}
/>

<SearchBar onSearch={setSearchTerm} />
<SearchBar onSearch={setSearchTerm} onSort={setSortBy} sortBy={sortBy} />

<div className="space-y-4">
{metadata?.domains.map((domain) => {
Expand All @@ -212,11 +231,12 @@ function FindExperts() {
</div>
);
})}
{filteredConsultants.length === 0 && (

{filteredAndSortedConsultants.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-500 text-lg">
No consultants found matching your criteria. Try adjusting your filters.
No consultants found matching your criteria. Try adjusting your
filters.
</p>
</div>
)}
Expand Down

0 comments on commit acd877a

Please sign in to comment.