diff --git a/app/student/profile/page.tsx b/app/student/profile/page.tsx index c3f8ed32..674cf74a 100644 --- a/app/student/profile/page.tsx +++ b/app/student/profile/page.tsx @@ -76,13 +76,11 @@ export default function ProfilePage() { const { redirectIfNotLoggedIn } = useAuthContext(); const profile = useProfileData(); const profileActions = useProfileActions(); - const modalRegistry = useModalRegistry(); const [isEditing, setIsEditing] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [autoApplySaving, setAutoApplySaving] = useState(false); const [autoApplyError, setAutoApplyError] = useState(null); - const router = useRouter(); const { url: resumeURL, sync: syncResumeURL } = useFile({ fetcher: UserService.getMyResumeURL, @@ -110,18 +108,12 @@ export default function ProfilePage() { setAutoApplySaving(true); setAutoApplyError(null); - const prev = !!profile.data?.apply_for_me; - try { - // await profileActions.update.mutateAsync({ apply_for_me: !prev }); await UserService.updateMyProfile({ apply_for_me: newEnabled, auto_apply_enabled_at: newEnabled ? new Date().toISOString() : null, }); void queryClient.invalidateQueries({ queryKey: ["my-profile"] }); - - // console.log("apply_for_me: ", profile?.data?.apply_for_me); - // console.log("auto_apply_enabled_at:", profile.data?.auto_apply_enabled_at); } catch (e: any) { setAutoApplyError((e as string) ?? "Failed to update auto-apply"); } finally { diff --git a/app/student/saved/page.tsx b/app/student/saved/page.tsx index ef546121..f303ba14 100644 --- a/app/student/saved/page.tsx +++ b/app/student/saved/page.tsx @@ -1,25 +1,49 @@ "use client"; +import React from "react"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Heart } from "lucide-react"; +import { ArrowUpRight, Heart } from "lucide-react"; import { useJobsData } from "@/lib/api/student.data.api"; import { useAuthContext } from "@/lib/ctx-auth"; import { Loader } from "@/components/ui/loader"; import { Card } from "@/components/ui/card"; import { JobHead, SuperListingBadge } from "@/components/shared/jobs"; -import { Job } from "@/lib/db/db.types"; +import { Job, SavedJob } from "@/lib/db/db.types"; import { HeaderIcon, HeaderText } from "@/components/ui/text"; import { Separator } from "@/components/ui/separator"; import { PageError } from "@/components/ui/error"; -import { useJobActions } from "@/lib/api/student.actions.api"; +import { SaveJobButton } from "@/components/features/student/job/save-job-button"; import { cn } from "@/lib/utils"; +type SavedJobItem = SavedJob & Partial; + export default function SavedJobsPage() { const { isAuthenticated, redirectIfNotLoggedIn } = useAuthContext(); const jobs = useJobsData(); - const jobActions = useJobActions(); + const savedJobs = React.useMemo( + () => + [...(jobs.savedJobs as SavedJobItem[])].sort((a, b) => { + const firstSavedAt = + (a as { saved_at?: string | null }).saved_at ?? + a.created_at ?? + a.job?.created_at ?? + a.jobs?.created_at ?? + ""; + const secondSavedAt = + (b as { saved_at?: string | null }).saved_at ?? + b.created_at ?? + b.job?.created_at ?? + b.jobs?.created_at ?? + ""; + return ( + new Date(secondSavedAt).getTime() - new Date(firstSavedAt).getTime() + ); + }), + [jobs.savedJobs], + ); redirectIfNotLoggedIn(); @@ -32,7 +56,7 @@ export default function SavedJobsPage() { Saved Jobs - {jobs.savedJobs?.length} saved + {savedJobs.length} saved @@ -44,7 +68,7 @@ export default function SavedJobsPage() { title="Failed to load saved jobs." description={jobs.error.message} /> - ) : jobs.savedJobs.length === 0 ? ( + ) : savedJobs.length === 0 ? (

@@ -63,13 +87,10 @@ export default function SavedJobsPage() {

) : (
- {jobs.savedJobs.map((savedJob) => ( + {savedJobs.map((savedJob, index) => ( { - void jobActions.toggleSave.mutateAsync(savedJob.id ?? ""); - }} - saving={jobActions.toggleSave.isPending} /> ))}
@@ -79,45 +100,79 @@ export default function SavedJobsPage() { ); } -const SavedJobCard = ({ - savedJob, - handleUnsaveJob, - saving, -}: { - savedJob: Job; - handleUnsaveJob: () => void; - saving: boolean; -}) => { - const superListingTitle = savedJob.challenge?.title?.trim(); - const isSuperListing = Boolean(superListingTitle); +const SavedJobCard = ({ savedJob }: { savedJob: SavedJobItem }) => { + const router = useRouter(); + const job = savedJob.job ?? savedJob.jobs ?? savedJob; + const jobRecord = job as Record | undefined; + const challengeTitleFromJoin = ( + jobRecord?.jobs_challenge as { title?: unknown } | undefined + )?.title; + const superListingTitle = job.challenge?.title?.trim(); + const isSuperListing = + Boolean(superListingTitle) || + (typeof challengeTitleFromJoin === "string" && + challengeTitleFromJoin.trim().length > 0); + const isUnavailable = job?.is_active === false || job?.is_deleted === true; + const jobId = job.id ?? savedJob.job_id ?? savedJob.id; + const canOpenListing = !!jobId && !isUnavailable; + const saveButtonJob = { + ...(job ?? {}), + id: jobId ?? "", + } as Job; return ( { + if (!canOpenListing) return; + void router.push(`/search/${jobId}`); + }} + role={canOpenListing ? "button" : undefined} + tabIndex={canOpenListing ? 0 : undefined} + onKeyDown={(event) => { + if (!canOpenListing) return; + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + void router.push(`/search/${jobId}`); + }} > -
- {isSuperListing && } - +
+
+
+ {isSuperListing && } + {isUnavailable && ( + Job no longer available. + )} +
+
+ {jobId ? ( +
event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + +
+ ) : null} + {canOpenListing && ( + + + + )} +
+
+

- {savedJob.description} + {job.description}

-
- - - - -
); diff --git a/app/student/super-listing/anteriore/components/ApplyPanel.tsx b/app/student/super-listing/anteriore/components/ApplyPanel.tsx new file mode 100644 index 00000000..060aff62 --- /dev/null +++ b/app/student/super-listing/anteriore/components/ApplyPanel.tsx @@ -0,0 +1,376 @@ +"use client"; + +import { type ChangeEvent, type FormEvent, useEffect, useRef } from "react"; +import { + ArrowLeft, + FileText, + FolderOpen, + Globe, + Loader2, + Video, +} from "lucide-react"; +import { Turnstile } from "@marsidev/react-turnstile"; +import confetti from "canvas-confetti"; +import { motion, useReducedMotion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Loader } from "@/components/ui/loader"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { AnterioreSubmissionForm, SubmissionStep } from "./types"; + +type ApplyPanelProps = { + form: AnterioreSubmissionForm; + submissionStep: SubmissionStep; + hasSubmitted: boolean; + submittedEmail: string; + isSubmitting: boolean; + isError: boolean; + resultMessage: string; + isDevelopment: boolean; + token: string; + tokenFail: boolean; + turnstileSiteKey?: string; + onFieldChange: (field: keyof AnterioreSubmissionForm, value: string) => void; + onNextStep: () => void; + onBackStep: () => void; + onSubmit: (event: FormEvent) => void; + onBackToOverview: () => void; + onTokenSuccess: (token: string) => void; + onTokenError: () => void; +}; + +export function ApplyPanel({ + form, + submissionStep, + hasSubmitted, + submittedEmail, + isSubmitting, + isError, + resultMessage, + isDevelopment, + token, + tokenFail, + turnstileSiteKey, + onFieldChange, + onNextStep, + onBackStep, + onSubmit, + onBackToOverview, + onTokenSuccess, + onTokenError, +}: ApplyPanelProps) { + const prefersReduce = useReducedMotion(); + const hasCelebratedRef = useRef(false); + + useEffect(() => { + if (!hasSubmitted) { + hasCelebratedRef.current = false; + return; + } + + if (hasCelebratedRef.current || prefersReduce) return; + if (typeof window === "undefined") return; + + hasCelebratedRef.current = true; + + type ConfettiFn = ( + options?: Record, + ) => Promise | null; + const fireConfetti = confetti as unknown as ConfettiFn; + + void fireConfetti({ + particleCount: 90, + spread: 74, + startVelocity: 34, + origin: { y: 0.65 }, + colors: ["#274b7d", "#3f6aa6", "#93aed2", "#ffffff"], + }); + + window.setTimeout(() => { + void fireConfetti({ + particleCount: 60, + spread: 60, + startVelocity: 28, + origin: { x: 0.75, y: 0.68 }, + colors: ["#274b7d", "#5b84bc", "#ffffff"], + }); + }, 180); + }, [hasSubmitted, prefersReduce]); + + const stepLabel = submissionStep === 1 ? "Submission Link" : "Personal Info"; + + const updateField = + (field: keyof AnterioreSubmissionForm) => + ( + event: ChangeEvent | ChangeEvent, + ) => { + onFieldChange(field, event.target.value); + }; + + return ( +
+
+
+

+ Apply +

+
+ + {!hasSubmitted && ( +
+

+ Step {submissionStep} - {stepLabel} +

+
+ = 1 ? "bg-white" : "bg-white/30", + )} + /> + = 2 ? "bg-white" : "bg-white/30", + )} + /> +
+
+ )} +
+ +
+ {hasSubmitted ? ( + + {!prefersReduce && ( + + )} + + +

+ Submission sent +

+ + Thank you for applying. We sent a confirmation to{" "} + + {submittedEmail || "your email"} + + . We'll review your submission and get back to you in 24 hours. + + + + +
+
+ ) : ( +
void onSubmit(e)}> + {submissionStep === 1 && ( + <> +
+ + +

+ + + Google Drive + + + + Google Docs + + + + Live Demo + + + +

+
+ +
+ +