From 86d507cbef8bc4ee9fe05d4777c23236fbaed46f Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 8 Nov 2025 01:35:11 +0000 Subject: [PATCH] OpportunityDetail page - view individual opportunity cgen-fc9e4569b2a04ca0a476d425c6d55404 --- .../pages/opportunities/OpportunityDetail.tsx | 293 ++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 client/pages/opportunities/OpportunityDetail.tsx diff --git a/client/pages/opportunities/OpportunityDetail.tsx b/client/pages/opportunities/OpportunityDetail.tsx new file mode 100644 index 00000000..00a44e03 --- /dev/null +++ b/client/pages/opportunities/OpportunityDetail.tsx @@ -0,0 +1,293 @@ +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import Layout from "@/components/Layout"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { + Loader2, + ArrowLeft, + DollarSign, + Briefcase, + Clock, + Send, +} from "lucide-react"; +import { getOpportunityById, submitApplication } from "@/api/opportunities"; +import { ArmBadge } from "@/components/creator-network/ArmBadge"; +import { useAethexToast } from "@/hooks/use-aethex-toast"; +import { useAuthContext } from "@/contexts/AuthContext"; +import type { Opportunity } from "@/api/opportunities"; + +export default function OpportunityDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { user } = useAuthContext(); + const { toast } = useAethexToast(); + const [opportunity, setOpportunity] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isApplying, setIsApplying] = useState(false); + const [showApplyDialog, setShowApplyDialog] = useState(false); + const [coverLetter, setCoverLetter] = useState(""); + + useEffect(() => { + const fetchOpportunity = async () => { + if (!id) return; + setIsLoading(true); + try { + const data = await getOpportunityById(id); + setOpportunity(data); + } catch (error) { + console.error("Failed to fetch opportunity:", error); + setOpportunity(null); + } finally { + setIsLoading(false); + } + }; + + fetchOpportunity(); + }, [id]); + + const handleApply = async () => { + if (!opportunity || !user) return; + + if (!coverLetter.trim()) { + toast("Please write a cover letter", "error"); + return; + } + + setIsApplying(true); + try { + await submitApplication({ + opportunity_id: opportunity.id, + cover_letter: coverLetter.trim(), + }); + toast("Application submitted successfully!", "success"); + setCoverLetter(""); + setShowApplyDialog(false); + navigate("/profile/applications"); + } catch (error) { + toast( + error instanceof Error ? error.message : "Failed to submit application", + "error" + ); + } finally { + setIsApplying(false); + } + }; + + const formatSalary = (min?: number, max?: number) => { + if (!min && !max) return "Not specified"; + if (min && max) return `$${min.toLocaleString()} - $${max.toLocaleString()}`; + if (min) return `$${min.toLocaleString()}+`; + if (max) return `Up to $${max.toLocaleString()}`; + }; + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (!opportunity) { + return ( + +
+
+

Opportunity Not Found

+

+ The opportunity you're looking for doesn't exist. +

+ +
+
+
+ ); + } + + const poster = opportunity.aethex_creators; + + return ( + +
+ {/* Background */} +
+
+ +
+
+ {/* Header */} +
+ +
+ + {/* Opportunity Card */} + + +
+ +
+ +

+ {opportunity.title} +

+ +
+
+

Job Type

+
+ +

{opportunity.job_type}

+
+
+
+

Salary

+
+ +

+ {formatSalary( + opportunity.salary_min, + opportunity.salary_max + )} +

+
+
+
+

Experience

+

+ {opportunity.experience_level || "Any"} +

+
+
+

Posted

+
+ +

+ {new Date(opportunity.created_at).toLocaleDateString()} +

+
+
+
+ + {/* Posted By */} +
+ + + + {poster.username.charAt(0).toUpperCase()} + + +
+

Posted by

+

@{poster.username}

+
+ +
+ + {/* Description */} +
+

Description

+

+ {opportunity.description} +

+
+ + {/* Apply Button */} + {user ? ( + + ) : ( + + )} +
+
+
+
+ + {/* Apply Dialog */} + + + + Apply for {opportunity.title} + + Submit your application with a cover letter explaining why you're + interested in this opportunity. + + + +
+
+ +