Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 126 additions & 6 deletions apps/web/src/components/dashboard/ProjectsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { DashboardProjectsProps } from "@/types";
import Image from "next/image";
import { useFilterStore } from "@/store/useFilterStore";
import { usePathname } from "next/navigation";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MagnifyingGlassIcon, FunnelIcon } from "@heroicons/react/24/outline";
import { useState, useMemo } from "react";

type ProjectsContainerProps = { projects: DashboardProjectsProps[] };

Expand Down Expand Up @@ -58,6 +59,53 @@ export default function ProjectsContainer({
const { projectTitle } = useProjectTitleStore();
const { setShowFilters } = useFilterStore();
const isProjectsPage = pathname === "/dashboard/projects";
const [searchQuery, setSearchQuery] = useState("");

// Client-side filtering of projects based on search query
// Memoized for performance optimization
const filteredProjects = useMemo(() => {
// Return all projects if no search query or empty projects array
if (!searchQuery.trim() || !projects || projects.length === 0) {
return projects || [];
}

const query = searchQuery.toLowerCase().trim();

// Filter projects by checking all searchable fields
return projects.filter((project) => {
// Search in project name
const nameMatch = project.name?.toLowerCase().includes(query) ?? false;

// Search in description
const descriptionMatch = project.description?.toLowerCase().includes(query) ?? false;

// Search in primary language
const languageMatch = project.primaryLanguage?.toLowerCase().includes(query) ?? false;

// Search in stage
const stageMatch = project.stage?.toLowerCase().includes(query) ?? false;

// Search in popularity
const popularityMatch = project.popularity?.toLowerCase().includes(query) ?? false;

// Search in competition level
const competitionMatch = project.competition?.toLowerCase().includes(query) ?? false;

// Search in activity level
const activityMatch = project.activity?.toLowerCase().includes(query) ?? false;

// Return true if any field matches the query
return (
nameMatch ||
descriptionMatch ||
languageMatch ||
stageMatch ||
popularityMatch ||
competitionMatch ||
activityMatch
);
});
}, [projects, searchQuery]);

return (
<div className="w-full p-6 sm:p-6">
Expand All @@ -67,14 +115,61 @@ export default function ProjectsContainer({
</h2>
{isProjectsPage && (
<Button
className="font-semibold text-white bg-ox-purple text-sm sm:text-base h-10 sm:h-11 px-5 sm:px-6 hover:bg-white-500 rounded-md"
className="font-semibold text-white bg-ox-purple text-sm sm:text-base h-10 sm:h-11 px-5 sm:px-6 hover:bg-ox-purple/90 rounded-md flex items-center gap-2"
onClick={() => setShowFilters(true)}
>
Find projects
<FunnelIcon className="size-4 sm:size-5" />
<span className="hidden sm:inline">Filter Projects</span>
<span className="sm:hidden">Filter</span>
</Button>
)}
</div>

{/* Search Input for Quick Filtering */}
{isProjectsPage && projects && projects.length > 0 && (
<div className="mb-4">
<div className="relative max-w-md">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 size-5 text-zinc-400" />
<input
type="text"
placeholder="Search projects by name, description, language, stage, popularity..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-[#15161a] border border-[#1a1a1d] rounded-md text-white text-sm placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-ox-purple focus:border-transparent transition-all"
aria-label="Search projects"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-zinc-400 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-ox-purple focus:ring-offset-2 focus:ring-offset-[#15161a] rounded"
aria-label="Clear search"
>
<svg
className="size-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
{searchQuery && filteredProjects !== undefined && (
<p className="mt-2 text-xs text-zinc-400">
Showing {filteredProjects.length} of {projects.length} project{projects.length !== 1 ? 's' : ''}
</p>
)}
</div>
)}

{projects && projects.length > 0 ? (
<div
className="
Expand Down Expand Up @@ -109,7 +204,8 @@ export default function ProjectsContainer({
</TableHeader>

<TableBody>
{projects.map((p) => (
{filteredProjects && filteredProjects.length > 0 ? (
filteredProjects.map((p) => (
<TableRow
key={p.id}
className="border-y border-ox-gray cursor-pointer hover:bg-white/5 transition-colors"
Expand Down Expand Up @@ -158,7 +254,31 @@ export default function ProjectsContainer({
{p.activity}
</TableCell>
</TableRow>
))}
))
) : (
<TableRow>
<TableCell
colSpan={tableColumns.length}
className="text-center py-12 text-zinc-400"
>
<div className="flex flex-col items-center gap-2">
<MagnifyingGlassIcon className="size-12 text-ox-purple/50" />
<p className="text-base font-medium">No projects found</p>
<p className="text-sm">
Try adjusting your search query or{" "}
<button
onClick={() => setSearchQuery("")}
type="button"
className="text-ox-purple hover:underline focus:outline-none focus:ring-2 focus:ring-ox-purple focus:ring-offset-2 focus:ring-offset-transparent rounded px-1"
aria-label="Clear search"
>
clear the search
</button>
</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
Expand All @@ -169,7 +289,7 @@ export default function ProjectsContainer({
<p className="text-xl font-medium">Find Your Next Project</p>
</div>
<p className="text-base text-center max-w-md">
Click the &apos;Find projects&apos; button above to discover open
Click the &apos;Filter Projects&apos; button above to discover open
source projects that match your interests
</p>
</div>
Expand Down