diff --git a/apps/api/prisma/migrations/20251010150458_init/migration.sql b/apps/api/prisma/migrations/20251010150458_init/migration.sql new file mode 100644 index 0000000..6ae3afa --- /dev/null +++ b/apps/api/prisma/migrations/20251010150458_init/migration.sql @@ -0,0 +1,46 @@ +-- CreateTable +CREATE TABLE "QueryCount" ( + "id" INTEGER NOT NULL DEFAULT 1, + "total_queries" BIGINT NOT NULL, + + CONSTRAINT "QueryCount_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "authMethod" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastLogin" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/apps/api/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/apps/api/src/routers/projects.ts b/apps/api/src/routers/projects.ts index f3006d9..5eaaaa3 100644 --- a/apps/api/src/routers/projects.ts +++ b/apps/api/src/routers/projects.ts @@ -5,7 +5,7 @@ import { projectService } from "../services/project.service.js"; import { queryService } from "../services/query.service.js"; const filterPropsSchema = z.object({ - language: z.string().optional(), + language: z.array(z.string()).optional(), stars: z .object({ min: z.string().optional(), @@ -40,9 +40,6 @@ export const projectRouter = router({ .input(inputSchema) .query(async ({ input, ctx }: any): Promise => { await queryService.incrementQueryCount(ctx.db.prisma); - return await projectService.fetchGithubProjects( - input.filters as any, - input.options as any - ); + return await projectService.fetchGithubProjects(input.filters as any, input.options as any); }), }); diff --git a/apps/api/src/services/project.service.ts b/apps/api/src/services/project.service.ts index 7ae4d5e..8317f57 100644 --- a/apps/api/src/services/project.service.ts +++ b/apps/api/src/services/project.service.ts @@ -1,11 +1,6 @@ import { graphql } from "@octokit/graphql"; import dotenv from "dotenv"; -import type { - FilterProps, - RepositoryProps, - GraphQLResponseProps, - OptionsTypesProps, -} from "@opensox/shared"; +import type { FilterProps, RepositoryProps, GraphQLResponseProps, OptionsTypesProps } from "@opensox/shared"; dotenv.config(); diff --git a/apps/web/src/components/ui/Filter.tsx b/apps/web/src/components/ui/Filter.tsx index 5c1340b..33e539e 100644 --- a/apps/web/src/components/ui/Filter.tsx +++ b/apps/web/src/components/ui/Filter.tsx @@ -5,63 +5,83 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Label } from "@/components/ui/label"; import { useFilterInputStore } from "@/store/useFilterInputStore"; import clsx from "clsx"; +import { useState } from "react"; +import { Checkbox } from "./checkbox"; export default function Filter({ filterName, filters, onClick, + multiple = false, }: { filterName: string; filters: string[]; onClick?: () => void; + multiple?: boolean; }) { const { updateFilters } = useFilterInputStore(); - const inputData: { [key: string]: string } = {}; + const [selected, setSelected] = useState(multiple ? [] : ""); + const recordFilterInput = (filter: string) => { - inputData[filterName] = filter; - updateFilters(inputData); + let updatedValue: string | string[]; + + if (multiple) { + // Toggle checkbox selections + const current = selected as string[]; + updatedValue = current.includes(filter) ? current.filter((f) => f !== filter) : [...current, filter]; + } else { + // Single choice (radio) + updatedValue = filter; + } + + setSelected(updatedValue); + updateFilters({ [filterName]: updatedValue }); }; const triggerClasses = clsx("text-sm font-medium", { - "text-slate-500": ["Hire contributors", "Funding", "Trending"].includes( - filterName - ), + "text-slate-500": ["Hire contributors", "Funding", "Trending"].includes(filterName), }); return (
- - {filterName} - + {filterName} - - {filters.map((filter) => ( -
- { - recordFilterInput(filter); - }} - /> - -
- ))} -
+ {multiple ? ( + // Multiple-choice (checkbox) +
+ {filters.map((filter) => ( +
+ recordFilterInput(filter)} + /> + +
+ ))} +
+ ) : ( + // Single-choice (radio) + + {filters.map((filter) => ( +
+ recordFilterInput(filter)} /> + +
+ ))} +
+ )}
diff --git a/apps/web/src/components/ui/FiltersContainer.tsx b/apps/web/src/components/ui/FiltersContainer.tsx index 9eb79e3..9e44214 100644 --- a/apps/web/src/components/ui/FiltersContainer.tsx +++ b/apps/web/src/components/ui/FiltersContainer.tsx @@ -8,10 +8,7 @@ import Filter from "./Filter"; import { useFilterStore } from "@/store/useFilterStore"; import { useFilterInputStore } from "@/store/useFilterInputStore"; import { useGetProjects } from "@/hooks/useGetProjects"; -import { - convertUserInputToApiInput, - convertApiOutputToUserOutput, -} from "@/utils/converter"; +import { convertUserInputToApiInput, convertApiOutputToUserOutput } from "@/utils/converter"; import { useRouter } from "next/navigation"; import { useRenderProjects } from "@/store/useRenderProjectsStore"; import { useProjectsData } from "@/store/useProjectsDataStore"; @@ -58,18 +55,12 @@ export default function FiltersContainer() { return (
-
toggleShowFilters()} - /> +
toggleShowFilters()} />

Filters

- toggleShowFilters()} - /> + toggleShowFilters()} />
@@ -95,38 +86,15 @@ export default function FiltersContainer() { "Html", "Elixir", ]} + multiple={true} /> - - - - - - - + + + + + + +
diff --git a/apps/web/src/store/useFilterInputStore.ts b/apps/web/src/store/useFilterInputStore.ts index 927eb80..dbdfd66 100644 --- a/apps/web/src/store/useFilterInputStore.ts +++ b/apps/web/src/store/useFilterInputStore.ts @@ -1,8 +1,8 @@ import { create } from "zustand"; interface FilterInputState { - filters: object; - updateFilters: (newFilter: Record) => void; + filters: Record; + updateFilters: (newFilter: Record) => void; resetFilters: () => void; } diff --git a/apps/web/src/types/filter.ts b/apps/web/src/types/filter.ts index db245af..593f5e2 100644 --- a/apps/web/src/types/filter.ts +++ b/apps/web/src/types/filter.ts @@ -48,7 +48,7 @@ export type ProjectProps = { }; export type UserInputFilterProps = { - "Tech stack"?: string; + "Tech stack"?: string[]; Popularity?: string; Competition?: string; Stage?: string; diff --git a/packages/shared/types/filter.ts b/packages/shared/types/filter.ts index 602d308..a7155d4 100644 --- a/packages/shared/types/filter.ts +++ b/packages/shared/types/filter.ts @@ -1,23 +1,23 @@ export type DateRange = { - start: string; - end?: string; -} + start: string; + end?: string; +}; export type StarRange = { - min?: string; - max?: string; - custom?: string; -} + min?: string; + max?: string; + custom?: string; +}; export type ForkRange = { - min?: string; - max?: string; -} + min?: string; + max?: string; +}; export type FilterProps = { - language?: string; - stars?: StarRange; - forks?: ForkRange; - pushed?: string; - created?: string; -} \ No newline at end of file + language?: string[]; + stars?: StarRange; + forks?: ForkRange; + pushed?: string; + created?: string; +};