Skip to content

implemented company filtering #195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 10, 2025
Merged
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
65 changes: 56 additions & 9 deletions apps/web/src/app/(pages)/(dashboard)/companies/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,73 @@
"use client";

import { useRouter, useSearchParams } from "next/navigation";
import { IndustryType } from "@cooper/db/schema";

Check warning on line 3 in apps/web/src/app/(pages)/(dashboard)/companies/page.tsx

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`
import { useRouter } from "next/navigation";

import { CompanyCardPreview } from "~/app/_components/companies/company-card-preview";
import LoadingResults from "~/app/_components/loading-results";
import NoResults from "~/app/_components/no-results";
import SearchFilter from "~/app/_components/search/search-filter";
import { api } from "~/trpc/react";

export default function Companies() {
const searchParams = useSearchParams();
const searchValue = searchParams.get("search") ?? ""; // Get search query from URL

export default function Companies({
searchParams,
}: {
searchParams?: {
industry?: IndustryType;
location?: string;
search?: string;
};
}) {
const companies = api.company.list.useQuery({
search: searchValue,
options: {
industry: searchParams?.industry,
location: searchParams?.location,
},
search: searchParams?.search,
});

const locationQuery = api.location.getById.useQuery(
{ id: searchParams?.location ?? "" },
{ enabled: !!searchParams?.location },
);

const router = useRouter();

return (
<>
<div className="w-[95%] justify-center">
<SearchFilter
searchType="COMPANIES"
industry={searchParams?.industry}
location={locationQuery.data}
/>
<hr className="my-4 border-t border-[#9A9A9A] w-full" />
<div className="text-[26px]">
{searchParams?.industry ? (
<>
<span className="font-bold">
{searchParams.industry.charAt(0) +
searchParams.industry.slice(1).toLowerCase()}
</span>{" "}
Companies
</>
) : (
"Companies"
)}
{locationQuery.data && (
<>
{" in "}
<span className="font-bold">
{locationQuery.data.city}
{locationQuery.data.state ? `, ${locationQuery.data.state}` : ""}
</span>
</>
)}
</div>
<div className="text-cooper-gray-400">
{companies.data?.length ?? 0} results
</div>
{companies.isSuccess && companies.data.length > 0 ? (
<div className="mb-8 mt-6 grid h-[86dvh] w-3/4 grid-cols-1 gap-4 overflow-y-scroll md:grid-cols-2 xl:grid-cols-3">
<div className="mb-8 mt-6 grid h-[86dvh] grid-cols-1 gap-4 overflow-y-auto md:grid-cols-2 xl:grid-cols-3 ">
{companies.data.map((company) => (
<div
key={company.id}
Expand All @@ -36,6 +83,6 @@
) : companies.isPending ? (
<LoadingResults className="h-full" />
) : null}
</>
</div>
);
}
16 changes: 11 additions & 5 deletions apps/web/src/app/_components/combo-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface ComboBoxProps {
onSelect: (option: string) => void;
triggerClassName?: string;
onChange?: (value: string) => void;
variant?: "default" | "form";
variant?: "default" | "form" | "filtering";
}

/**
Expand All @@ -51,15 +51,18 @@ export default function ComboBox({
const styleVariant =
variant === "form"
? "flex h-16 w-full rounded-md border-[3px] border-cooper-blue-600 bg-white px-3 py-2 text-xl font-normal ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
: "";
: variant === "filtering"
? "w-[21rem] h-12 rounded-none border-[0.75px] border-l-0 border-t-0 border-cooper-gray-400 text-lg placeholder:opacity-50 focus:ring-0 active:ring-0 lg:rounded-md lg:border-[0.75px] py-0"
: "h-8 py-0";

return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger
asChild
className={cn(
"w-[400px] overflow-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"overflow-hidden file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
triggerClassName,
variant !== "filtering" ? "w-[400px]" : "",
)}
>
<Button
Expand All @@ -68,10 +71,13 @@ export default function ComboBox({
aria-expanded={isOpen}
className={cn(
styleVariant,
"w-[400px] justify-between overflow-hidden text-ellipsis text-nowrap",
"justify-between overflow-hidden text-ellipsis text-nowrap h-12 py-0 min-h-0",
variant !== "filtering" ? "w-[400px]" : "",
)}
>
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
<span
className={`overflow-hidden text-lg whitespace-nowrap ${defaultLabel === "Location" ? "text-cooper-gray-400" : "text-gray font-normal"}`}
>
{defaultLabel}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/_components/header-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export default async function HeaderLayout({
return (
<div className="flex h-screen flex-col justify-between">
<Header auth={button} />
<article className="flex h-screen w-screen flex-col items-center justify-start">
<article className="flex h-screen flex-col items-center justify-start">
<div className="mx-0 mt-2 flex h-[6dvh] justify-center xl:mt-0 xl:hidden">
<SearchFilter searchClassName="w-screen px-4 mb-2" />
<SearchFilter searchClassName="px-4 mb-2" />
</div>
{children}
</article>
Expand Down
188 changes: 188 additions & 0 deletions apps/web/src/app/_components/search/company-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";

import { Button } from "@cooper/ui/button";
import { FormControl, FormField, FormItem } from "@cooper/ui/form";
import { useCallback } from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@cooper/ui/select";
import { Industry } from "@cooper/db/schema";
import type { IndustryType, LocationType } from "@cooper/db/schema";
import { api } from "~/trpc/react";
import { usePathname, useRouter } from "next/navigation";
import { z } from "zod";
import ComboBox from "../combo-box";

interface SearchBarProps {
industry?: IndustryType;
location?: LocationType;
}

const _formSchema = z.object({
searchIndustry: z.string().optional(),
searchLocation: z.string().optional(),
});
type FormSchema = z.infer<typeof _formSchema>;

export function CompanySearchBar({ industry, location }: SearchBarProps) {
const form = useFormContext();
const { handleSubmit, setValue } = form;
const router = useRouter();
const pathName = usePathname();

const [selectedIndustry, setSelectedIndustry] = useState<string | undefined>(
industry,
);

const [locationLabel, setLocationLabel] = useState<string>(
location ? `${location.city}, ${location.state}` : "Location",
);
const [searchTerm, setSearchTerm] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");

useEffect(() => {
const newPrefix =
searchTerm.length === 3 ? searchTerm.slice(0, 3).toLowerCase() : null;
if (newPrefix && newPrefix !== prefix) {
setPrefix(newPrefix);
}
}, [prefix, searchTerm]);
const locationsToUpdate = api.location.getByPrefix.useQuery(
{ prefix },
{ enabled: searchTerm.length === 3 },
);

const locationValuesAndLabels = locationsToUpdate.data
? locationsToUpdate.data.map((location) => {
return {
value: location.id,
label:
location.city +
(location.state ? `, ${location.state}` : "") +
", " +
location.country,
};
})
: [];

const onSubmit = (data: FormSchema) => {
router.push(pathName + "?" + createQueryString(data));
};

const createQueryString = useCallback(
({ searchIndustry, searchLocation }: FormSchema) => {
const params = new URLSearchParams();
if (searchIndustry) {
params.set("industry", searchIndustry);
}
if (searchLocation) {
params.set("location", searchLocation);
}

return params.toString();
},
[],
);

return (
<div className="flex flex-row w-full justify-between items-center pt-4 ">
<div className="text-[30px] justify-left">Browse Companies</div>
<div className="flex flex-row gap-6 min-w-0 items-center">
<FormField
control={form.control}
name="searchIndustry"
render={({ field }) => (
<FormItem className="col-span-5 lg:col-span-2">
<FormControl>
<Select
onValueChange={(value) => {
setSelectedIndustry(value);
const finalValue = value === "INDUSTRY" ? undefined : value;
setValue(field.name, finalValue);
void handleSubmit(onSubmit)();
}}
value={selectedIndustry}
>
<SelectTrigger
className={`h-12 w-[21rem] ${selectedIndustry === "INDUSTRY" ? "text-cooper-gray-400" : "text-gray font-normal "} rounded-none border-[0.75px] border-l-0 focus:ring-offset-0 border-t-0 border-cooper-gray-400 text-lg placeholder:opacity-50 focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:outline-none focus-visible:ring-offset-0 lg:rounded-md lg:border-[0.75px]`}
>
<span
className={`overflow-hidden text-lg whitespace-nowrap ${selectedIndustry === "INDUSTRY" ? "text-cooper-gray-400" : "text-gray"}`}
>
<SelectValue placeholder="Industry" />
</span>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem className="font-bold" value="INDUSTRY">
Industry
</SelectItem>
<SelectSeparator />
{Object.entries(Industry).map(([key, value]) => (
<SelectItem key={value} value={value}>
{key.charAt(0) + key.slice(1).toLowerCase()}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="searchLocation"
render={({ field }) => (
<FormItem className="col-span-5 lg:col-span-2">
<FormControl>
<ComboBox
{...field}
variant="filtering"
defaultLabel={locationLabel || "Location"}
searchPlaceholder="Type to begin..."
searchEmpty="No location found."
valuesAndLabels={locationValuesAndLabels}
currLabel={locationLabel}
onChange={(value) => {
setSearchTerm(value);
}}
onSelect={(currentValue) => {
setLocationLabel(currentValue);
const selectedLoc = locationsToUpdate.data?.find(
(loc) =>
`${loc.city}${loc.state ? `, ${loc.state}` : ""}${loc.country ? `, ${loc.country}` : ""}` ===
currentValue,
);
const finalValue =
currentValue === "LOCATION" ? undefined : selectedLoc?.id;
setValue(field.name, finalValue);
void handleSubmit(onSubmit)();
}}
/>
</FormControl>
</FormItem>
)}
/>
<Button
className="bg-white hover:bg-white hover:text-[#9A9A9A] border-white text-cooper-gray-400 p-0"
onClick={() => {
setSelectedIndustry("INDUSTRY");
setLocationLabel("");
form.setValue("searchIndustry", undefined);
form.setValue("searchLocation", undefined);
}}
>
Clear
</Button>
</div>
</div>
);
}
Loading