diff --git a/apps/api/package.json b/apps/api/package.json index 0c6722ac..f2852ab3 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -8,7 +8,7 @@ "test": "echo \"Error: no test specified\" && exit 1", "dev": "tsx src/index.ts", "build": "prisma generate && tsc", - "postinstall": "[ -f prisma/schema.prisma ] && prisma generate || true" + "postinstall": "prisma generate" }, "keywords": [], "author": "Ajeet Pratpa Singh", diff --git a/apps/api/src/routers/projects.ts b/apps/api/src/routers/projects.ts index f3006d90..efeb4fe6 100644 --- a/apps/api/src/routers/projects.ts +++ b/apps/api/src/routers/projects.ts @@ -33,6 +33,7 @@ const optionsSchema = z.object({ const inputSchema = z.object({ filters: filterPropsSchema.optional(), options: optionsSchema.optional(), + search: z.string().optional(), }); export const projectRouter = router({ @@ -42,7 +43,8 @@ export const projectRouter = router({ await queryService.incrementQueryCount(ctx.db.prisma); return await projectService.fetchGithubProjects( input.filters as any, - input.options as any + input.options as any, + input.search ); }), }); diff --git a/apps/api/src/services/project.service.ts b/apps/api/src/services/project.service.ts index 7ae4d5e9..dcef0f6e 100644 --- a/apps/api/src/services/project.service.ts +++ b/apps/api/src/services/project.service.ts @@ -23,10 +23,15 @@ export const projectService = { */ async fetchGithubProjects( filters: Partial = {}, - options: Partial = {} + options: Partial = {}, + search?: string ): Promise { const queryParts: string[] = []; + if (search) { + queryParts.push(`${search} in:name,description`); + } + if (filters.language) { queryParts.push(`language:${filters.language}`); } @@ -53,6 +58,7 @@ export const projectService = { queryParts.push(`fork:true`); const searchQueryString = queryParts.join(" "); + // console.log(searchQueryString); const response: GraphQLResponseProps = await graphqlWithAuth( ` @@ -88,7 +94,7 @@ export const projectService = { first: options.per_page || 100, } ); - + // console.log(response.search.nodes); return response.search.nodes; }, }; diff --git a/apps/web/src/components/dashboard/DashboardContainer.tsx b/apps/web/src/components/dashboard/DashboardContainer.tsx index e5274e88..f09a9ad2 100644 --- a/apps/web/src/components/dashboard/DashboardContainer.tsx +++ b/apps/web/src/components/dashboard/DashboardContainer.tsx @@ -8,29 +8,41 @@ import { useProjectsNotFoundStore } from "@/store/useProjectsFoundStore"; import { ErrMsg } from "../ui/ErrMsg"; import SpinnerElm from "../ui/SpinnerElm"; import { usePathname } from "next/navigation"; +import ProjectsSearchController from "./ProjectSearchController"; export default function DashboardContainer() { const { renderProjects } = useRenderProjects(); const { data } = useProjectsData(); + const { loading } = useLoading(); const { projectsNotFound } = useProjectsNotFoundStore(); const pathname = usePathname(); - + const isProjectsPage = pathname === "/dashboard/projects"; return ( -
-
+
+
+ {isProjectsPage && ( +
+ +
+ )} + {renderProjects && !loading && ( - +
+ +
)} + {loading && ( -
+
)} + {projectsNotFound && !loading && ( -
+
{ + if (input.length === 0) { + // Reset to default state when input is cleared + setProjectsNotFound(false); + + setData([]); + return; + } + + if (input.length < 2) return; + + const t = setTimeout(async () => { + try { + setLoading(true); + + const res = await getProjects({ search: input }); + const modified = convertApiOutputToUserOutput(res, {}); + + if (!res || res.length === 0) { + setProjectsNotFound(true); + setData([]); + } else { + setProjectsNotFound(false); + setData(modified); + } + } catch (error) { + console.error("Search failed:", error); + setProjectsNotFound(true); + setData([]); + } finally { + setLoading(false); + } + }, 600); + + return () => clearTimeout(t); + }, [input, setData, setLoading, setProjectsNotFound, getProjects]); + return ( +
+ + setInput(e.target.value)} + placeholder="Search open source projects…" + className="pl-10" + /> +
+ ); +} diff --git a/apps/web/src/components/dashboard/ProjectsContainer.tsx b/apps/web/src/components/dashboard/ProjectsContainer.tsx index 80e380ac..138ced31 100644 --- a/apps/web/src/components/dashboard/ProjectsContainer.tsx +++ b/apps/web/src/components/dashboard/ProjectsContainer.tsx @@ -16,6 +16,8 @@ import Image from "next/image"; import { useFilterStore } from "@/store/useFilterStore"; import { usePathname } from "next/navigation"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { Input } from "@/components/ui/input"; +import { useState, useMemo, useEffect } from "react"; type ProjectsContainerProps = { projects: DashboardProjectsProps[] }; @@ -38,8 +40,13 @@ const languageColors: Record = { elixir: "bg-purple-600/15 text-purple-600", }; -const getColor = (c?: string) => - languageColors[(c || "").toLowerCase()] || "bg-gray-200/10 text-gray-300"; +const getColor = (c?: any) => { + const lang = typeof c === "string" ? c : c?.name; + + return ( + languageColors[(lang || "").toLowerCase()] || "bg-gray-200/10 text-gray-300" + ); +}; const tableColumns = [ "Project", @@ -119,7 +126,7 @@ export default function ProjectsContainer({
{p.name} - {p.primaryLanguage} + {typeof p.primaryLanguage === "string" + ? p.primaryLanguage + : p.primaryLanguage?.name} diff --git a/apps/web/src/components/ui/FiltersContainer.tsx b/apps/web/src/components/ui/FiltersContainer.tsx index 84c92feb..314d78e6 100644 --- a/apps/web/src/components/ui/FiltersContainer.tsx +++ b/apps/web/src/components/ui/FiltersContainer.tsx @@ -40,7 +40,7 @@ export default function FiltersContainer() { setLoading(true); router.push("/dashboard/projects"); const modifiedFilters = convertUserInputToApiInput(filters); - const response = await getProjects(modifiedFilters); + const response = await getProjects( {filters: modifiedFilters}); const projects = response; if (!projects) { setProjectsNotFound(true); diff --git a/apps/web/src/hooks/useGetProjects.ts b/apps/web/src/hooks/useGetProjects.ts index 70a37c41..d926ec11 100644 --- a/apps/web/src/hooks/useGetProjects.ts +++ b/apps/web/src/hooks/useGetProjects.ts @@ -2,12 +2,21 @@ import { useCallback } from "react"; import { FilterProps, RepositoryProps } from "@opensox/shared/types"; import { trpc } from "@/lib/trpc"; +type GetProjectsInput = { + search?: string; + filters?: FilterProps; +}; + export const useGetProjects = () => { const utils = trpc.useUtils(); const func = useCallback( - async (filters: FilterProps): Promise => { + async ({ + search, + filters = {}, + }: GetProjectsInput): Promise => { const data = await (utils.client.project.getGithubProjects as any).query({ + search, filters: filters as any, options: { sort: "stars" as const,