Skip to content
Draft
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
);
}),
});
10 changes: 8 additions & 2 deletions apps/api/src/services/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ export const projectService = {
*/
async fetchGithubProjects(
filters: Partial<FilterProps> = {},
options: Partial<OptionsTypesProps> = {}
options: Partial<OptionsTypesProps> = {},
search?: string
): Promise<RepositoryProps[]> {
const queryParts: string[] = [];

if (search) {
queryParts.push(`${search} in:name,description`);
}

if (filters.language) {
queryParts.push(`language:${filters.language}`);
}
Expand All @@ -53,6 +58,7 @@ export const projectService = {
queryParts.push(`fork:true`);

const searchQueryString = queryParts.join(" ");
// console.log(searchQueryString);

const response: GraphQLResponseProps = await graphqlWithAuth(
`
Expand Down Expand Up @@ -88,7 +94,7 @@ export const projectService = {
first: options.per_page || 100,
}
);

// console.log(response.search.nodes);
return response.search.nodes;
},
};
24 changes: 18 additions & 6 deletions apps/web/src/components/dashboard/DashboardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={`min-h-[calc(100vh-64px)] ${isProjectsPage ? "flex items-center justify-center" : ""}`}>
<div className={`w-full ${!loading ? "h-full" : ""}`}>
<div className="min-h-[calc(100vh-64px)]">
<div className="w-full h-full">
{isProjectsPage && (
<div className="px-8 pt-6">
<ProjectsSearchController />
</div>
)}

{renderProjects && !loading && (
<ProjectsContainer projects={data}></ProjectsContainer>
<div className={isProjectsPage ? "px-8 pt-4" : ""}>
<ProjectsContainer projects={data}></ProjectsContainer>
</div>
)}

{loading && (
<div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<SpinnerElm text={"loading cool projects for you..."}></SpinnerElm>
</div>
)}

{projectsNotFound && !loading && (
<div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<ErrMsg
text={
"No projects were found matching the selected filters. Please adjust the filters and try again."
Expand Down
67 changes: 67 additions & 0 deletions apps/web/src/components/dashboard/ProjectSearchController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client";

import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { convertApiOutputToUserOutput } from "@/utils/converter";
import { useProjectsData } from "@/store/useProjectsDataStore";
import { useLoading } from "@/store/useLoadingStore";
import { useProjectsNotFoundStore } from "@/store/useProjectsFoundStore";
import { useGetProjects } from "@/hooks/useGetProjects";

export default function ProjectsSearchController() {
const { setData } = useProjectsData();
const { setLoading } = useLoading();
const { setProjectsNotFound } = useProjectsNotFoundStore();
const getProjects = useGetProjects();

const [input, setInput] = useState("");

useEffect(() => {
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 (
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-5 text-gray-400" />
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Search open source projects…"
className="pl-10"
/>
</div>
);
}
17 changes: 13 additions & 4 deletions apps/web/src/components/dashboard/ProjectsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] };

Expand All @@ -38,8 +40,13 @@ const languageColors: Record<string, string> = {
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",
Expand Down Expand Up @@ -119,7 +126,7 @@ export default function ProjectsContainer({
<div className="flex items-center gap-2">
<div className="rounded-full overflow-hidden inline-block h-4 w-4 sm:h-6 sm:w-6 border">
<Image
src={p.avatarUrl}
src={p.avatarUrl || "/placeholder.svg"}
className="w-full h-full object-cover"
alt={p.name}
width={24}
Expand All @@ -141,7 +148,9 @@ export default function ProjectsContainer({
variant="secondary"
className={`${getColor(p.primaryLanguage)} text-[10px] sm:text-xs whitespace-nowrap`}
>
{p.primaryLanguage}
{typeof p.primaryLanguage === "string"
? p.primaryLanguage
: p.primaryLanguage?.name}
</Badge>
</TableCell>

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ui/FiltersContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/hooks/useGetProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RepositoryProps[]> => {
async ({
search,
filters = {},
}: GetProjectsInput): Promise<RepositoryProps[]> => {
const data = await (utils.client.project.getGithubProjects as any).query({
search,
filters: filters as any,
options: {
sort: "stars" as const,
Expand Down