From e8a303d93f4ffdde672ce4b525d75773f9a29b2b Mon Sep 17 00:00:00 2001 From: Rishikesh Kanabar <63854326+RishikeshNK@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:08:11 -0500 Subject: [PATCH 1/5] Cooper Testing is CURSED (#94) * chore: add a mock test * chore: fix web app linting * chore: add mock and turbo * chore: add mocks * chore: test passing locally fingers crossed * chore: disable check * chore: ignore unbound methods * chore: add another test * chore: add two more tests * chore: remove unused import --- .../app/(pages)/(dashboard)/roles/page.tsx | 12 ++- packages/api/package.json | 1 + packages/api/tests/mocks/review.ts | 5 + packages/api/tests/review.test.ts | 102 ++++++++++++++++++ packages/api/tsconfig.json | 2 +- turbo.json | 4 + 6 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 packages/api/tests/mocks/review.ts create mode 100644 packages/api/tests/review.test.ts diff --git a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx index 59a827b..b908bcc 100644 --- a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx @@ -3,13 +3,12 @@ import { useEffect, useState } from "react"; import { z } from "zod"; -import { +import type { ReviewType, - WorkEnvironment, WorkEnvironmentType, - WorkTerm, WorkTermType, } from "@cooper/db/schema"; +import { WorkEnvironment, WorkTerm } from "@cooper/db/schema"; import { cn } from "@cooper/ui"; import { useToast } from "@cooper/ui/hooks/use-toast"; @@ -60,7 +59,12 @@ export default function Roles({ variant: "destructive", }); } - }, [toast, mounted]); + }, [ + toast, + mounted, + validationResult.success, + validationResult.error?.issues, + ]); const reviews = api.review.list.useQuery({ options: validationResult.success ? validationResult.data : {}, diff --git a/packages/api/package.json b/packages/api/package.json index dfca51e..e5d57ef 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -13,6 +13,7 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", + "test": "vitest", "clean": "rm -rf .turbo dist node_modules", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint", diff --git a/packages/api/tests/mocks/review.ts b/packages/api/tests/mocks/review.ts new file mode 100644 index 0000000..4cdce65 --- /dev/null +++ b/packages/api/tests/mocks/review.ts @@ -0,0 +1,5 @@ +export const data = [ + { id: "1", workTerm: "SPRING", workEnvironment: "REMOTE" }, + { id: "2", workTerm: "FALL", workEnvironment: "INPERSON" }, + { id: "3", workTerm: "SUMMER", workEnvironment: "HYBRID" }, +]; diff --git a/packages/api/tests/review.test.ts b/packages/api/tests/review.test.ts new file mode 100644 index 0000000..6016eb5 --- /dev/null +++ b/packages/api/tests/review.test.ts @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/unbound-method */ +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import type { Session } from "@cooper/auth"; +import type { ReviewType } from "@cooper/db/schema"; +import { and, eq } from "@cooper/db"; +import { db } from "@cooper/db/client"; +import { Review } from "@cooper/db/schema"; + +import { appRouter } from "../src/root"; +import { createCallerFactory, createTRPCContext } from "../src/trpc"; +import { data } from "./mocks/review"; + +vi.mock("@cooper/db/client", () => ({ + db: { + query: { + Review: { + findMany: vi.fn(), + }, + }, + }, +})); + +vi.mock("@cooper/auth", () => ({ + auth: vi.fn(), +})); + +describe("Review Router", async () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.mocked(db.query.Review.findMany).mockResolvedValue(data as ReviewType[]); + }); + + const session: Session = { + user: { + id: "1", + }, + expires: "1", + }; + + const ctx = await createTRPCContext({ + session, + headers: new Headers(), + }); + + const caller = createCallerFactory(appRouter)(ctx); + + test("list endpoint returns all reviews", async () => { + const reviews = await caller.review.list({}); + + expect(reviews).toEqual(data); + + expect(db.query.Review.findMany).toHaveBeenCalledWith({ + orderBy: expect.anything(), + where: undefined, + }); + }); + + test("list endpoint with cycle filter", async () => { + await caller.review.list({ + options: { + cycle: "SPRING", + }, + }); + + expect(db.query.Review.findMany).toHaveBeenCalledWith({ + orderBy: expect.anything(), + where: and(eq(Review.workTerm, "SPRING")), + }); + }); + + test("list endpoint with term filter", async () => { + await caller.review.list({ + options: { + term: "REMOTE", + }, + }); + + expect(db.query.Review.findMany).toHaveBeenCalledWith({ + orderBy: expect.anything(), + where: and(eq(Review.workEnvironment, "REMOTE")), + }); + }); + + test("list endpoint with cycle and term filter", async () => { + await caller.review.list({ + options: { + cycle: "SPRING", + term: "REMOTE", + }, + }); + + expect(db.query.Review.findMany).toHaveBeenCalledWith({ + orderBy: expect.anything(), + where: and( + eq(Review.workTerm, "SPRING"), + eq(Review.workEnvironment, "REMOTE"), + ), + }); + }); +}); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index c97abb4..15703de 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -4,6 +4,6 @@ "outDir": "dist", "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, - "include": ["src"], + "include": ["src", "tests"], "exclude": ["node_modules"] } diff --git a/turbo.json b/turbo.json index 7d054a4..7b739a4 100644 --- a/turbo.json +++ b/turbo.json @@ -60,6 +60,10 @@ "start": { "cache": false, "interactive": true + }, + "test": { + "cache": true, + "interactive": true } }, "globalEnv": [ From 65750da04a6e1e722d697e21ac81554b329478ee Mon Sep 17 00:00:00 2001 From: Xiaole Su <138140316+suxls@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:33:50 -0500 Subject: [PATCH 2/5] Implement fuse (#91) * initial commit with comments * installed fuse.js * YIPPEE * h * feat: add fuzzy searching to reviews * fix: add fuzzy search integration on roles page * fix: return early if no search * fix: remove unnecessary check * feat: set search bar contents to search param * fixed bug with updated selected view implemented with useEffect, looking for alternative solution * functional fuse * remove unnecessary params * reformatted --------- Co-authored-by: Rishikesh Kanabar --- apps/web/package.json | 1 + .../src/app/(pages)/(dashboard)/roles/page.tsx | 10 +++++++++- .../app/_components/search/search-filter.tsx | 13 +++++++++++-- packages/api/src/router/review.ts | 18 ++++++++++++++++-- pnpm-lock.yaml | 13 +++++++------ 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 7453554..4404c5e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,6 +25,7 @@ "@trpc/react-query": "11.0.0-rc.441", "@trpc/server": "11.0.0-rc.441", "dayjs": "^1.11.13", + "fuse.js": "^7.0.0", "geist": "^1.3.0", "lucide-react": "^0.436.0", "next": "^14.2.4", diff --git a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx index b908bcc..b798648 100644 --- a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx @@ -21,6 +21,7 @@ export default function Roles({ searchParams, }: { searchParams?: { + search?: string; cycle?: WorkTermType; term?: WorkEnvironmentType; }; @@ -67,6 +68,7 @@ export default function Roles({ ]); const reviews = api.review.list.useQuery({ + search: searchParams?.search, options: validationResult.success ? validationResult.data : {}, }); @@ -74,9 +76,15 @@ export default function Roles({ reviews.data ? reviews.data[0] : undefined, ); + useEffect(() => { + if (reviews.data) { + setSelectedReview(reviews.data[0]); + } + }, [reviews.data]); + return ( <> - + {reviews.data && (
diff --git a/apps/web/src/app/_components/search/search-filter.tsx b/apps/web/src/app/_components/search/search-filter.tsx index fe32850..e0376fd 100644 --- a/apps/web/src/app/_components/search/search-filter.tsx +++ b/apps/web/src/app/_components/search/search-filter.tsx @@ -16,11 +16,20 @@ const formSchema = z.object({ export type SearchFilterFormType = typeof formSchema; -export default function SearchFilter() { +interface SearchFilterProps { + search?: string; +} + +/** + * Handles searching logic, updates the search param base on user search and passes the text to backend with fuzzy searching. + * @param param0 user input text that's passed to the fuzzy search + * @returns the search bar with the user inputted text + */ +export default function SearchFilter({ search }: SearchFilterProps) { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - searchText: "", + searchText: search ?? "", }, }); diff --git a/packages/api/src/router/review.ts b/packages/api/src/router/review.ts index 0cec664..217097a 100644 --- a/packages/api/src/router/review.ts +++ b/packages/api/src/router/review.ts @@ -1,4 +1,5 @@ import type { TRPCRouterRecord } from "@trpc/server"; +import Fuse from "fuse.js"; import { z } from "zod"; import { and, desc, eq } from "@cooper/db"; @@ -10,6 +11,7 @@ export const reviewRouter = { list: publicProcedure .input( z.object({ + search: z.string().optional(), options: z .object({ cycle: z.enum(["SPRING", "FALL", "SUMMER"]).optional(), @@ -18,7 +20,7 @@ export const reviewRouter = { .optional(), }), ) - .query(({ ctx, input }) => { + .query(async ({ ctx, input }) => { const { options } = input; const conditions = [ @@ -26,10 +28,22 @@ export const reviewRouter = { options?.term && eq(Review.workEnvironment, options.term), ].filter(Boolean); - return ctx.db.query.Review.findMany({ + const reviews = await ctx.db.query.Review.findMany({ orderBy: desc(Review.id), where: conditions.length > 0 ? and(...conditions) : undefined, }); + + if (!input.search) { + return reviews; + } + + const fuseOptions = { + keys: ["reviewHeadline", "textReview", "location"], + }; + + const fuse = new Fuse(reviews, fuseOptions); + + return fuse.search(input.search).map((result) => result.item); }), getByRole: publicProcedure diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 358988a..cfa1601 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + fuse.js: + specifier: ^7.0.0 + version: 7.0.0 geist: specifier: ^1.3.0 version: 1.3.1(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) @@ -511,7 +514,7 @@ importers: version: 7.35.0(eslint@9.7.0) eslint-plugin-react-hooks: specifier: rc - version: 5.1.0-rc-cae764ce-20241025(eslint@9.7.0) + version: 5.1.0-rc-7c8e5e7a-20241101(eslint@9.7.0) eslint-plugin-turbo: specifier: ^2.0.6 version: 2.0.9(eslint@9.7.0) @@ -1585,11 +1588,9 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -5108,8 +5109,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-react-hooks@5.1.0-rc-cae764ce-20241025: - resolution: {integrity: sha512-T0dII19NLMIDsd/kbOrPSVnbr0IhlFITOQoCVZuHDBatzGUY6p9wFPuAei7tQYpOnBhdywGIdx2nR4w/cfkcsA==} + eslint-plugin-react-hooks@5.1.0-rc-7c8e5e7a-20241101: + resolution: {integrity: sha512-90qwhATd1dlXHJ7uNmAIelLkfG3XnxR4vdbgG9WUbd81DT0Na+mXg9VVDYG+3/OQSdkAQlac24/hVQevRHMOpw==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -14519,7 +14520,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@5.1.0-rc-cae764ce-20241025(eslint@9.7.0): + eslint-plugin-react-hooks@5.1.0-rc-7c8e5e7a-20241101(eslint@9.7.0): dependencies: eslint: 9.7.0 From 1b42a44a1a8cd933f0bddd073bdc544248ca617e Mon Sep 17 00:00:00 2001 From: Alton Banushi Date: Sun, 24 Nov 2024 15:27:50 -0500 Subject: [PATCH 3/5] Begin Rework Search Filter + Bar, Adjust Styles on Header (#93) --- apps/web/public/svg/hidingLogo.svg | 49 +++--- apps/web/src/app/(pages)/(dashboard)/page.tsx | 2 +- .../app/(pages)/(dashboard)/roles/page.tsx | 26 ++- apps/web/src/app/_components/header.tsx | 19 +-- .../src/app/_components/search/search-bar.tsx | 150 +++++++++++++--- .../app/_components/search/search-filter.tsx | 58 +++++-- packages/ui/package.json | 1 + packages/ui/src/select.tsx | 160 ++++++++++++++++++ pnpm-lock.yaml | 89 +++++++++- 9 files changed, 456 insertions(+), 98 deletions(-) create mode 100644 packages/ui/src/select.tsx diff --git a/apps/web/public/svg/hidingLogo.svg b/apps/web/public/svg/hidingLogo.svg index bcefdd5..70e2a5b 100644 --- a/apps/web/public/svg/hidingLogo.svg +++ b/apps/web/public/svg/hidingLogo.svg @@ -1,28 +1,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/(pages)/(dashboard)/page.tsx b/apps/web/src/app/(pages)/(dashboard)/page.tsx index 45b8310..c660326 100644 --- a/apps/web/src/app/(pages)/(dashboard)/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/page.tsx @@ -4,7 +4,7 @@ export default function Home() { return (
- +
); diff --git a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx index b798648..aa1a937 100644 --- a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx @@ -47,25 +47,19 @@ export default function Roles({ useEffect(() => { setMounted(true); }, []); + useEffect(() => { - if (!mounted) { - return; - } - if (!validationResult.success) { + if (mounted && !validationResult.success) { toast({ - title: "Invalid search params", + title: "Invalid Search Parameters", description: validationResult.error.issues .map((issue) => issue.message) .join(", "), variant: "destructive", }); + setMounted(false); } - }, [ - toast, - mounted, - validationResult.success, - validationResult.error?.issues, - ]); + }, [toast, mounted, validationResult]); const reviews = api.review.list.useQuery({ search: searchParams?.search, @@ -73,19 +67,19 @@ export default function Roles({ }); const [selectedReview, setSelectedReview] = useState( - reviews.data ? reviews.data[0] : undefined, + reviews.isSuccess ? reviews.data[0] : undefined, ); useEffect(() => { - if (reviews.data) { + if (reviews.isSuccess) { setSelectedReview(reviews.data[0]); } - }, [reviews.data]); + }, [reviews.isSuccess, reviews.data]); return ( <> - - {reviews.data && ( + + {reviews.isSuccess && (
{reviews.data.map((review, i) => { diff --git a/apps/web/src/app/_components/header.tsx b/apps/web/src/app/_components/header.tsx index 1b02b41..4391843 100644 --- a/apps/web/src/app/_components/header.tsx +++ b/apps/web/src/app/_components/header.tsx @@ -28,12 +28,12 @@ export default function Header({ session, auth }: HeaderProps) {
{/* Logo + Cooper */} -
+
Logo Picture

{/* Centered Links */} -
+

- {" "} - Jobs{" "} + Jobs

Companies diff --git a/apps/web/src/app/_components/search/search-bar.tsx b/apps/web/src/app/_components/search/search-bar.tsx index 6ca060e..1c0ba5e 100644 --- a/apps/web/src/app/_components/search/search-bar.tsx +++ b/apps/web/src/app/_components/search/search-bar.tsx @@ -1,46 +1,144 @@ +import { useState } from "react"; import { useFormContext } from "react-hook-form"; +import { Button } from "@cooper/ui/button"; import { FormControl, FormField, FormItem } from "@cooper/ui/form"; import { Input } from "@cooper/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@cooper/ui/select"; -export function SearchBar() { +interface SearchBarProps { + cycle?: "FALL" | "SPRING" | "SUMMER"; + term?: "INPERSON" | "HYBRID" | "REMOTE"; +} + +/** + * This Search Bar employs filtering and fuzzy searching. + * + * NOTE: Cycle and Term only make sense for Roles + * + * @param param0 Cycle and Term to be set as default values for their respective dropdowns + * @returns A search bar which is connected to a parent 'useForm' + */ +export function SearchBar({ cycle, term }: SearchBarProps) { const form = useFormContext(); + const [selectedCycle, setSelectedCycle] = useState(cycle); + const [selectedTerm, setSelectedTerm] = useState(term); + return ( -
+
( - + -
- {/* SEARCH TEXT INPUT */} - - - - - -
+ +
+
+ )} + /> + ( + + + + + + )} + /> + ( + + + )} /> +
); } diff --git a/apps/web/src/app/_components/search/search-filter.tsx b/apps/web/src/app/_components/search/search-filter.tsx index e0376fd..0d2224d 100644 --- a/apps/web/src/app/_components/search/search-filter.tsx +++ b/apps/web/src/app/_components/search/search-filter.tsx @@ -1,58 +1,86 @@ "use client"; import { useCallback } from "react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { WorkEnvironment, WorkTerm } from "@cooper/db/schema"; import { Form } from "@cooper/ui/form"; import { SearchBar } from "~/app/_components/search/search-bar"; const formSchema = z.object({ searchText: z.string(), + searchCycle: z + .nativeEnum(WorkTerm, { + message: "Invalid cycle type", + }) + .optional(), + searchTerm: z + .nativeEnum(WorkEnvironment, { + message: "Invalid cycle type", + }) + .optional(), }); export type SearchFilterFormType = typeof formSchema; interface SearchFilterProps { search?: string; + cycle?: "FALL" | "SPRING" | "SUMMER"; + term?: "INPERSON" | "HYBRID" | "REMOTE"; + alternatePathname?: string; } /** * Handles searching logic, updates the search param base on user search and passes the text to backend with fuzzy searching. - * @param param0 user input text that's passed to the fuzzy search + * @param param0 user input text that's passed to the fuzzy search, cycle filter, and term filter. + * - alternatePathname example: "/roles" or "/companies" * @returns the search bar with the user inputted text */ -export default function SearchFilter({ search }: SearchFilterProps) { +export default function SearchFilter({ + search, + cycle, + term, + alternatePathname, +}: SearchFilterProps) { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { searchText: search ?? "", + searchCycle: cycle, + searchTerm: term, }, }); - const searchParams = useSearchParams(); const router = useRouter(); const pathName = usePathname(); const createQueryString = useCallback( - (name: string, value: string) => { - const params = new URLSearchParams(searchParams); - params.set(name, value); - return params.toString(); + ({ searchText, searchCycle, searchTerm }: z.infer) => { + // Initialize URLSearchParams with the required searchText + const params = new URLSearchParams({ search: searchText }); + + // Conditionally add searchCycle and searchTerm if they have values + if (searchCycle) { + params.set("cycle", searchCycle); + } + if (searchTerm) { + params.set("term", searchTerm); + } + + return params.toString(); // Returns a query string, e.g., "search=yo&cycle=SPRING" }, - [searchParams], + [], ); function onSubmit(values: z.infer) { - if (values.searchText != "") { - router.push( - pathName + `/?${createQueryString("search", values.searchText)}`, - ); + if (alternatePathname) { + router.push(alternatePathname + "?" + createQueryString(values)); } else { - router.push(pathName); + router.push(pathName + "?" + createQueryString(values)); } } @@ -60,7 +88,7 @@ export default function SearchFilter({ search }: SearchFilterProps) {
- +
diff --git a/packages/ui/package.json b/packages/ui/package.json index cdcde64..f1c076b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.2", "class-variance-authority": "^0.7.0", diff --git a/packages/ui/src/select.tsx b/packages/ui/src/select.tsx new file mode 100644 index 0000000..d45f1a8 --- /dev/null +++ b/packages/ui/src/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "@cooper/ui"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className, + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfa1601..36531fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -411,6 +411,9 @@ importers: '@radix-ui/react-radio-group': specifier: ^1.2.0 version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -2457,6 +2460,9 @@ packages: '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.0.1': resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} @@ -2652,6 +2658,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.0.4': resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} peerDependencies: @@ -2883,6 +2898,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.2': + resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.0.2': resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -5108,7 +5136,6 @@ packages: engines: {node: '>=4.0'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-react-hooks@5.1.0-rc-7c8e5e7a-20241101: resolution: {integrity: sha512-90qwhATd1dlXHJ7uNmAIelLkfG3XnxR4vdbgG9WUbd81DT0Na+mXg9VVDYG+3/OQSdkAQlac24/hVQevRHMOpw==} engines: {node: '>=10'} @@ -7786,6 +7813,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-router-config@5.1.1: resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} peerDependencies: @@ -11614,6 +11651,8 @@ snapshots: '@polka/url@1.0.0-next.25': {} + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.0.1': dependencies: '@babel/runtime': 7.25.4 @@ -11808,6 +11847,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 @@ -12041,6 +12086,35 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-select@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.4 @@ -14519,7 +14593,7 @@ snapshots: object.fromentries: 2.0.8 safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - + eslint-plugin-react-hooks@5.1.0-rc-7c8e5e7a-20241101(eslint@9.7.0): dependencies: eslint: 9.7.0 @@ -17729,6 +17803,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll@2.6.0(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.3 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + react-router-config@5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.4 From 01d33753033e4318ebfbc5b56aca634968b072af Mon Sep 17 00:00:00 2001 From: Alton Banushi Date: Fri, 29 Nov 2024 21:18:33 -0500 Subject: [PATCH 4/5] #95 empty state search (#96) * Begin Rework Search Filter + Bar, Adjust Styles on Header * Change Cycle/WorkModel to dropdowns + add search button since shi is broke * Search bar changes * Lint... * Lint again.. * No Results Component * Search Filters Populate Based on URL Search Params * Rishi micromanaging (jk) * Home page reroutes to roles * Loading Page Updates * Jank solution to reset select * Lint * Linting + Toast Issue * Lint... * Prettier Fix * how to center a div? --- .../(pages)/(dashboard)/companies/page.tsx | 19 +++++++---- .../app/(pages)/(dashboard)/roles/page.tsx | 6 +++- apps/web/src/app/_components/cooper-logo.tsx | 16 +++++++++ apps/web/src/app/_components/header.tsx | 9 ++--- .../src/app/_components/loading-results.tsx | 12 +++++++ apps/web/src/app/_components/no-results.tsx | 33 +++++++++++++++++++ 6 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/app/_components/cooper-logo.tsx create mode 100644 apps/web/src/app/_components/loading-results.tsx create mode 100644 apps/web/src/app/_components/no-results.tsx diff --git a/apps/web/src/app/(pages)/(dashboard)/companies/page.tsx b/apps/web/src/app/(pages)/(dashboard)/companies/page.tsx index 41d2419..f8659bf 100644 --- a/apps/web/src/app/(pages)/(dashboard)/companies/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/companies/page.tsx @@ -1,5 +1,6 @@ import { unstable_noStore as noStore } from "next/cache"; +import NoResults from "~/app/_components/no-results"; import { RoleReviewCard } from "~/app/_components/reviews/role-review-card"; import SearchFilter from "~/app/_components/search/search-filter"; import { api } from "~/trpc/server"; @@ -20,13 +21,17 @@ export default async function Companies() { return ( <> -
- {roles.map((role) => { - return ( - - ); - })} -
+ {roles.length > 0 ? ( +
+ {roles.map((role) => { + return ( + + ); + })} +
+ ) : ( + + )} ); } diff --git a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx index aa1a937..e47adbe 100644 --- a/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx +++ b/apps/web/src/app/(pages)/(dashboard)/roles/page.tsx @@ -12,6 +12,8 @@ import { WorkEnvironment, WorkTerm } from "@cooper/db/schema"; import { cn } from "@cooper/ui"; import { useToast } from "@cooper/ui/hooks/use-toast"; +import LoadingResults from "~/app/_components/loading-results"; +import NoResults from "~/app/_components/no-results"; import { ReviewCard } from "~/app/_components/reviews/review-card"; import { ReviewCardPreview } from "~/app/_components/reviews/review-card-preview"; import SearchFilter from "~/app/_components/search/search-filter"; @@ -79,7 +81,7 @@ export default function Roles({ return ( <> - {reviews.isSuccess && ( + {reviews.isSuccess && reviews.data.length > 0 && (
{reviews.data.map((review, i) => { @@ -106,6 +108,8 @@ export default function Roles({
)} + {reviews.isSuccess && reviews.data.length === 0 && } + {reviews.isPending && } ); } diff --git a/apps/web/src/app/_components/cooper-logo.tsx b/apps/web/src/app/_components/cooper-logo.tsx new file mode 100644 index 0000000..5339fbc --- /dev/null +++ b/apps/web/src/app/_components/cooper-logo.tsx @@ -0,0 +1,16 @@ +import Image from "next/image"; + +interface CooperLogoProps { + width?: number; +} + +export default function CooperLogo({ width }: CooperLogoProps) { + return ( + Logo Picture + ); +} diff --git a/apps/web/src/app/_components/header.tsx b/apps/web/src/app/_components/header.tsx index 4391843..cb6d287 100644 --- a/apps/web/src/app/_components/header.tsx +++ b/apps/web/src/app/_components/header.tsx @@ -1,6 +1,5 @@ "use client"; -import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -9,6 +8,7 @@ import { cn } from "@cooper/ui"; import { NewReviewDialog } from "~/app/_components/reviews/new-review-dialogue"; import { altivoFont } from "~/app/styles/font"; +import CooperLogo from "./cooper-logo"; interface HeaderProps { session: Session | null; @@ -29,12 +29,7 @@ export default function Header({ session, auth }: HeaderProps) { {/* Logo + Cooper */}
- Logo Picture +

+ +
+

Loading ...

+
+ + ); +} diff --git a/apps/web/src/app/_components/no-results.tsx b/apps/web/src/app/_components/no-results.tsx new file mode 100644 index 0000000..43c8650 --- /dev/null +++ b/apps/web/src/app/_components/no-results.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; + +import { Button } from "@cooper/ui/button"; + +import CooperLogo from "./cooper-logo"; + +export default function NoResults() { + const router = useRouter(); + const pathname = usePathname(); + + function clearFilters() { + router.push(pathname); + } + + return ( +
+ +
+

No Results Found

+ +
+
+ ); +} From 88a1f5af105d73bf3d2a2d00b2bf478c39577a83 Mon Sep 17 00:00:00 2001 From: Matthew Wang Date: Sun, 26 Jan 2025 14:09:01 -0500 Subject: [PATCH 5/5] Max 5 Reviews Per User (#110) * prevents creation of new review if already 5 * max 2 reviews per cycle * resolved changes --- packages/api/src/router/review.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/api/src/router/review.ts b/packages/api/src/router/review.ts index 217097a..fdf7053 100644 --- a/packages/api/src/router/review.ts +++ b/packages/api/src/router/review.ts @@ -1,4 +1,5 @@ import type { TRPCRouterRecord } from "@trpc/server"; +import { TRPCError } from "@trpc/server"; import Fuse from "fuse.js"; import { z } from "zod"; @@ -72,7 +73,33 @@ export const reviewRouter = { create: protectedProcedure .input(CreateReviewSchema) - .mutation(({ ctx, input }) => { + .mutation(async ({ ctx, input }) => { + if (!input.profileId) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You must be logged in to leave a review", + }); + } + const reviews = await ctx.db.query.Review.findMany({ + where: eq(Review.profileId, input.profileId), + }); + if (reviews.length >= 5) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "You can only leave 5 reviews", + }); + } + const reviewsInSameCycle = reviews.filter( + (review) => + review.workTerm === input.workTerm && + review.workYear === input.workYear, + ); + if (reviewsInSameCycle.length >= 2) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "You can only leave 2 reviews per cycle", + }); + } return ctx.db.insert(Review).values(input); }),