From c5b2ee974e388ec4e7f2f40aa2a7280322aab058 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 15 Nov 2025 09:24:48 +0000 Subject: [PATCH] Create ApplicationsWidget component for NEXUS cgen-56c47a3f5a8549c59a2211cc7dd18fd1 --- client/components/ApplicationsWidget.tsx | 235 +++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 client/components/ApplicationsWidget.tsx diff --git a/client/components/ApplicationsWidget.tsx b/client/components/ApplicationsWidget.tsx new file mode 100644 index 00000000..b7524aea --- /dev/null +++ b/client/components/ApplicationsWidget.tsx @@ -0,0 +1,235 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Briefcase, AlertCircle, CheckCircle, Clock, ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export interface Application { + id: string; + title?: string; + opportunity?: { + id: string; + title: string; + description?: string; + category?: string; + budget?: number; + }; + status: "submitted" | "accepted" | "rejected" | "interview" | "pending"; + proposed_rate?: number; + proposed_rate_type?: "hourly" | "fixed" | "percentage"; + cover_letter?: string; + created_at?: string; + user?: { + id: string; + name: string; + avatar_url?: string; + }; +} + +interface ApplicationsWidgetProps { + applications: Application[]; + title?: string; + description?: string; + accentColor?: "purple" | "blue" | "cyan" | "green" | "amber" | "red"; + onViewDetails?: (application: Application) => void; + showCTA?: boolean; + ctaText?: string; + onCTA?: () => void; +} + +const colorMap = { + purple: { + bg: "bg-gradient-to-br from-purple-950/40 to-purple-900/20", + border: "border-purple-500/20", + text: "text-purple-300", + }, + blue: { + bg: "bg-gradient-to-br from-blue-950/40 to-blue-900/20", + border: "border-blue-500/20", + text: "text-blue-300", + }, + cyan: { + bg: "bg-gradient-to-br from-cyan-950/40 to-cyan-900/20", + border: "border-cyan-500/20", + text: "text-cyan-300", + }, + green: { + bg: "bg-gradient-to-br from-green-950/40 to-green-900/20", + border: "border-green-500/20", + text: "text-green-300", + }, + amber: { + bg: "bg-gradient-to-br from-amber-950/40 to-amber-900/20", + border: "border-amber-500/20", + text: "text-amber-300", + }, + red: { + bg: "bg-gradient-to-br from-red-950/40 to-red-900/20", + border: "border-red-500/20", + text: "text-red-300", + }, +}; + +const statusMap = { + submitted: { color: "bg-blue-600/50 text-blue-100", icon: Clock, label: "Submitted" }, + pending: { color: "bg-yellow-600/50 text-yellow-100", icon: Clock, label: "Pending" }, + accepted: { color: "bg-green-600/50 text-green-100", icon: CheckCircle, label: "Accepted" }, + rejected: { color: "bg-red-600/50 text-red-100", icon: AlertCircle, label: "Rejected" }, + interview: { color: "bg-purple-600/50 text-purple-100", icon: Briefcase, label: "Interview" }, +}; + +export function ApplicationsWidget({ + applications, + title = "My Applications", + description = "Track all your job applications and bids", + accentColor = "purple", + onViewDetails, + showCTA = false, + ctaText = "Browse More Opportunities", + onCTA, +}: ApplicationsWidgetProps) { + const colors = colorMap[accentColor]; + const statusCounts = { + submitted: applications.filter(a => a.status === "submitted").length, + pending: applications.filter(a => a.status === "pending").length, + accepted: applications.filter(a => a.status === "accepted").length, + rejected: applications.filter(a => a.status === "rejected").length, + interview: applications.filter(a => a.status === "interview").length, + }; + + return ( + + + + + {title} + + {description} + + + {applications.length === 0 ? ( +
+ +

No applications yet

+ {showCTA && onCTA && ( + + )} +
+ ) : ( +
+ {/* Status Summary */} + {Object.entries(statusCounts).filter(([_, count]) => count > 0).length > 0 && ( +
+ {statusCounts.submitted > 0 && ( +
+

Submitted

+

{statusCounts.submitted}

+
+ )} + {statusCounts.interview > 0 && ( +
+

Interview

+

{statusCounts.interview}

+
+ )} + {statusCounts.accepted > 0 && ( +
+

Accepted

+

{statusCounts.accepted}

+
+ )} + {statusCounts.pending > 0 && ( +
+

Pending

+

{statusCounts.pending}

+
+ )} + {statusCounts.rejected > 0 && ( +
+

Rejected

+

{statusCounts.rejected}

+
+ )} +
+ )} + + {/* Applications List */} +
+ {applications.map((app) => { + const statusInfo = statusMap[app.status]; + const StatusIcon = statusInfo.icon; + const rateDisplay = app.proposed_rate + ? `$${app.proposed_rate.toLocaleString()}${app.proposed_rate_type === "hourly" ? "/hr" : ""}` + : "N/A"; + + return ( +
onViewDetails?.(app)} + > +
+
+

+ {app.opportunity?.title || app.title || "Untitled Opportunity"} +

+ {app.opportunity?.category && ( +

{app.opportunity.category}

+ )} +
+ + + {statusInfo.label} + +
+ + {app.opportunity?.description && ( +

+ {app.opportunity.description} +

+ )} + +
+ + Proposed: {rateDisplay} + + {app.created_at && ( + + {new Date(app.created_at).toLocaleDateString()} + + )} +
+ + {app.cover_letter && ( +

+ "{app.cover_letter.substring(0, 100)}..." +

+ )} +
+ ); + })} +
+ + {showCTA && onCTA && applications.length > 0 && ( + + )} +
+ )} +
+
+ ); +} + +export default ApplicationsWidget;