Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
d442948
styled model
NUTiffanyUong Feb 2, 2026
0f85ae9
donut chart
NUTiffanyUong Feb 5, 2026
826a835
donut values and sort benefits
NUTiffanyUong Feb 5, 2026
29990a7
fixed linting
NUTiffanyUong Feb 5, 2026
2e12cde
new modal container
NUTiffanyUong Feb 6, 2026
18dd37b
modal
NUTiffanyUong Feb 6, 2026
94e9fea
Merge branch 'main' into modal-container
Tiffybin Feb 8, 2026
6c4f8b5
Merge branch 'main' into modal-container
NUTiffanyUong Feb 9, 2026
02194ba
Merge branch 'modal-container' of https://github.com/sandboxnu/cooper…
NUTiffanyUong Feb 12, 2026
6636d9d
separated role info from modal and replaced collapsable containers wi…
NUTiffanyUong Feb 13, 2026
9a8d770
formatting
NUTiffanyUong Feb 13, 2026
36fbeb3
removed work models
NUTiffanyUong Feb 13, 2026
3beef33
more formatting
NUTiffanyUong Feb 13, 2026
095d775
removed unused import
NUTiffanyUong Feb 13, 2026
fffe53d
change info card to modal container
NUTiffanyUong Feb 15, 2026
b34c86e
linting
NUTiffanyUong Feb 15, 2026
793a470
added modal styling
NUTiffanyUong Feb 15, 2026
5ea567c
fixed div
NUTiffanyUong Feb 15, 2026
14448ef
linting
NUTiffanyUong Feb 15, 2026
883688e
popup styling
NUTiffanyUong Feb 16, 2026
b34fdd6
pop up props
NUTiffanyUong Feb 21, 2026
dd57f1d
handle routing and popping up
NUTiffanyUong Feb 21, 2026
3b8cab1
handle popup when form is filled out
NUTiffanyUong Feb 21, 2026
d29b904
mobile popup styling & formatting
NUTiffanyUong Feb 22, 2026
dbd9c95
Merge branch 'main' into review-form-popup
Tiffybin Feb 22, 2026
cfed8f1
remove duplicates and moved popup
NUTiffanyUong Feb 22, 2026
6f0bdf2
more styling changes
NUTiffanyUong Feb 22, 2026
8ddd866
formatting
NUTiffanyUong Feb 22, 2026
e21045b
Merge branch 'modal-container' into review-form-popup
NUTiffanyUong Feb 22, 2026
3290c65
replace modals
NUTiffanyUong Feb 22, 2026
3ac7666
additional styling changes
NUTiffanyUong Feb 22, 2026
af23513
Merge branch 'main' into review-form-popup
Tiffybin Feb 22, 2026
590f15a
more styling
NUTiffanyUong Feb 22, 2026
c9f0358
Merge branch 'review-form-popup' of https://github.com/sandboxnu/coop…
NUTiffanyUong Feb 22, 2026
7beefac
formatting
NUTiffanyUong Feb 22, 2026
37a0768
utils
NUTiffanyUong Feb 22, 2026
6f5f57a
Merge branch 'main' into review-form-popup
Tiffybin Feb 22, 2026
59905c6
formatting
NUTiffanyUong Feb 22, 2026
931989e
Merge branch 'review-form-popup' of https://github.com/sandboxnu/coop…
NUTiffanyUong Feb 22, 2026
849a031
imports
NUTiffanyUong Feb 22, 2026
1ab7ef1
formatting
NUTiffanyUong Feb 22, 2026
0ceb652
move utils
NUTiffanyUong Feb 22, 2026
ec5a3cb
update published status to reviews
Mar 15, 2026
3f2fa40
change columns to accept null values and upsert drafts into db
Mar 15, 2026
c2a83da
add null values and change save draft text
Mar 19, 2026
f8a747c
add save draft button to review form
Mar 23, 2026
2e560a6
add draft review card
Mar 25, 2026
3216b22
duplicate drafts are not saved to the db
Mar 26, 2026
43e8e1e
drafts go to the top of reviews on profile
Mar 26, 2026
ed674c3
design changes and draft roles not showing up
Mar 27, 2026
086a5d5
new review card styling
Mar 27, 2026
44607a0
fixed radios being handled
Mar 27, 2026
ae8f027
linting
Mar 27, 2026
1ca52cf
Merge branch 'main' into review-form-popup
Tiffybin Mar 27, 2026
455656e
fix merge conflict
Mar 27, 2026
9f80740
fix jobtypetype duplication and linting
Mar 27, 2026
1472607
fix errors in review and roleAndCompany
Mar 27, 2026
e197809
fixed null errors
Mar 28, 2026
708c2ee
handle logo click
Mar 29, 2026
cfe311a
Merge branch 'main' of https://github.com/sandboxnu/cooper into revie…
songmichael11 Apr 2, 2026
751c40e
Merge branch 'main' of https://github.com/sandboxnu/cooper into revie…
songmichael11 Apr 2, 2026
fd1ff7b
made review fields nullable
songmichael11 Apr 2, 2026
5f47020
some linting but there's a lot more linting errors
songmichael11 Apr 2, 2026
1fbbd37
Revert "handle logo click"
gpalmer27 Apr 2, 2026
0964467
fixed redirecting
gpalmer27 Apr 2, 2026
f1a0a26
fixes
Apr 2, 2026
b59ea3c
Merge branch 'review-form-popup' of https://github.com/sandboxnu/coop…
Apr 2, 2026
f891fcb
db stuff
Apr 2, 2026
77fae95
fix interview difficulty error
Apr 2, 2026
77b3b72
fixed averages with drafts
Apr 5, 2026
4f11f5f
Merge branch 'main' into review-form-popup
Tiffybin Apr 5, 2026
0c03ea2
formatting
Apr 5, 2026
cb8aba0
changed || to ??
Apr 5, 2026
7d79959
normalized hourly pay
Apr 6, 2026
9c52a3c
add space to bottom of reviews
Apr 6, 2026
fecc8bf
profile card changes + review form fix
songmichael11 Apr 7, 2026
49cfa29
Merge branch 'review-form-popup' of https://github.com/sandboxnu/coop…
songmichael11 Apr 7, 2026
a80b7e0
more fixes
songmichael11 Apr 7, 2026
1e33155
altered review limit only for published reviews
songmichael11 Apr 7, 2026
7912f0c
lint
songmichael11 Apr 7, 2026
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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@trpc/server": "11.8.0",
"bad-words": "^4.0.0",
"dayjs": "^1.11.13",
"echarts": "^6.0.0",
"fuse.js": "^7.0.0",
"geist": "^1.3.0",
"lucide-react": "^0.436.0",
Expand Down
31 changes: 23 additions & 8 deletions apps/web/src/app/(pages)/(protected)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ProfileCardHeader from "~/app/_components/profile/profile-card-header";
import ProfileTabs from "~/app/_components/profile/profile-tabs";
import { ReviewCard } from "~/app/_components/reviews/review-card";
import { api } from "~/trpc/react";
import { DraftReviewCard } from "~/app/_components/reviews/draft-review-card";

export default function Profile() {
const searchParams = useSearchParams();
Expand Down Expand Up @@ -138,15 +139,29 @@ export default function Profile() {
</div>
</div>

<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 pb-4">
{reviews.length > 0 &&
reviews.map((review) => (
<ReviewCard
key={review.id}
reviewObj={review}
className="w-[100%]"
/>
))}
reviews
.sort(
(a, b) =>
(a.status === "DRAFT" ? -1 : 1) -
(b.status === "DRAFT" ? -1 : 1),
)
.map((review) =>
review.status === "DRAFT" ? (
<DraftReviewCard
key={review.id}
reviewObj={review}
className="w-[100%]"
/>
) : (
<ReviewCard
key={review.id}
reviewObj={review}
className="w-[100%]"
/>
),
)}
</div>
</section>
)}
Expand Down
188 changes: 156 additions & 32 deletions apps/web/src/app/(pages)/(protected)/review-form/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { Filter } from "bad-words";
import { useForm } from "react-hook-form";

import { Button } from "@cooper/ui/button";
Expand All @@ -13,10 +14,10 @@ import {
InterviewSection,
ReviewSection,
} from "~/app/_components/form/sections";
import Popup from "~/app/_components/form/sections/popup";
import { z } from "zod";
import { useCustomToast } from "@cooper/ui";
import { WorkEnvironment, WorkTerm, JobType } from "@cooper/db/schema";
import { Filter } from "bad-words";
import { WorkEnvironment, WorkTerm, JobType, Status } from "@cooper/db/schema";
import dayjs from "dayjs";
import { Form } from "node_modules/@cooper/ui/src/form";
import { PaySection } from "~/app/_components/form/sections/pay-section";
Expand Down Expand Up @@ -91,14 +92,16 @@ const formSchema = z.object({
})
.min(1, {
message: "You need to enter a company.",
}),
})
.nullable(),
roleName: z
.string({
required_error: "You need to enter a company.",
})
.min(1, {
message: "You need to enter a company.",
}),
})
.nullable(),
locationId: z.string().min(1, {
message: "You need to select a location.",
}),
Expand Down Expand Up @@ -133,12 +136,11 @@ const formSchema = z.object({
})
.transform((x) => x === "true")
.pipe(z.boolean()),
federalHolidays: z.boolean().default(false),
freeLunch: z.boolean().default(false),
travelBenefits: z.boolean().default(false),
freeMerch: z.boolean().default(false),
snackBar: z.boolean().default(false),
employeeLounge: z.boolean().default(false),
federalHolidays: z.boolean(),
freeLunch: z.boolean(),
travelBenefits: z.boolean(),
freeMerch: z.boolean(),
snackBar: z.boolean(),
otherBenefits: z.string().nullable(),
});

Expand All @@ -160,6 +162,7 @@ export default function ReviewForm() {
const { toast } = useCustomToast();
const [roleId, setRoleId] = useState<string>("");
const [companyId, setCompanyId] = useState<string>("");
const [showModal, setShowModal] = useState(false);

const form = useForm<z.infer<ReviewFormType>>({
resolver: zodResolver(formSchema),
Expand All @@ -181,17 +184,31 @@ export default function ReviewForm() {
pto: undefined,
federalHolidays: false,
freeLunch: false,
travelBenefits: false,
freeMerch: false,
snackBar: false,
otherBenefits: "",
roleName: "",
companyName: "",
},
});

// Watch form values and update roleId and companyId
const roleName = form.watch("roleName");
const companyName = form.watch("companyName");

const isDirty = form.formState.isDirty;
const isDirtyRef = useRef(isDirty);

useEffect(() => {
isDirtyRef.current = isDirty;
}, [isDirty]);

useEffect(() => {
form.reset();
setShowModal(false);
isDirtyRef.current = false;
}, [form]);

useEffect(() => {
if (roleName) {
setRoleId(roleName);
Expand All @@ -208,6 +225,21 @@ export default function ReviewForm() {
}
}, [companyName]);

useEffect(() => {
const handleLeave: EventListener = () => {
if (isDirtyRef.current) {
setShowModal(true);
} else {
router.push("/roles");
}
};
window.addEventListener("review-form:leave-attempt", handleLeave);

return () => {
window.removeEventListener("review-form:leave-attempt", handleLeave);
};
}, [router]);

const profileId = profile?.id;

const reviews = api.review.getByProfile.useQuery(
Expand All @@ -225,6 +257,7 @@ export default function ReviewForm() {

const reviewsForCurrentTerm = reviews.data.filter(
(review) =>
review.status === Status.PUBLISHED &&
String(review.workTerm) === currentTerm &&
review.workYear === Number(currentYear),
);
Expand All @@ -241,6 +274,16 @@ export default function ReviewForm() {
},
});

const draftMutation = api.review.saveDraft.useMutation({
onSuccess: () => {
router.push("/roles");
setShowModal(false);
},
onError: (error) => {
toast.error(error.message || "Failed to save draft. Please try again.");
},
});

async function onSubmit(values: z.infer<ReviewFormType>) {
try {
await mutation.mutateAsync({
Expand All @@ -250,6 +293,7 @@ export default function ReviewForm() {
...values,
interviewRating: 1,
reviewHeadline: "",
status: Status.PUBLISHED,
});
} catch (error) {
// Error is already handled by onError callback
Expand All @@ -258,13 +302,65 @@ export default function ReviewForm() {
}

if (!sessionLoading && !profileLoading && (!session || !profile)) {
router.push("/");
router.push("/roles");
}

if (!session || !profile) {
return null;
}

const discardDraft = () => {
router.push("/roles");
};

const normalizeRadios = (v: unknown) =>
v === true || v === "yes" ? true : v === false || v === "no" ? false : null;

async function onSaveDraft() {
try {
const values = form.getValues();

const draftPayload: Record<string, unknown> = {
roleId: roleId,
profileId: profileId,
companyId: companyId,
workTerm: values.workTerm,
workYear: values.workYear,
overallRating: values.overallRating,
cultureRating: values.cultureRating,
supervisorRating: values.supervisorRating,
interviewRating: 1,
interviewDifficulty: +values.interviewDifficulty || null,
interviewReview: values.interviewReview ?? null,
reviewHeadline: "",
textReview: values.textReview || null,
locationId: values.locationId || null,
jobType: values.jobType,
hourlyPay: values.hourlyPay === "" ? null : values.hourlyPay,
workEnvironment: values.workEnvironment,
drugTest: normalizeRadios(values.drugTest),
pto: normalizeRadios(values.pto),
overtimeNormal: normalizeRadios(values.overtimeNormal),
federalHolidays: values.federalHolidays || null,
freeLunch: values.freeLunch || null,
travelBenefits: values.travelBenefits || null,
freeMerch: values.freeMerch || null,
snackBar: values.snackBar || null,
otherBenefits: values.otherBenefits ?? null,
status: Status.DRAFT,
};

await draftMutation.mutateAsync(
draftPayload as Parameters<typeof draftMutation.mutateAsync>[0],
);

form.reset(values);
isDirtyRef.current = false;
toast.success("This draft has been saved.");
} catch (error) {
console.error("Draft save failed:", error);
}
}
if (
session.user.role &&
session.user.role !== UserRole.STUDENT &&
Expand All @@ -283,7 +379,9 @@ export default function ReviewForm() {

return (
<Form {...form}>
<div className="flex h-screen w-full flex-col items-center justify-center overflow-auto bg-white md:flex-row">
<div
className={`${showModal ? "pointer-events-none" : ""} flex h-screen w-full flex-col items-center justify-center overflow-auto bg-white md:flex-row`}
>
<div className="justify-left mt-4 flex h-full w-[65%] flex-col pr-3.5 pt-10">
<div className="text-cooper-gray-550 text-lg">Basic information</div>
<div className="flex w-full flex-wrap gap-10 pb-12 xl:flex-nowrap">
Expand Down Expand Up @@ -317,30 +415,56 @@ export default function ReviewForm() {
<div className="flex flex-wrap gap-10 overflow-auto pb-10 xl:flex-nowrap">
<ReviewSection />
</div>

{/* Submit Button */}
<div className="flex justify-end pb-12 pt-6">
<Button
type="button"
onClick={async () => {
const isValid = await form.trigger();
if (!isValid) {
toast.error("Please fill in all required fields.");
return;
}
await form.handleSubmit(onSubmit)();
}}
disabled={mutation.isPending}
className="bg-cooper-gray-550 hover:bg-cooper-gray-600 rounded-lg border-none px-8 py-3 text-lg font-semibold text-white"
>
{mutation.isPending ? "Submitting..." : "Submit review"}
</Button>
<div className="flex gap-2 justify-end">
{/* Save Draft Button */}
<div className="pb-12 pt-6">
<Button
type="button"
onClick={onSaveDraft}
disabled={mutation.isPending || draftMutation.isPending}
className="bg-white hover:bg-cooper-gray-600
rounded-lg border border-cooper-gray-150 2-253 px-8 py-3 text-lg font-semibold
text-[#151515]"
>
{draftMutation.isPending ? "Saving draft..." : "Save draft"}
</Button>
</div>
{/* Submit Button */}
<div className="pb-12 pt-6">
<Button
type="button"
onClick={async () => {
const isValid = await form.trigger();
if (!isValid) {
toast.error("Please fill in all required fields.");
return;
}
await form.handleSubmit(onSubmit)();
}}
disabled={mutation.isPending || draftMutation.isPending}
className="bg-cooper-gray-550 hover:bg-cooper-gray-600 rounded-lg border-none px-8 py-3 text-lg font-semibold text-white"
>
{mutation.isPending ? "Submitting..." : "Submit review"}
</Button>
</div>
</div>
</div>
) : (
<div>You already submitted too many reviews for this term</div>
)}
</div>
{isDirty && showModal && (
<div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-xs">
<div className="pointer-events-auto">
<Popup
showModal={showModal}
onCancel={() => setShowModal(false)}
onDiscard={discardDraft}
onSave={onSaveDraft}
/>
</div>
</div>
)}
</div>
</Form>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/_components/admin/dashboard-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
category: DashboardCategory;
title: string;
subtitle?: string;
description?: string;
description?: string | null;
company?: string;
location?: string | null;
createdAt: Date;
Expand Down Expand Up @@ -603,7 +603,7 @@

return result;
},
[activeTab, searchQuery],

Check warning on line 606 in apps/web/src/app/_components/admin/dashboard-table.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has an unnecessary dependency: 'searchQuery'. Either exclude it or remove the dependency array
);

const recent = useProcessedItems({
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/_components/companies/company-reviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function CompanyReview({ companyObj }: CompanyReviewProps) {

const averages = api.review.list
.useQuery({})
.data?.filter((r) => r.overallRating != 0)
.map((review) => review.overallRating);
.data?.map((review) => review.overallRating)
.filter((rating): rating is number => rating != null && rating !== 0);
const cooperAvg: number =
Math.round(
((averages ?? []).reduce((accumulator, currentValue) => {
Expand Down
Loading
Loading