diff --git a/apps/web/src/components/ui/Filter.tsx b/apps/web/src/components/ui/Filter.tsx index b091b813..0df55016 100644 --- a/apps/web/src/components/ui/Filter.tsx +++ b/apps/web/src/components/ui/Filter.tsx @@ -1,7 +1,7 @@ "use client"; import { AccordionContent, AccordionItem, AccordionTrigger } from "./accordion"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { useFilterInputStore } from "@/store/useFilterInputStore"; import clsx from "clsx"; @@ -15,12 +15,8 @@ export default function Filter({ filters: string[]; onClick?: () => void; }) { - const { updateFilters } = useFilterInputStore(); - const inputData: { [key: string]: string } = {}; - const recordFilterInput = (filter: string) => { - inputData[filterName] = filter; - updateFilters(inputData); - }; + const { filters: selectedFilters, toggleFilter } = useFilterInputStore(); + const selectedValues = selectedFilters[filterName] || []; const triggerClasses = clsx("text-sm font-medium", { "text-slate-300": ["Hire contributors", "Funding", "Trending"].includes( @@ -35,25 +31,24 @@ export default function Filter({ {filterName} - +
{filters.map((filter) => (
- recordFilterInput(filter)} - className="border-[#28282c] bg-[#141418] text-ox-purple transition data-[state=checked]:border-ox-purple data-[state=checked]:bg-ox-purple/20 data-[state=checked]:ring-2 data-[state=checked]:ring-ox-purple/50" + toggleFilter(filterName, filter)} + className="border-[#28282c] bg-[#141418] transition data-[state=checked]:border-ox-purple data-[state=checked]:bg-ox-purple data-[state=checked]:text-white" />
))} - +
diff --git a/apps/web/src/store/useFilterInputStore.ts b/apps/web/src/store/useFilterInputStore.ts index 927eb80e..62414715 100644 --- a/apps/web/src/store/useFilterInputStore.ts +++ b/apps/web/src/store/useFilterInputStore.ts @@ -1,16 +1,22 @@ import { create } from "zustand"; interface FilterInputState { - filters: object; - updateFilters: (newFilter: Record) => void; + filters: Record; + toggleFilter: (filterName: string, value: string) => void; resetFilters: () => void; } export const useFilterInputStore = create((set) => ({ filters: {}, - updateFilters: (newFilter) => - set((state) => ({ - filters: { ...state.filters, ...newFilter }, - })), + toggleFilter: (filterName, value) => + set((state) => { + const currentValues = state.filters[filterName] || []; + const newValues = currentValues.includes(value) + ? currentValues.filter((v) => v !== value) + : [...currentValues, value]; + return { + filters: { ...state.filters, [filterName]: newValues }, + }; + }), resetFilters: () => set({ filters: {} }), })); diff --git a/apps/web/src/types/filter.ts b/apps/web/src/types/filter.ts index db245af3..f00bb2c6 100644 --- a/apps/web/src/types/filter.ts +++ b/apps/web/src/types/filter.ts @@ -48,9 +48,9 @@ export type ProjectProps = { }; export type UserInputFilterProps = { - "Tech stack"?: string; - Popularity?: string; - Competition?: string; - Stage?: string; - Activity?: string; + "Tech stack"?: string[]; + Popularity?: string[]; + Competition?: string[]; + Stage?: string[]; + Activity?: string[]; }; diff --git a/apps/web/src/utils/converter.ts b/apps/web/src/utils/converter.ts index 53a5c793..50ec37fa 100644 --- a/apps/web/src/utils/converter.ts +++ b/apps/web/src/utils/converter.ts @@ -111,27 +111,62 @@ export const convertUserInputToApiInput = ( ): FilterProps => { const data: Partial = {}; - if (filter["Tech stack"]) { - data.language = filter["Tech stack"]; + // Handle multiple tech stacks - join with comma for API + if (filter["Tech stack"] && filter["Tech stack"].length > 0) { + data.language = filter["Tech stack"].join(","); } - if (filter.Popularity) { - data.stars = - userInputValues.Popularity[filter.Popularity as keyof PopularityProps]; + // Handle multiple popularity values - merge ranges (min of mins, max of maxes) + if (filter.Popularity && filter.Popularity.length > 0) { + const ranges = filter.Popularity.map( + (p) => userInputValues.Popularity[p as keyof PopularityProps] + ).filter(Boolean); + if (ranges.length > 0) { + const mins = ranges.map((r) => parseInt(r.min || "0", 10)); + const maxes = ranges.map((r) => (r.max ? parseInt(r.max, 10) : Infinity)); + data.stars = { + min: String(Math.min(...mins)), + ...(Math.max(...maxes) !== Infinity && { max: String(Math.max(...maxes)) }), + }; + } } - if (filter.Competition) { - data.forks = - userInputValues.Competition[filter.Competition as keyof CompetitionProps]; + // Handle multiple competition values - merge ranges + if (filter.Competition && filter.Competition.length > 0) { + const ranges = filter.Competition.map( + (c) => userInputValues.Competition[c as keyof CompetitionProps] + ).filter(Boolean); + if (ranges.length > 0) { + const mins = ranges.map((r) => parseInt(r.min || "0", 10)); + const maxes = ranges.map((r) => (r.max ? parseInt(r.max, 10) : Infinity)); + data.forks = { + min: String(Math.min(...mins)), + ...(Math.max(...maxes) !== Infinity && { max: String(Math.max(...maxes)) }), + }; + } } - if (filter.Activity) { - data.pushed = - userInputValues.Activity[filter.Activity as keyof ActivityProps]; + // Handle multiple activity values - use the most recent date (broadest range) + if (filter.Activity && filter.Activity.length > 0) { + const dates = filter.Activity.map( + (a) => userInputValues.Activity[a as keyof ActivityProps] + ).filter(Boolean); + if (dates.length > 0) { + // Find the oldest date (most inclusive) + const oldestDate = dates.sort()[0]; + data.pushed = oldestDate; + } } - if (filter.Stage) { - data.created = userInputValues.Stage[filter.Stage as keyof StageProps]; + // Handle multiple stage values - use the oldest date (most inclusive) + if (filter.Stage && filter.Stage.length > 0) { + const dates = filter.Stage.map( + (s) => userInputValues.Stage[s as keyof StageProps] + ).filter(Boolean); + if (dates.length > 0) { + const oldestDate = dates.sort()[0]; + data.created = oldestDate; + } } return data as FilterProps; @@ -149,10 +184,10 @@ export const convertApiOutputToUserOutput = ( avatarUrl: item.owner.avatarUrl, totalIssueCount: item.issues.totalCount, primaryLanguage: item.primaryLanguage?.name || "Other", - popularity: filters.Popularity ? filters.Popularity : "-", - stage: filters.Stage ? filters.Stage : "-", - competition: filters.Competition ? filters.Competition : "-", - activity: filters.Activity ? filters.Activity : "-", + popularity: filters.Popularity?.join(", ") || "-", + stage: filters.Stage?.join(", ") || "-", + competition: filters.Competition?.join(", ") || "-", + activity: filters.Activity?.join(", ") || "-", })); return data; };