Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
93 changes: 29 additions & 64 deletions app/student/search/[job_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

import React, { useCallback, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { ArrowLeft, EllipsisVertical, X } from "lucide-react";
import { Button } from "@betterinternship/components";
import { ArrowLeft, X } from "lucide-react";
import { Button, PageContainer, Card } from "@betterinternship/components";
import { useProfileData, useJobData } from "@/lib/api/student.data.api";
import { useModalRef } from "@/hooks/use-modal";
import { useMobile } from "@/hooks/use-mobile";
import { Loader } from "@/components/ui/loader";
import { JobDetails } from "@/components/shared/jobs";
import { Card } from "@/components/ui/card";
import { ApplySuccessModal } from "@/components/modals/ApplySuccessModal";
import { PageError } from "@/components/ui/error";
import { SaveJobButton } from "@/components/features/student/job/save-job-button";
Expand Down Expand Up @@ -59,7 +58,7 @@ export default function JobPage() {
<PageError title="Failed to load job" description={job.error.message} />
);

if (!job.data && !job.isPending) {
if (!job || (!job.data && !job.isPending)) {
return (
<div className="h-screen bg-white flex justify-center py-6">
<div className="flex flex-col justify-start items-start gap-4">
Expand Down Expand Up @@ -93,87 +92,53 @@ export default function JobPage() {
{job.isPending ? (
<Loader>Loading job details...</Loader>
) : (
<div className="relative w-full flex flex-col h-full bg-gray-50">
{isMobile ? (
<div className="sticky top-0 z-20 bg-white/95 backdrop-blur border-b px-4 pb-2 pt-5">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<div className="relative w-full flex flex-col h-full">
<div className="bg-white border-b border-gray-200 shrink-0">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="h-8 w-8 p-0 -ml-2 hover:bg-gray-100 rounded-full"
aria-label="Back"
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 px-3 py-2 transition-colors"
>
<ArrowLeft className="h-5 w-5 text-gray-500" />
<ArrowLeft className="w-4 h-4" />
</Button>
{!job.data?.hibernating && (
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 hover:bg-gray-100 rounded-full"
aria-label="More actions"
onClick={() => setIsActionsSheetOpen(true)}
>
<EllipsisVertical className="h-5 w-5 text-gray-500" />
</Button>
)}
</div>
</div>
) : (
<div className="bg-white border-b border-gray-200 shadow-sm flex-shrink-0">
<div className="max-w-4xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<Button
variant="ghost"
onClick={() => router.back()}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 px-3 py-2 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back
</Button>
<div className="flex flex-wrap items-center gap-3">
<ShareJobButton job={job.data} />
{job.data && !job.data.hibernating && (
<div className="flex flex-wrap items-center gap-3">
<ShareJobButton job={job.data} />
<>
<SaveJobButton job={job.data} />
<ApplyToJobButton
profile={profile.data}
job={job.data}
onApply={handleApply}
/>
</div>
</>
)}
</div>
</div>
</div>
)}
</div>

{job.data?.id && (
<div className="flex-1 overflow-y-auto">
<div
className={
isMobile
? "max-w-4xl mx-auto px-4 sm:px-6 py-4 sm:py-8 pb-[calc(env(safe-area-inset-bottom)+96px)]"
: "max-w-4xl mx-auto px-6 py-8"
}
>
{/* Job Header Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{/* Job Details Grid */}
<JobDetails
user={{
github_link: profile.data?.github_link ?? null,
portfolio_link: profile.data?.portfolio_link ?? null,
}}
job={job.data}
/>
</div>
</div>
</div>
<PageContainer>
{/* Job Header Card */}
<Card className="bg-white border border-gray-200 rounded-[0.33em] p-4 sm:p-6 overflow-hidden">
{/* Job Details Grid */}
<JobDetails
user={{
github_link: profile.data?.github_link ?? null,
portfolio_link: profile.data?.portfolio_link ?? null,
}}
job={job.data}
/>
</Card>
</PageContainer>
)}

{isMobile && job.data && !job.data.hibernating && (
<div className="absolute bottom-0 left-0 right-0 z-30 bg-white border-t p-4 pb-[calc(env(safe-area-inset-bottom)+16px)]">
<div className="max-w-4xl mx-auto flex gap-3">
<div className="max-w-7xl mx-auto flex gap-3">
<SaveJobButton job={job.data} />
<ApplyToJobButton
profile={profile.data}
Expand All @@ -193,7 +158,7 @@ export default function JobPage() {
className="absolute inset-0 bg-black/40"
onClick={() => setIsActionsSheetOpen(false)}
/>
<div className="absolute inset-x-0 bottom-0 z-50 rounded-t-2xl border-t bg-white p-4 pb-[calc(env(safe-area-inset-bottom)+16px)] shadow-xl">
<div className="absolute inset-x-0 bottom-0 z-50 rounded-t-[0.33em] border-t bg-white p-4 pb-[calc(env(safe-area-inset-bottom)+16px)] shadow-xl">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-base font-semibold text-gray-900">
Expand Down
8 changes: 0 additions & 8 deletions components/features/hire/listings/jobDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,15 @@

import { JobDetails } from "@/components/shared/jobs";
import { Card } from "@/components/ui/card";
import { useAppContext } from "@/lib/ctx-app";
import { Job } from "@/lib/db/db.types";
import { cn, PageContainer } from "@betterinternship/components";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
import { ShareJobButton } from "@/components/features/student/job/share-job-button";

interface JobDetailsPageProps {
job: Job;
}

const JobDetailsPage = ({ job }: JobDetailsPageProps) => {
const router = useRouter();
const { isMobile } = useAppContext();
const [exitingBack, setExitingBack] = useState(false);

return (
<PageContainer>
<Card>
Expand Down
124 changes: 44 additions & 80 deletions components/features/student/job/hibernating-listing-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { toast } from "sonner";
import { Bell, BellOff, CheckCircle2, Clock, HeartCrack } from "lucide-react";
import { Button } from "@betterinternship/components";
import { StatusNotice } from "@betterinternship/components/status-notice";
import { Job } from "@/lib/db/db.types";
import { cn } from "@betterinternship/components";
import { useAuthContext } from "@/lib/ctx-auth";
Expand All @@ -14,6 +16,14 @@ import { useWaitlistActions } from "@/lib/api/student.actions.api";
* alert. Copy is the single source of truth in
* Docs/plans/HIBERNATING_LISTINGS_IMPLEMENTATION_PLAN.md §7.1.
*
* Built on top of `StatusNotice` (`@betterinternship/components`). Its
* published API has no slot for a crossfading full-bleed background, so the
* hero variant below is a best-effort fit: it swaps a single background
* image per state via `className` instead of animating between two layered
* images. If that crossfade needs to come back, it means extending
* `StatusNotice` itself (e.g. a `background` prop) in Package.Components and
* publishing a new version — this file alone can't get it back.
*
* @component
*/
export const HibernatingListingBanner = ({
Expand Down Expand Up @@ -54,89 +64,43 @@ export const HibernatingListingBanner = ({

if (applied) {
return (
<div
className={cn("bg-white border-b border-gray-200 px-5 py-4", className)}
>
<p className="text-sm text-gray-700">
You&apos;ve already applied — your application is still visible to the
employer.
</p>
</div>
<StatusNotice
icon={CheckCircle2}
variant="primary"
title="You've already applied"
description="Your application is still visible to the employer."
/>
);
}

return (
<div className={cn("relative overflow-hidden", className)}>
<div
aria-hidden
className={cn(
"absolute inset-0 bg-[url('/student/images/paused-banner-bg.png')] bg-cover bg-center transition-opacity duration-700 ease-in-out pointer-events-none",
onAlert ? "opacity-0" : "opacity-100",
)}
/>
<div
aria-hidden
className={cn(
"absolute inset-0 bg-[url('/student/images/alert-banner-bg.png')] bg-cover bg-center transition-opacity duration-700 ease-in-out pointer-events-none",
onAlert ? "opacity-100" : "opacity-0",
)}
/>
<div className="relative z-10 flex flex-row px-6 py-5">
<div className="flex flex-col sm:items-start items-center sm:justify-between gap-4 w-full">
<div className="flex items-stretch gap-3 w-full">
<div
className={cn(
"hidden sm:block relative aspect-square shrink-0 overflow-hidden rounded-full m-1",
onAlert ? "bg-green-100 opacity-75" : "bg-sky-200 opacity-75",
)}
>
<img
src={
onAlert
? "/student/images/clock.png"
: "/student/images/alert.png"
}
alt=""
className="absolute inset-0 h-full w-full object-contain scale-75"
/>
</div>
<div className="w-full">
<p className="text-3xl font-bold text-white tracking-tight leading-snug">
{onAlert ? "Alert set." : "You just missed it."}
</p>
<p
className={cn(
"text-md",
onAlert ? "text-green-100" : "text-blue-100",
)}
>
{onAlert
? "We'll email you if this listing reopens."
: "This internship is no longer accepting applicants."}
</p>
</div>
</div>
<Button
disabled={pending}
onClick={() => void handleToggle()}
size={"lg"}
className={cn(
"shrink-0 rounded-[0.33em] text-gray-600 text-lg tracking-tight",
onAlert
? "bg-green-50 hover:bg-gray-200"
: "bg-sky-50 hover:bg-gray-200",
)}
>
{onAlert ? (
"🔕 Turn off alert"
) : (
<span className="inline-block animate-jitter">
🔔 Get an alert when it&apos;s back
</span>
)}
</Button>
</div>
</div>
</div>
<StatusNotice
title={onAlert ? "Alert set." : "You just missed it."}
description={
onAlert
? "We'll email you if this listing reopens."
: "This internship is no longer accepting applicants."
}
icon={onAlert ? Bell : HeartCrack}
variant={onAlert ? "primary" : "warning"}
action={
<Button
disabled={pending}
onClick={() => void handleToggle()}
className="shrink-0 rounded-[0.33em]"
variant={onAlert ? "outline" : "default"}
scheme={onAlert ? "default" : "supportive"}
>
{onAlert ? (
"Turn off alert"
) : (
<span className="animate-jitter flex gap-1 items-center">
<Bell />
Get an alert when it&apos;s back
</span>
)}
</Button>
}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function SearchResultsDesktop({
</div>

{/* Right: Details */}
<div className="w-2/3 flex flex-col overflow-hidden">
<div className="w-2/3 flex flex-col overflow-hidden p-6">
{selectedJob?.id ? (
<JobDetails
user={{
Expand Down
13 changes: 3 additions & 10 deletions components/shared/jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ export function SuperChallengeDetails({
{superChallengeTitle || "Tell us your funniest joke."}
</h3>
</div>
<div className="relative whitespace-pre-wrap break-words text-sm leading-relaxed text-gray-700">
<div className="relative whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-gray-700">
{superChallengeDescription || "No challenge description provided."}
</div>
</div>
Expand Down Expand Up @@ -833,17 +833,10 @@ export function JobDetails({
const missingRequired =
(needsGithub && !hasGithub) || (needsPortfolio && !hasPortfolio);

const { isMobile } = useAppContext();

return (
<>
{job.hibernating && <HibernatingListingBanner job={job} />}
<div
className={cn(
"flex-1 overflow-y-auto space-y-5",
isMobile ? "px-3 py-4" : "px-8 pt-7",
)}
>
<div className={cn("flex-1 overflow-y-auto space-y-5")}>
{job.hibernating && <HibernatingListingBanner job={job} />}
<HeaderWithActions
job={job}
actions={job.hibernating ? [] : actions}
Expand Down