mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-20 14:34:05 +01:00
feat: filtering functionality on all deals page
This commit is contained in:
parent
a2b90d9fb9
commit
86103baa7f
@ -1,66 +1,166 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Clock3Icon, MessageSquareIcon, UserIcon, UsersIcon } from "lucide-react";
|
import { Clock3Icon, UserIcon, UsersIcon } from "lucide-react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ProjectCard } from "@/components/projectCard";
|
import { ProjectCard } from "@/components/projectCard";
|
||||||
|
import { getAllTagsQuery, getALlFundedStatusQuery, getAllBusinessTypeQuery } from "@/lib/data/dropdownQuery";
|
||||||
|
import { createSupabaseClient } from "@/lib/supabase/clientComponentClient";
|
||||||
|
import { useQuery } from "@supabase-cache-helpers/postgrest-react-query";
|
||||||
|
import { searchProjectsQuery, FilterParams, FilterProjectQueryParams } from "@/lib/data/projectQuery";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const ProjectSection = ({ filteredProjects }) => {
|
||||||
|
interface Tags {
|
||||||
|
tag_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filteredProjects) {
|
||||||
|
return <div>No projects found!</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<h2 className="text-2xl">Deals</h2>
|
||||||
|
<p className="mt-3">The deals attracting the most interest right now</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Block for all the deals */}
|
||||||
|
<div className="mt-10 grid grid-cols-3 gap-4">
|
||||||
|
{filteredProjects.map((item, index) => (
|
||||||
|
<ProjectCard
|
||||||
|
key={index}
|
||||||
|
name={item.project_name}
|
||||||
|
description={item.project_short_description}
|
||||||
|
joinDate={item.published_time}
|
||||||
|
imageUri={item.card_image_url}
|
||||||
|
location={item.business_location}
|
||||||
|
minInvestment={item.min_investment}
|
||||||
|
totalInvestor={item.total_investment}
|
||||||
|
totalRaised={item.target_investment}
|
||||||
|
tags={item.tags.map((tag: Tags) => tag.tag_name)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShowFilter = ({ filterParams, clearAll }: { filterParams: FilterParams; clearAll: () => void }) => {
|
||||||
|
const { searchTerm, tagsFilter, projectStatusFilter, businessTypeFilter, sortByTimeFilter } = filterParams;
|
||||||
|
|
||||||
|
if (!searchTerm && !tagsFilter && !projectStatusFilter && !businessTypeFilter && !sortByTimeFilter) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
projectStatusFilter === "all" &&
|
||||||
|
businessTypeFilter === "all" &&
|
||||||
|
sortByTimeFilter === "all" &&
|
||||||
|
(!tagsFilter || tagsFilter.length === 0)
|
||||||
|
) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{searchTerm && (
|
||||||
|
<Button key={searchTerm} variant="secondary">
|
||||||
|
{searchTerm}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tagsFilter &&
|
||||||
|
tagsFilter.map((tag: string) => (
|
||||||
|
<Button key={tag} variant="secondary">
|
||||||
|
{tag}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{projectStatusFilter && projectStatusFilter !== "all" && (
|
||||||
|
<Button key={projectStatusFilter} variant="secondary">
|
||||||
|
{projectStatusFilter}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{businessTypeFilter && businessTypeFilter !== "all" && (
|
||||||
|
<Button key={businessTypeFilter} variant="secondary">
|
||||||
|
{businessTypeFilter}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sortByTimeFilter && sortByTimeFilter !== "all" && (
|
||||||
|
<Button key={sortByTimeFilter} variant="secondary">
|
||||||
|
{sortByTimeFilter}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Clear All button */}
|
||||||
|
<Button variant="destructive" onClick={clearAll}>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Deals() {
|
export default function Deals() {
|
||||||
const [postAtFilter, setPostAtFilter] = useState("");
|
const supabase = createSupabaseClient();
|
||||||
const [contentTypeFilter, setContentTypeFilter] = useState("");
|
|
||||||
const [authorFilter, setAuthorFilter] = useState("");
|
|
||||||
const [groupsFilter, setGroupFilter] = useState("");
|
|
||||||
const [selectedTag, setSelectedTag] = useState("");
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
name: "NVDA",
|
|
||||||
description: "Founded in 1993, NVIDIA is a key innovator of computer graphics and AI technology",
|
|
||||||
joinDate: "December 2021",
|
|
||||||
location: "Bangkok, Thailand",
|
|
||||||
tags: ["AI", "Technology"],
|
|
||||||
imageUri: "/login.png",
|
|
||||||
minInvestment: 10000,
|
|
||||||
totalInvestor: 58400,
|
|
||||||
totalRaised: 9000000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Apple Inc.",
|
|
||||||
description:
|
|
||||||
"Founded in 1976, Apple Inc. is a leading innovator in consumer electronics, software, and online services, known for products like the iPhone, MacBook, and the App Store.",
|
|
||||||
joinDate: "February 2020",
|
|
||||||
location: "Cupertino, California, USA",
|
|
||||||
tags: ["Consumer Electronics", "Software"],
|
|
||||||
imageUri: "/money.png",
|
|
||||||
minInvestment: 10000,
|
|
||||||
totalInvestor: 58400,
|
|
||||||
totalRaised: 9000000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Google LLC",
|
|
||||||
description:
|
|
||||||
"Founded in 1998, Google LLC specializes in internet-related services and products, including search engines, online advertising, cloud computing, and the Android operating system.",
|
|
||||||
joinDate: "April 2019",
|
|
||||||
location: "Mountain View, California, USA",
|
|
||||||
tags: ["Internet", "Search Engine"],
|
|
||||||
imageUri: "/money.png",
|
|
||||||
minInvestment: 10000,
|
|
||||||
totalInvestor: 5000,
|
|
||||||
totalRaised: 1500000000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Microsoft Corporation",
|
|
||||||
description: "Microsoft Corporation is a multinational technology company.",
|
|
||||||
joinDate: "January 2018",
|
|
||||||
location: "California, USA",
|
|
||||||
tags: ["Technology", "Software"],
|
|
||||||
imageUri: null,
|
|
||||||
minInvestment: 250,
|
|
||||||
totalInvestor: 5000,
|
|
||||||
totalRaised: 1500000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredData = selectedTag ? data.filter((item) => item.tags.includes(selectedTag)) : data;
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [searchTermVisual, setSearchTermVisual] = useState("");
|
||||||
|
const [sortByTimeFilter, setSortByTimeFilter] = useState("all");
|
||||||
|
const [businessTypeFilter, setBusinessTypeFilter] = useState("all");
|
||||||
|
const [tagFilter, setTagFilter] = useState([]);
|
||||||
|
const [projectStatusFilter, setprojectStatusFilter] = useState("all");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(4);
|
||||||
|
|
||||||
|
const filterParams: FilterParams = {
|
||||||
|
searchTerm,
|
||||||
|
tagsFilter: tagFilter,
|
||||||
|
projectStatusFilter,
|
||||||
|
businessTypeFilter,
|
||||||
|
sortByTimeFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterProjectQueryParams: FilterProjectQueryParams = {
|
||||||
|
searchTerm,
|
||||||
|
tagsFilter: tagFilter,
|
||||||
|
projectStatusFilter,
|
||||||
|
businessTypeFilter,
|
||||||
|
sortByTimeFilter,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: tags, isLoading: isLoadingTags, error: tagsLoadingError } = useQuery(getAllTagsQuery(supabase));
|
||||||
|
const {
|
||||||
|
data: projectStatus,
|
||||||
|
isLoading: isLoadingFunded,
|
||||||
|
error: fundedLoadingError,
|
||||||
|
} = useQuery(getALlFundedStatusQuery(supabase));
|
||||||
|
const {
|
||||||
|
data: businessType,
|
||||||
|
isLoading: isLoadingBusinessType,
|
||||||
|
error: businessTypeLoadingError,
|
||||||
|
} = useQuery(getAllBusinessTypeQuery(supabase));
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: projects,
|
||||||
|
isLoading: isLoadingProjects,
|
||||||
|
error: projectsLoadingError,
|
||||||
|
} = useQuery(searchProjectsQuery(supabase, filterProjectQueryParams));
|
||||||
|
|
||||||
|
const clearAll = () => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setTagFilter([]);
|
||||||
|
setprojectStatusFilter("all");
|
||||||
|
setBusinessTypeFilter("all");
|
||||||
|
setSortByTimeFilter("all");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container max-w-screen-xl mx-auto px-4">
|
<div className="container max-w-screen-xl mx-auto px-4">
|
||||||
@ -72,58 +172,98 @@ export default function Deals() {
|
|||||||
All companies are <u>vetted & pass due diligence.</u>
|
All companies are <u>vetted & pass due diligence.</u>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* {JSON.stringify(projects, null, 4)} */}
|
||||||
<div className="flex flex-wrap mt-10 gap-3">
|
|
||||||
<Select onValueChange={(value) => setPostAtFilter(value)}>
|
<div className="flex mt-10 gap-3">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search projects"
|
||||||
|
value={searchTermVisual}
|
||||||
|
onChange={(e) => setSearchTermVisual(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setSearchTerm(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Posted At Filter */}
|
||||||
|
<Select onValueChange={(value) => setSortByTimeFilter(value)}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<Clock3Icon className="ml-2" />
|
<Clock3Icon className="ml-2" />
|
||||||
<SelectValue placeholder="Posted at" />
|
<SelectValue placeholder="Posted at" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Time</SelectItem>
|
||||||
<SelectItem value="Today">Today</SelectItem>
|
<SelectItem value="Today">Today</SelectItem>
|
||||||
<SelectItem value="Yesterday">Yesterday</SelectItem>
|
<SelectItem value="This Week">This Week</SelectItem>
|
||||||
|
<SelectItem value="This Month">This Month</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select onValueChange={(value) => setSelectedTag(value)}>
|
{/* Business Type Filter */}
|
||||||
|
<Select onValueChange={(value) => setBusinessTypeFilter}>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
<MessageSquareIcon className="ml-2" />
|
<UsersIcon className="ml-2" />
|
||||||
<SelectValue placeholder="Tags" />
|
<SelectValue placeholder="Business Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="_">All Tags</SelectItem>
|
{isLoadingBusinessType ? (
|
||||||
<SelectItem value="AI">AI</SelectItem>
|
<SelectItem disabled value="_">
|
||||||
<SelectItem value="Technology">Technology</SelectItem>
|
Loading...
|
||||||
<SelectItem value="Consumer Electronics">Consumer Electronics</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="Software">Software</SelectItem>
|
) : businessTypeLoadingError ? (
|
||||||
<SelectItem value="Internet">Internet</SelectItem>
|
<SelectItem disabled value="_">
|
||||||
|
No data available
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectItem value="all">All Types</SelectItem>
|
||||||
|
{businessType &&
|
||||||
|
businessType.map((type) => (
|
||||||
|
<SelectItem key={type.id} value={type.value}>
|
||||||
|
{type.value}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Project Status Filter */}
|
||||||
|
<Select onValueChange={(key) => setprojectStatusFilter(key)}>
|
||||||
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
|
<UserIcon className="ml-2" />
|
||||||
|
<SelectValue placeholder="Project Status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{isLoadingFunded ? (
|
||||||
|
<SelectItem disabled value="_">
|
||||||
|
Loading...
|
||||||
|
</SelectItem>
|
||||||
|
) : fundedLoadingError ? (
|
||||||
|
<SelectItem disabled value="_">
|
||||||
|
No data available
|
||||||
|
</SelectItem>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectItem value="all">All Statuses</SelectItem>
|
||||||
|
{projectStatus &&
|
||||||
|
projectStatus.map((status) => (
|
||||||
|
<SelectItem key={status.id} value={status.value}>
|
||||||
|
{status.value}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<ShowFilter filterParams={filterParams} clearAll={clearAll} />
|
||||||
<Separator className="mt-10" />
|
<Separator className="mt-10" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-10">
|
{/* Project Cards Section */}
|
||||||
<h2 className="text-2xl">Deals</h2>
|
<ProjectSection filteredProjects={projects} />
|
||||||
<p className="mt-3">The deals attracting the most interest right now</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Block for all the deals */}
|
|
||||||
<div className="mt-10 grid grid-cols-3 gap-4">
|
|
||||||
{filteredData.map((item, index) => (
|
|
||||||
<ProjectCard
|
|
||||||
key={index}
|
|
||||||
name={item.name}
|
|
||||||
description={item.description}
|
|
||||||
joinDate={item.joinDate}
|
|
||||||
imageUri={item.imageUri}
|
|
||||||
location={item.location}
|
|
||||||
minInvestment={item.minInvestment}
|
|
||||||
totalInvestor={item.totalInvestor}
|
|
||||||
totalRaised={item.totalRaised}
|
|
||||||
tags={item.tags}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <div>hello</div>;
|
|
||||||
}
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
|
||||||
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
async function getTopProjects(client: SupabaseClient, numberOfRecords: number = 4) {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
@ -45,5 +44,139 @@ async function getTopProjects(client: SupabaseClient, numberOfRecords: number =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getTopProjects };
|
async function searchProjects(client: SupabaseClient, searchTerm: string | null, page: number = 1, pageSize: number = 4) {
|
||||||
|
const start = (page - 1) * pageSize;
|
||||||
|
const end = start + pageSize - 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let query = client.from("Project").select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
projectName,
|
||||||
|
businessId,
|
||||||
|
publishedTime,
|
||||||
|
projectShortDescription,
|
||||||
|
cardImage,
|
||||||
|
ProjectInvestmentDetail (
|
||||||
|
minInvestment,
|
||||||
|
totalInvestment,
|
||||||
|
targetInvestment,
|
||||||
|
investmentDeadline
|
||||||
|
),
|
||||||
|
ItemTag (
|
||||||
|
Tag (
|
||||||
|
id,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Business (
|
||||||
|
location
|
||||||
|
)
|
||||||
|
`
|
||||||
|
).order("publishedTime", { ascending: false })
|
||||||
|
.range(start, end);
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.ilike('projectName', `%${searchTerm}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error searching projects:", error.message);
|
||||||
|
return { data: null, error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Unexpected error:", err);
|
||||||
|
return { data: null, error: "An unexpected error occurred." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterParams {
|
||||||
|
searchTerm?: string;
|
||||||
|
tagsFilter?: string[];
|
||||||
|
projectStatus?: string;
|
||||||
|
projectStatusFilter?: string;
|
||||||
|
businessTypeFilter?: string;
|
||||||
|
sortByTimeFilter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterProjectQueryParams extends FilterParams {
|
||||||
|
page: number,
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchProjectsQuery(client: SupabaseClient, {searchTerm, tagsFilter, projectStatus, businessTypeFilter, sortByTimeFilter, page = 1, pageSize = 4}: FilterProjectQueryParams) {
|
||||||
|
const start = (page - 1) * pageSize;
|
||||||
|
const end = start + pageSize - 1;
|
||||||
|
|
||||||
|
let query = client.from("Project").select(
|
||||||
|
`
|
||||||
|
id,
|
||||||
|
project_name:projectName,
|
||||||
|
published_time:publishedTime,
|
||||||
|
project_short_description:projectShortDescription,
|
||||||
|
card_image_url:cardImage,
|
||||||
|
...ProjectStatus!Project_projectStatusId_fkey!inner (
|
||||||
|
project_status:value
|
||||||
|
),
|
||||||
|
...ProjectInvestmentDetail!inner (
|
||||||
|
min_investment:minInvestment,
|
||||||
|
total_investment:totalInvestment,
|
||||||
|
target_investment:targetInvestment,
|
||||||
|
investment_deadline:investmentDeadline
|
||||||
|
),
|
||||||
|
tags:ItemTag!inner (
|
||||||
|
...Tag!inner (
|
||||||
|
tag_name:value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...Business!inner (
|
||||||
|
...businessType!inner (
|
||||||
|
business_type:value
|
||||||
|
),
|
||||||
|
business_location:location
|
||||||
|
)
|
||||||
|
`
|
||||||
|
).order("publishedTime", { ascending: false }).range(start, end)
|
||||||
|
|
||||||
|
if (sortByTimeFilter === "all") {
|
||||||
|
sortByTimeFilter = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectStatus === "all") {
|
||||||
|
projectStatus = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessTypeFilter === "all") {
|
||||||
|
businessTypeFilter = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagsFilter?.length === 0) {
|
||||||
|
tagsFilter = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.ilike('projectName', `%${searchTerm}%`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagsFilter) {
|
||||||
|
query = query.in('ItemTag.Tag.value', tagsFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectStatus) {
|
||||||
|
query = query.eq("ProjectStatus.value", projectStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessTypeFilter) {
|
||||||
|
query = query.eq("Business.businessType.value", businessTypeFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { getTopProjects, searchProjects, searchProjectsQuery };
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user