Skip to content

Commit

Permalink
Merge branch 'main' into cities-api-combobox
Browse files Browse the repository at this point in the history
  • Loading branch information
tracyyh committed Feb 25, 2025
2 parents 476e216 + a00b9f6 commit fe2837c
Show file tree
Hide file tree
Showing 33 changed files with 14,356 additions and 7,215 deletions.
3 changes: 3 additions & 0 deletions apps/web/public/svg/magnifyingGlass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/public/svg/star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 26 additions & 82 deletions apps/web/src/app/(pages)/(dashboard)/roles/page.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,59 @@
"use client";

import { useEffect, useState } from "react";
import { z } from "zod";

import type {
ReviewType,
WorkEnvironmentType,
WorkTermType,
} from "@cooper/db/schema";
import { WorkEnvironment, WorkTerm } from "@cooper/db/schema";
import type { RoleType } 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";
import { RoleCardPreview } from "~/app/_components/reviews/role-card-preview";
import { RoleInfo } from "~/app/_components/reviews/role-info";
import { api } from "~/trpc/react";

export default function Roles({
searchParams,
}: {
searchParams?: {
search?: string;
cycle?: WorkTermType;
term?: WorkEnvironmentType;
};
}) {
const { toast } = useToast();
export default function Roles() {
const roles = api.role.list.useQuery();

const RolesSearchParam = z.object({
cycle: z
.nativeEnum(WorkTerm, {
message: "Invalid cycle type",
})
.optional(),
term: z
.nativeEnum(WorkEnvironment, {
message: "Invalid term type",
})
.optional(),
});

const validationResult = RolesSearchParam.safeParse(searchParams);
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

useEffect(() => {
if (mounted && !validationResult.success) {
toast({
title: "Invalid Search Parameters",
description: validationResult.error.issues
.map((issue) => issue.message)
.join(", "),
variant: "destructive",
});
setMounted(false);
}
}, [toast, mounted, validationResult]);

const reviews = api.review.list.useQuery({
search: searchParams?.search,
options: validationResult.success ? validationResult.data : {},
});

const [selectedReview, setSelectedReview] = useState<ReviewType | undefined>(
reviews.isSuccess ? reviews.data[0] : undefined,
const [selectedRole, setSelectedRole] = useState<RoleType | undefined>(
roles.isSuccess ? roles.data[0] : undefined,
);

useEffect(() => {
if (reviews.isSuccess) {
setSelectedReview(reviews.data[0]);
if (roles.isSuccess) {
setSelectedRole(roles.data[0]);
}
}, [reviews.isSuccess, reviews.data]);
}, [roles.isSuccess, roles.data]);

return (
<>
<SearchFilter search={searchParams?.search} {...validationResult.data} />
{reviews.isSuccess && reviews.data.length > 0 && (
<div className="mb-8 grid h-[70dvh] w-4/5 grid-cols-5 gap-4 lg:w-3/4">
<div className="col-span-2 gap-3 overflow-scroll pr-4">
{reviews.data.map((review, i) => {
{roles.isSuccess && roles.data.length > 0 && (
<div className="flex h-[100dvh] w-[95%] gap-4 divide-x divide-[#474747] pl-4 pr-4 lg:w-[95%]">
<div className="w-[28%] gap-3">
{roles.data.map((role, i) => {
return (
<div key={review.id} onClick={() => setSelectedReview(review)}>
<ReviewCardPreview
reviewObj={review}
<div key={role.id} onClick={() => setSelectedRole(role)}>
<RoleCardPreview
reviewObj={role}
className={cn(
"mb-4 hover:border-2",
selectedReview
? selectedReview.id === review.id &&
"border-2 bg-cooper-gray-100"
: !i && "border-2 bg-cooper-gray-100",
"mb-4 hover:bg-cooper-gray-100",
selectedRole
? selectedRole.id === role.id && "bg-cooper-gray-200"
: !i && "bg-cooper-gray-200",
)}
/>
</div>
);
})}
</div>
<div className="col-span-3 overflow-scroll">
{reviews.data.length > 0 && reviews.data[0] && (
<ReviewCard reviewObj={selectedReview ?? reviews.data[0]} />
<div className="col-span-3 w-[72%] overflow-auto p-1">
{roles.data.length > 0 && roles.data[0] && (
<RoleInfo roleObj={selectedRole ?? roles.data[0]} />
)}
</div>
</div>
)}
{reviews.isSuccess && reviews.data.length === 0 && <NoResults />}
{reviews.isPending && <LoadingResults />}
{roles.isSuccess && roles.data.length === 0 && <NoResults />}
{roles.isPending && <LoadingResults />}
</>
);
}
12 changes: 10 additions & 2 deletions apps/web/src/app/_components/combo-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface ComboBoxProps {
valuesAndLabels: ComboBoxOption<string>[];
currLabel: string;
onSelect: (option: string) => void;
triggerClassName?: string;
onChange?: (value: string) => void;
variant?: "default" | "form";
}
Expand All @@ -42,6 +43,7 @@ export default function ComboBox({
valuesAndLabels,
currLabel,
onSelect,
triggerClassName,
onChange,
variant,
}: ComboBoxProps) {
Expand All @@ -54,14 +56,20 @@ export default function ComboBox({

return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild className="min-w-[400px]">
<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",
triggerClassName,
)}
>
<Button
variant="outline"
role="combobox"
aria-expanded={isOpen}
className={cn(
styleVariant,
"w-[180px] justify-between overflow-hidden text-ellipsis text-nowrap",
"justify-between overflow-hidden text-ellipsis text-nowrap",
)}
>
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/_components/form/rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Rating = forwardRef<HTMLInputElement, RatingProps>(
"size-20",
i < hoveredIndex || i < +getValues(name)
? "fill-cooper-yellow-500"
: "fill-cooper-gray-100",
: "fill-cooper-gray-200",
"pr-2 hover:cursor-pointer",
)}
onMouseEnter={() => setHoveredIndex(i + 1)}
Expand Down
76 changes: 66 additions & 10 deletions apps/web/src/app/_components/form/review-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { WorkEnvironment, WorkTerm } from "@cooper/db/schema";
import { cn } from "@cooper/ui";
import { Button } from "@cooper/ui/button";
import { Form } from "@cooper/ui/form";
import { useToast } from "@cooper/ui/hooks/use-toast";
import { CheckIcon } from "@cooper/ui/icons";

import {
Expand All @@ -23,6 +24,7 @@ import {
} from "~/app/_components/form/sections";
import { SubmissionConfirmation } from "~/app/_components/form/submission-confirmation";
import { api } from "~/trpc/react";
import { SubmissionFailure } from "./submission-failure";

const formSchema = z.object({
workTerm: z.nativeEnum(WorkTerm, {
Expand Down Expand Up @@ -227,9 +229,40 @@ export function ReviewForm(props: ReviewFormProps) {
});

const [currentStep, setCurrentStep] = useState<number>(0);
const [validForm, setValidForm] = useState(true);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const { toast } = useToast();

type FieldName = keyof z.infer<typeof formSchema>;

const profile = api.profile.getCurrentUser.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const profileId = profile.data?.id;

const reviews = api.review.getByProfile.useQuery(
{ id: profileId ?? "" },
{
enabled: !!profileId,
},
);

const canReviewForTerm = (): boolean => {
if (!reviews.data) return false;

const currentTerm = form.getValues("workTerm");
const currentYear = form.getValues("workYear");

const reviewsForCurrentTerm = reviews.data.filter(
(review) =>
String(review.workTerm) === currentTerm &&
review.workYear === Number(currentYear),
);

return reviewsForCurrentTerm.length < 2;
};

const next = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
const fields = steps[currentStep - 1]?.fields;
Expand All @@ -241,6 +274,11 @@ export function ReviewForm(props: ReviewFormProps) {
return;
}

if (currentStep === 1 && !canReviewForTerm()) {
alert("You have already submitted too many reviews for this term!");
return;
}

// FIXME: Fix the scrolling eslint issue

if (currentStep <= steps.length) {
Expand All @@ -262,20 +300,38 @@ export function ReviewForm(props: ReviewFormProps) {
scroll.scrollToTop({ duration: 250, smooth: true });
};

const mutation = api.review.create.useMutation();
const mutation = api.review.create.useMutation({
onError: (error) => {
setValidForm(false);
setErrorMessage(error.message || "An unknown error occurred.");

function onSubmit(values: z.infer<ReviewFormType>) {
mutation.mutate({
roleId: props.roleId,
profileId: props.profileId,
companyId: props.company.id,
...values,
});
toast({
title: "Submission Error",
description: error.message || "Something went wrong. Please try again.",
variant: "destructive",
});
},
});

async function onSubmit(values: z.infer<ReviewFormType>) {
try {
await mutation.mutateAsync({
roleId: props.roleId,
profileId: props.profileId,
companyId: props.company.id,
...values,
});
} catch (error) {
console.error("Mutation failed:", error);
}
}

if (currentStep === steps.length + 1) {
// Also check if the mutation is successful before displaying this. Otherwise, show a loading spinner.
return <SubmissionConfirmation />;
if (validForm) {
return <SubmissionConfirmation />;
} else {
return <SubmissionFailure message={errorMessage ?? undefined} />;
}
}

if (currentStep === 0) {
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/app/_components/form/submission-failure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Link from "next/link";

import { Button } from "@cooper/ui/button";

interface SubmissionFailureProps {
message?: string; // Allow passing an optional error message
}

export function SubmissionFailure({ message }: SubmissionFailureProps) {
return (
<div className="flex flex-col border-2">
<div className="z-10 -mb-4 h-4 w-full rounded-t-xl bg-gradient-to-r from-rose-400 via-yellow-400 to-blue-600" />
<div className="flex h-[80vh] w-full flex-col items-center justify-center space-y-6 rounded-xl bg-white px-8 py-32 text-cooper-blue-600">
<div className="text-5xl font-bold">
Whoops, your review is invalid...
</div>
<div className="max-w-3xl text-center text-2xl">
{message ? message : "Something went wrong. Please try again."}
</div>
<div className="flex justify-between space-x-8">
<Link href="/">
<Button>Return to home </Button>
</Link>
</div>
</div>
</div>
);
}
Loading

0 comments on commit fe2837c

Please sign in to comment.