From 2766f02d66e05000687df12e19935a57700237c4 Mon Sep 17 00:00:00 2001 From: MingoHaha Date: Tue, 19 Aug 2025 21:31:13 +0800 Subject: [PATCH 1/4] i added ratings --- .../components/application-tracker.tsx | 144 +++++- .../components/job-rating-modal.tsx | 443 ++++++++++++++++++ app/api/students/ratings/route.ts | 77 +++ lib/authOptions.ts | 2 +- package-lock.json | 8 +- package.json | 2 +- public/animations/Briefcase.json | 1 + public/animations/Star rating.json | 1 + public/animations/star.json | 1 + types/next-auth.d.ts | 22 + 10 files changed, 686 insertions(+), 15 deletions(-) create mode 100644 app/(app)/students/jobs/applications/components/job-rating-modal.tsx create mode 100644 app/api/students/ratings/route.ts create mode 100644 public/animations/Briefcase.json create mode 100644 public/animations/Star rating.json create mode 100644 public/animations/star.json diff --git a/app/(app)/students/jobs/applications/components/application-tracker.tsx b/app/(app)/students/jobs/applications/components/application-tracker.tsx index 6ffb965..53a1b61 100644 --- a/app/(app)/students/jobs/applications/components/application-tracker.tsx +++ b/app/(app)/students/jobs/applications/components/application-tracker.tsx @@ -25,6 +25,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { ApplicationDetailsModal } from "./application-details" import { FollowUpChatModal } from "./follow-up-chat-modal" +import { JobRatingModal } from "./job-rating-modal" import { toast } from "react-toastify" import Menu from "@mui/material/Menu" import MenuItem from "@mui/material/MenuItem" @@ -38,6 +39,7 @@ import { RiErrorWarningLine } from "react-icons/ri" import { BadgeCheck as LuBadgeCheck } from "lucide-react" import Tooltip from "@mui/material/Tooltip" import { styled } from "@mui/material/styles" +import { AiFillStar } from "react-icons/ai" type JobPosting = { employer_id?: string @@ -91,6 +93,17 @@ type ApplicationData = { portfolio?: string[] } +type JobRatingData = { + companyLogo: string + jobTitle: string + recruiterName: string + companyName: string + dateOfHiring: string + jobType: string + department: string + location: string +} + const CustomTooltip = styled(Tooltip)(() => ({ [`& .MuiTooltip-tooltip`]: { backgroundColor: "#fff", @@ -264,8 +277,13 @@ export default function ApplicationTrackerNoSidebar() { if (cached) { try { const parsed = JSON.parse(cached) + if (typeof parsed === "string") { + newLogoUrls[idx] = parsed + return + } if (parsed && typeof parsed === "object" && parsed.url && typeof parsed.url === "string") { newLogoUrls[idx] = parsed.url + sessionStorage.setItem(cacheKey, JSON.stringify(parsed.url)) return } } catch {} @@ -280,9 +298,16 @@ export default function ApplicationTrackerNoSidebar() { }), }) const json = await res.json() - if (json.signedUrl && typeof json.signedUrl === "string") { - newLogoUrls[idx] = json.signedUrl - sessionStorage.setItem(cacheKey, JSON.stringify({ url: json.signedUrl })) + + let url = "" + if (typeof json.signedUrl === "string") { + url = json.signedUrl + } else if (json.url && typeof json.url === "string") { + url = json.url + } + if (url && (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/"))) { + newLogoUrls[idx] = url + sessionStorage.setItem(cacheKey, JSON.stringify(url)) } else { newLogoUrls[idx] = null sessionStorage.removeItem(cacheKey) @@ -353,6 +378,74 @@ export default function ApplicationTrackerNoSidebar() { }; const [activeTab, setActiveTab] = useState("all") + const [isJobRatingModalOpen, setIsJobRatingModalOpen] = useState(false) + const [jobRatingData, setJobRatingData] = useState(null) + const [jobRatingCompanyImg, setJobRatingCompanyImg] = useState("") + const [jobRatingRecruiterImg, setJobRatingRecruiterImg] = useState("") + const [jobRatingRecruiterName, setJobRatingRecruiterName] = useState("") + + async function handleOpenJobRatingModal(app: ApplicationData) { + let logo = "" + let recruiterImg = "" + let recruiterName = "" + const logoPath = app.company_logo_image_path || app.job_postings?.company_logo_image_path || "" + if (logoPath) { + try { + const res = await fetch("/api/employers/get-signed-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + bucket: "company.logo", + path: logoPath, + }), + }) + const json = await res.json() + if (json.signedUrl && typeof json.signedUrl === "string") { + logo = json.signedUrl + } + } catch { + logo = "" + } + } + const recruiterProfileImg = app.profile_img || "" + if (recruiterProfileImg) { + if (recruiterProfileImg.startsWith("http")) { + recruiterImg = recruiterProfileImg + } else { + try { + const res = await fetch("/api/employers/get-signed-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + bucket: "user.avatars", + path: recruiterProfileImg, + }), + }) + const json = await res.json() + recruiterImg = json.signedUrl || "" + } catch { + recruiterImg = "" + } + } + } + recruiterName = app.job_postings?.registered_employers?.first_name + ? `${app.job_postings?.registered_employers?.first_name} ${app.job_postings?.registered_employers?.last_name || ""}`.trim() + : "" + setJobRatingCompanyImg(logo) + setJobRatingRecruiterImg(recruiterImg) + setJobRatingRecruiterName(recruiterName) + setJobRatingData({ + companyLogo: logo, + jobTitle: app.job_postings?.job_title || "", + recruiterName, + companyName: app.company_name || app.job_postings?.registered_employers?.company_name || "", + dateOfHiring: app.applied_at || "", + jobType: app.job_postings?.work_type || "", + department: "", + location: app.job_postings?.location || "", + }) + setIsJobRatingModalOpen(true) + } return ( <> @@ -473,7 +566,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData, logoUrls, - + handleOpenJobRatingModal ) : (
@@ -496,7 +589,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData.filter(a => (a.status || "").toLowerCase() === "new"), logoUrls, - + handleOpenJobRatingModal ) : (
@@ -519,7 +612,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData.filter(a => (a.status || "").toLowerCase() === "shortlisted"), logoUrls, - + handleOpenJobRatingModal ) : (
@@ -542,7 +635,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData.filter(a => (a.status || "").toLowerCase() === "interview scheduled"), logoUrls, - + handleOpenJobRatingModal ) : (
@@ -565,7 +658,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData.filter(a => (a.status || "").toLowerCase() === "hired"), logoUrls, - + handleOpenJobRatingModal ) : (
@@ -588,7 +681,7 @@ export default function ApplicationTrackerNoSidebar() { handleMenuOpen, applicationsData.filter(a => (a.status || "").toLowerCase() === "rejected"), logoUrls, - + handleOpenJobRatingModal ) : (
@@ -743,6 +836,16 @@ export default function ApplicationTrackerNoSidebar() { jobTitle={followUpDetails?.jobTitle || ""} company={followUpDetails?.company || ""} /> + + setIsJobRatingModalOpen(false)} + jobTitle={jobRatingData?.jobTitle || ""} + companyName={jobRatingData?.companyName || ""} + recruiterProfileImg={jobRatingRecruiterImg} + companyLogoImg={jobRatingCompanyImg} + recruiterName={jobRatingRecruiterName} + /> ) } @@ -757,6 +860,7 @@ function generateApplicationCards( handleMenuOpen: (event: React.MouseEvent, id: number) => void, applicationsData?: ApplicationData[], logoUrls?: { [key: number]: string | null }, + handleOpenJobRatingModal?: (app: ApplicationData) => void ) { const statusConfig = { all: { title: "Mixed", badge: "", hover: "hover:border-l-yellow-400" }, @@ -982,6 +1086,26 @@ function generateApplicationCards( Follow Up )} + {cardStatus === "hired" && ( + + )} + ))} +
+ ) + + useEffect(() => { + if (currentStep === "complete") { + } + }, [currentStep]) + + const getStepIcon = () => { + switch (currentStep) { + case "intro": + return + case "overall": + return + case "recruiter": + return recruiterImgUrl ? ( +
+ Recruiter +
+ ) : ( +
+ +
+ ) + case "company": + return companyLogoUrl ? ( +
+ Company +
+ ) : ( +
+ {companyName?.charAt(0) || "C"} +
+ ) + case "complete": + return ( +
+ + +
+ ) + } + } + + const getStepTitle = () => { + switch (currentStep) { + case "intro": + return "Share Your Experience" + case "overall": + return "Overall Job Experience" + case "recruiter": + return "Recruiter Experience" + case "company": + return "Company Rating" + default: + return "" + } + } + + const getStepContent = () => { + switch (currentStep) { + case "intro": + return ( +
+

{getStepTitle()}

+

+ Help others by rating your experience with the {jobTitle} position at {companyName} +

+
+

This is a multi-step rating that will only take a moment

+
+ + +
+
+ ) + case "overall": + return ( +
+

How would you rate your overall experience with this position?

+ handleRatingChange("overall", rating)} + /> +