1→"use client";
     2→
     3→import { useEffect, useState, useRef } from "react";
     4→import Link from "next/link";
     5→import { createClient } from "@/lib/supabase/client";
     6→import { Card, CardContent } from "@/components/ui/card";
     7→import { Badge } from "@/components/ui/badge";
     8→import { Button } from "@/components/ui/button";
     9→import { BottomNav } from "@/components/bottom-nav";
    10→import {
    11→  ArrowLeft,
    12→  Loader2,
    13→  Star,
    14→  MapPin,
    15→  Clock,
    16→  CheckCircle2,
    17→  UserCheck,
    18→  Briefcase,
    19→  AlertCircle,
    20→} from "lucide-react";
    21→import type { Job } from "@/lib/types";
    22→
    23→interface MatchWithWorker {
    24→  id: string;
    25→  job_id: string;
    26→  worker_id: string;
    27→  status: string;
    28→  match_score: number | null;
    29→  ai_summary: string | null;
    30→  contractor_action: string;
    31→  worker_action: string;
    32→  hired_at: string | null;
    33→  fee_charged: boolean;
    34→  created_at: string;
    35→  worker: {
    36→    id: string;
    37→    name: string | null;
    38→    suburb: string | null;
    39→    avatar_url: string | null;
    40→  } | null;
    41→  worker_profile: {
    42→    trades: string[];
    43→    experience_years: number | null;
    44→    certifications: string[];
    45→    availability: string | null;
    46→  } | null;
    47→}
    48→
    49→interface SwipeableMatchCardProps {
    50→  match: MatchWithWorker;
    51→  isHired: boolean;
    52→  justHired: boolean;
    53→  hiring: boolean;
    54→  onHire: (match: MatchWithWorker) => void;
    55→  onReject: (match: MatchWithWorker) => void;
    56→}
    57→
    58→function SwipeableMatchCard({ match, isHired, justHired, hiring, onHire, onReject }: SwipeableMatchCardProps) {
    59→  const [swipeX, setSwipeX] = useState(0);
    60→  const [swiping, setSwiping] = useState(false);
    61→  const startX = useRef(0);
    62→  const currentX = useRef(0);
    63→
    64→  const summary = match.ai_summary || generateAiSummary(match.worker, match.worker_profile);
    65→
    66→  const handleTouchStart = (e: React.TouchEvent) => {
    67→    if (isHired) return;
    68→    startX.current = e.touches[0].clientX;
    69→    setSwiping(true);
    70→  };
    71→
    72→  const handleTouchMove = (e: React.TouchEvent) => {
    73→    if (!swiping || isHired) return;
    74→    currentX.current = e.touches[0].clientX;
    75→    const diff = currentX.current - startX.current;
    76→    // Allow both directions
    77→    setSwipeX(Math.max(-150, Math.min(diff, 150)));
    78→  };
    79→
    80→  const handleTouchEnd = () => {
    81→    if (!swiping || isHired) return;
    82→    setSwiping(false);
    83→
    84→    // If swiped right > 100px, hire
    85→    if (swipeX > 100) {
    86→      onHire(match);
    87→    }
    88→    // If swiped left > 100px, reject
    89→    else if (swipeX < -100) {
    90→      onReject(match);
    91→    }
    92→
    93→    // Reset
    94→    setSwipeX(0);
    95→  };
    96→
    97→  const swipeProgress = Math.abs(swipeX) / 100;
    98→  const swipeDirection = swipeX > 0 ? "right" : "left";
    99→
   100→  return (
   101→

   102→      {/* Swipe indicator backgrounds */}
   103→      {!isHired && swipeX !== 0 && (
   104→        <>
   105→          {/* Right swipe (hire) */}
   106→          {swipeX > 0 && (
   107→

   114→              <UserCheck className="h-8 w-8 text-green-600" />
   115→
   116→                {swipeProgress >= 1 ? "Release to hire" : "Swipe to hire"}
   117→
   118→

   119→          )}
   120→          {/* Left swipe (reject) */}
   121→          {swipeX < 0 && (
   122→

   129→
   130→                {swipeProgress >= 1 ? "Release to pass" : "Swipe to pass"}
   131→
   132→              <AlertCircle className="h-8 w-8 text-red-600" />
   133→

   134→          )}
   135→        </>
   136→      )}
   137→
   138→      <Card
   139→        className={`transition-all touch-pan-y ${justHired ? "ring-2 ring-green-500" : ""}`}
   140→        style={{
   141→          transform: `translateX(${swipeX}px)`,
   142→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   143→        }}
   144→        onTouchStart={handleTouchStart}
   145→        onTouchMove={handleTouchMove}
   146→        onTouchEnd={handleTouchEnd}
   147→      >
   148→        <CardContent className="p-5">
   149→

   150→            {/* Avatar */}
   151→

   152→              {match.worker?.name?.[0]?.toUpperCase() || "?"}
   153→

   154→
   155→

   156→

   157→

   158→                  {match.worker?.name || "Worker"}
   159→

   160→                {isHired && (
   161→                  <Badge className="bg-green-600 text-xs shrink-0">Hired</Badge>
   162→                )}
   163→

   164→
   165→              {/* AI Summary */}
   166→

   167→                {summary}
   168→

   169→
   170→              {/* Details */}
   171→

   172→                {match.worker_profile?.experience_years && (
   173→
   174→                    <Clock className="h-4 w-4" />
   175→                    {match.worker_profile.experience_years}yr exp
   176→
   177→                )}
   178→                {match.worker?.suburb && (
   179→
   180→                    <MapPin className="h-4 w-4" />
   181→                    {match.worker.suburb}
   182→
   183→                )}
   184→                {match.match_score && (
   185→
   186→                    <Star className="h-4 w-4 fill-amber-400 text-amber-400" />
   187→                    {Math.round(match.match_score)}% match
   188→
   189→                )}
   190→

   191→
   192→              {/* Certs */}
   193→              {match.worker_profile?.certifications && match.worker_profile.certifications.length > 0 && (
   194→

   195→                  {match.worker_profile.certifications.slice(0, 3).map((cert) => (
   196→                    <Badge key={cert} variant="outline" className="text-xs px-2 py-1">
   197→                      {cert}
   198→                    </Badge>
   199→                  ))}
   200→

   201→              )}
   202→

   203→

   204→
   205→          {/* Action */}
   206→          {!isHired && (
   207→            <Button
   208→              onClick={() => onHire(match)}
   209→              className="mt-4 w-full h-14 text-base font-semibold"
   210→              disabled={hiring}
   211→            >
   212→              {hiring ? (
   213→                <>
   214→                  <Loader2 className="mr-2 h-5 w-5 animate-spin" />
   215→                  Processing...
   216→                </>
   217→              ) : (
   218→                <>
   219→                  <UserCheck className="mr-2 h-5 w-5" />
   220→                  Hire — $50 Fee
   221→                </>
   222→              )}
   223→            </Button>
   224→          )}
   225→
   226→          {justHired && (
   227→

   228→              <CheckCircle2 className="h-5 w-5" />
   229→              Hired! Worker has been notified.
   230→

   231→          )}
   232→
   233→          {isHired && !justHired && (
   234→

   235→              <CheckCircle2 className="h-5 w-5 text-green-600" />
   236→              Hired on{" "}
   237→              {match.hired_at
   238→                ? new Date(match.hired_at).toLocaleDateString("en-AU", {
   239→                    day: "numeric",
   240→                    month: "short",
   241→                  })
   242→                : "recently"}
   243→

   244→          )}
   245→        </CardContent>
   246→      </Card>
   247→

   248→  );
   249→}
   250→
   251→const TRADE_LABEL: Record<string, string> = {
   252→  "steel-fixer": "Steel Fixer",
   253→  "formworker": "Formworker",
   254→  "labourer": "Labourer",
   255→  "concreter": "Concreter",
   256→  "carpenter": "Carpenter",
   257→  "scaffolder": "Scaffolder",
   258→  "other": "Other",
   259→};
   260→
   261→function generateAiSummary(
   262→  worker: MatchWithWorker["worker"],
   263→  profile: MatchWithWorker["worker_profile"]
   264→): string {
   265→  const parts: string[] = [];
   266→  if (profile?.trades?.length) {
   267→    parts.push(profile.trades.map((t) => TRADE_LABEL[t] || t).join(", "));
   268→  }
   269→  if (profile?.experience_years) {
   270→    parts.push(`${profile.experience_years} years exp`);
   271→  }
   272→  if (worker?.suburb) {
   273→    parts.push(worker.suburb);
   274→  }
   275→  if (profile?.availability) {
   276→    parts.push(profile.availability);
   277→  }
   278→  if (profile?.certifications?.length) {
   279→    parts.push(profile.certifications.slice(0, 2).join(", "));
   280→  }
   281→  return parts.join(" \u00B7 ") || "Worker profile";
   282→}
   283→
   284→export default function ContractorMatchesPage() {
   285→  const supabase = createClient();
   286→  const [jobs, setJobs] = useState<Job[]>([]);
   287→  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
   288→  const [matches, setMatches] = useState<MatchWithWorker[]>([]);
   289→  const [loading, setLoading] = useState(true);
   290→  const [loadingMatches, setLoadingMatches] = useState(false);
   291→  const [hiring, setHiring] = useState<string | null>(null);
   292→  const [hireSuccess, setHireSuccess] = useState<string | null>(null);
   293→  const [error, setError] = useState("");
   294→
   295→  // Load contractor's jobs
   296→  useEffect(() => {
   297→    async function load() {
   298→      const { data: { user } } = await supabase.auth.getUser();
   299→      if (!user) return;
   300→
   301→      const { data: company } = await supabase
   302→        .from("companies")
   303→        .select("id")
   304→        .eq("profile_id", user.id)
   305→        .single();
   306→
   307→      if (!company) {
   308→        setLoading(false);
   309→        return;
   310→      }
   311→
   312→      const { data: jobsData } = await supabase
   313→        .from("jobs")
   314→        .select("*")
   315→        .eq("company_id", company.id)
   316→        .in("status", ["active", "filled"])
   317→        .order("created_at", { ascending: false });
   318→
   319→      if (jobsData && jobsData.length > 0) {
   320→        setJobs(jobsData);
   321→        setSelectedJobId(jobsData[0].id);
   322→      }
   323→      setLoading(false);
   324→    }
   325→    load();
   326→  }, [supabase]);
   327→
   328→  // Load matches for selected job
   329→  useEffect(() => {
   330→    if (!selectedJobId) return;
   331→    async function loadMatches() {
   332→      setLoadingMatches(true);
   333→
   334→      // Get matches for this job
   335→      const { data: matchData } = await supabase
   336→        .from("matches")
   337→        .select("*")
   338→        .eq("job_id", selectedJobId)
   339→        .order("match_score", { ascending: false });
   340→
   341→      if (!matchData || matchData.length === 0) {
   342→        setMatches([]);
   343→        setLoadingMatches(false);
   344→        return;
   345→      }
   346→
   347→      // Fetch worker profiles and base profiles
   348→      const workerIds = matchData.map((m: { worker_id: string }) => m.worker_id);
   349→
   350→      const [{ data: profiles }, { data: workerProfiles }] = await Promise.all([
   351→        supabase.from("profiles").select("id, name, suburb, avatar_url").in("id", workerIds),
   352→        supabase.from("worker_profiles").select("profile_id, trades, experience_years, certifications, availability").in("profile_id", workerIds),
   353→      ]);
   354→
   355→      type ProfileRow = { id: string; name: string | null; suburb: string | null; avatar_url: string | null };
   356→      type WPRow = { profile_id: string; trades: string[]; experience_years: number | null; certifications: string[]; availability: string | null };
   357→
   358→      const profileMap = new Map((profiles || []).map((p: ProfileRow) => [p.id, p]));
   359→      const wpMap = new Map((workerProfiles || []).map((w: WPRow) => [w.profile_id, w]));
   360→
   361→      const enriched: MatchWithWorker[] = matchData.map((m: Record<string, unknown>) => ({
   362→        id: m.id as string,
   363→        job_id: m.job_id as string,
   364→        worker_id: m.worker_id as string,
   365→        status: m.status as string,
   366→        match_score: m.match_score as number | null,
   367→        ai_summary: m.ai_summary as string | null,
   368→        contractor_action: m.contractor_action as string,
   369→        worker_action: m.worker_action as string,
   370→        hired_at: m.hired_at as string | null,
   371→        fee_charged: m.fee_charged as boolean,
   372→        created_at: m.created_at as string,
   373→        worker: profileMap.get(m.worker_id as string) || null,
   374→        worker_profile: wpMap.get(m.worker_id as string) || null,
   375→      }));
   376→
   377→      setMatches(enriched);
   378→      setLoadingMatches(false);
   379→    }
   380→    loadMatches();
   381→  }, [selectedJobId, supabase]);
   382→
   383→  async function handleHire(match: MatchWithWorker) {
   384→    setHiring(match.id);
   385→    setError("");
   386→
   387→    // Haptic feedback
   388→    if (navigator.vibrate) {
   389→      navigator.vibrate(50);
   390→    }
   391→
   392→    try {
   393→      const { data: { user } } = await supabase.auth.getUser();
   394→      if (!user) return;
   395→
   396→      // Get company for payment
   397→      const { data: company } = await supabase
   398→        .from("companies")
   399→        .select("id")
   400→        .eq("profile_id", user.id)
   401→        .single();
   402→
   403→      if (!company) {
   404→        setError("Company not found.");
   405→        setHiring(null);
   406→        return;
   407→      }
   408→
   409→      // Update match to hired
   410→      const { error: matchError } = await supabase
   411→        .from("matches")
   412→        .update({
   413→          status: "hired",
   414→          contractor_action: "accepted",
   415→          hired_at: new Date().toISOString(),
   416→          fee_charged: true,
   417→        })
   418→        .eq("id", match.id);
   419→
   420→      if (matchError) {
   421→        setError(matchError.message);
   422→        setHiring(null);
   423→        return;
   424→      }
   425→
   426→      // Create payment record
   427→      await supabase.from("payments").insert({
   428→        match_id: match.id,
   429→        company_id: company.id,
   430→        amount: 50.0,
   431→        status: "pending",
   432→      });
   433→
   434→      // Update local state
   435→      setMatches((prev) =>
   436→        prev.map((m) =>
   437→          m.id === match.id
   438→            ? { ...m, status: "hired", contractor_action: "accepted", hired_at: new Date().toISOString(), fee_charged: true }
   439→            : m
   440→        )
   441→      );
   442→
   443→      setHireSuccess(match.id);
   444→      setTimeout(() => setHireSuccess(null), 3000);
   445→    } catch {
   446→      setError("Something went wrong. Please try again.");
   447→    }
   448→
   449→    setHiring(null);
   450→  }
   451→
   452→  async function handleReject(match: MatchWithWorker) {
   453→    // Haptic feedback
   454→    if (navigator.vibrate) {
   455→      navigator.vibrate([30, 20, 30]);
   456→    }
   457→
   458→    // Remove from list (soft delete - just update status)
   459→    await supabase
   460→      .from("matches")
   461→      .update({
   462→        contractor_action: "rejected",
   463→      })
   464→      .eq("id", match.id);
   465→
   466→    // Remove from UI
   467→    setMatches((prev) => prev.filter((m) => m.id !== match.id));
   468→  }
   469→
   470→  if (loading) {
   471→    return (
   472→

   473→        <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
   474→

   475→    );
   476→  }
   477→
   478→  return (
   479→

   480→      {/* Header */}
   481→
   482→

   483→          <Link
   484→            href="/dashboard"
   485→            className="mr-3 flex h-12 w-12 items-center justify-center rounded-full active:bg-muted"
   486→          >
   487→            <ArrowLeft className="h-6 w-6" />
   488→          </Link>
   489→
Matches

   490→

   491→
   492→
   493→
   494→        {jobs.length === 0 ? (
   495→

   496→            <Briefcase className="h-16 w-16 text-muted-foreground/50" />
   497→
No jobs posted yet

   498→

   499→              Post a job to start seeing matched workers.
   500→

   501→            <Button asChild className="mt-8 h-16 px-8 text-lg font-semibold">
   502→              <Link href="/contractor/post-job">Post a Job</Link>
   503→            </Button>
   504→

   505→        ) : (
   506→          <>
   507→            {/* Job selector — horizontal scroll */}
   508→            {jobs.length > 1 && (
   509→

   510→                {jobs.map((job) => (
   511→                  <button
   512→                    key={job.id}
   513→                    onClick={() => setSelectedJobId(job.id)}
   514→                    className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   515→                      selectedJobId === job.id
   516→                        ? "bg-primary text-primary-foreground"
   517→                        : "bg-muted text-muted-foreground"
   518→                    }`}
   519→                  >
   520→                    {job.title}
   521→                  </button>
   522→                ))}
   523→

   524→            )}
   525→
   526→            {/* Selected job summary */}
   527→            {selectedJobId && (
   528→

   529→                {(() => {
   530→                  const job = jobs.find((j) => j.id === selectedJobId);
   531→                  if (!job) return null;
   532→                  return (
   533→

   534→                      <Badge variant="secondary">{TRADE_LABEL[job.trade] || job.trade}</Badge>
   535→                      {job.suburb && (
   536→
   537→                          <MapPin className="h-3.5 w-3.5" />
   538→                          {job.suburb}
   539→
   540→                      )}
   541→
   542→                        {matches.length} match{matches.length !== 1 ? "es" : ""}
   543→
   544→

   545→                  );
   546→                })()}
   547→

   548→            )}
   549→
   550→            {/* Matches */}
   551→            {loadingMatches ? (
   552→

   553→                <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
   554→

   555→            ) : matches.length === 0 ? (
   556→

   557→                <UserCheck className="h-12 w-12 text-muted-foreground/50" />
   558→
No matches yet

   559→

   560→                  Workers will show up here as they apply. We'll notify you.
   561→

   562→

   563→            ) : (
   564→

   565→                {error && (
   566→

   567→                    <AlertCircle className="h-4 w-4 shrink-0" />
   568→                    {error}
   569→

   570→                )}
   571→
   572→                {/* Swipe hint */}
   573→                {matches.length > 0 && matches.some(m => m.status !== "hired") && (
   574→

   575→

   576→                      👉 Swipe right to hire • Swipe left to pass
   577→

   578→

   579→                )}
   580→
   581→                {matches.map((match) => (
   582→                  <SwipeableMatchCard
   583→                    key={match.id}
   584→                    match={match}
   585→                    isHired={match.status === "hired"}
   586→                    justHired={hireSuccess === match.id}
   587→                    hiring={hiring === match.id}
   588→                    onHire={handleHire}
   589→                    onReject={handleReject}
   590→                  />
   591→                ))}
   592→

   593→            )}
   594→          </>
   595→        )}
   596→
   597→
   598→      <BottomNav userType="contractor" />
   599→

   600→  );
   601→}
   602→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import { createClient } from "@/lib/supabase/client";
     7→import { Button } from "@/components/ui/button";
     8→import { Input } from "@/components/ui/input";
     9→import { Label } from "@/components/ui/label";
    10→import { Card, CardContent } from "@/components/ui/card";
    11→import {
    12→  HardHat,
    13→  ArrowLeft,
    14→  ArrowRight,
    15→  Loader2,
    16→  CheckCircle2,
    17→  Sparkles,
    18→  MapPin,
    19→  Clock,
    20→  Shield,
    21→  Edit3,
    22→  Wrench,
    23→  Calendar,
    24→  Zap,
    25→} from "lucide-react";
    26→
    27→type Step = "trade" | "experience" | "location" | "availability" | "generating" | "review";
    28→
    29→const TRADES = [
    30→  "Labourer",
    31→  "Steel Fixer",
    32→  "Formworker",
    33→  "Carpenter",
    34→  "Electrician",
    35→  "Plumber",
    36→  "Concreter",
    37→  "Bricklayer",
    38→  "Painter",
    39→  "Roofer",
    40→  "Scaffolder",
    41→  "Tiler",
    42→  "Plasterer",
    43→  "Welder",
    44→  "Crane Operator",
    45→  "Excavator Operator",
    46→];
    47→
    48→const EXPERIENCE_OPTIONS = [
    49→  { label: "Less than 1 year", value: 0 },
    50→  { label: "1-3 years", value: 2 },
    51→  { label: "3-5 years", value: 4 },
    52→  { label: "5-10 years", value: 7 },
    53→  { label: "10+ years", value: 12 },
    54→];
    55→
    56→const AVAILABILITY_OPTIONS = [
    57→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    58→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    59→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    60→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    61→];
    62→
    63→interface ProfileData {
    64→  trade: string;
    65→  experience: number;
    66→  location: string;
    67→  availability: "full-time" | "casual" | "weekends" | "flexible";
    68→}
    69→
    70→interface GeneratedProfile {
    71→  bio: string;
    72→  skills: string[];
    73→  certifications: string[];
    74→}
    75→
    76→export default function ProfileBuildPage() {
    77→  const [step, setStep] = useState<Step>("trade");
    78→  const [loading, setLoading] = useState(true);
    79→  const [generating, setGenerating] = useState(false);
    80→  const [saving, setSaving] = useState(false);
    81→
    82→  // Form data
    83→  const [trade, setTrade] = useState<string>("");
    84→  const [experience, setExperience] = useState<number | null>(null);
    85→  const [location, setLocation] = useState("");
    86→  const [availability, setAvailability] = useState<ProfileData["availability"] | null>(null);
    87→
    88→  // Generated data
    89→  const [generatedBio, setGeneratedBio] = useState("");
    90→  const [generatedSkills, setGeneratedSkills] = useState<string[]>([]);
    91→  const [generatedCerts, setGeneratedCerts] = useState<string[]>([]);
    92→
    93→  // Editable states
    94→  const [editedBio, setEditedBio] = useState("");
    95→  const [editedSkills, setEditedSkills] = useState<string[]>([]);
    96→  const [editedCerts, setEditedCerts] = useState<string[]>([]);
    97→
    98→  const router = useRouter();
    99→  const supabase = createClient();
   100→
   101→  useEffect(() => {
   102→    async function checkAuth() {
   103→      const {
   104→        data: { user },
   105→      } = await supabase.auth.getUser();
   106→      if (!user) {
   107→        router.push("/auth/login");
   108→        return;
   109→      }
   110→      setLoading(false);
   111→    }
   112→    checkAuth();
   113→  }, [supabase.auth, router]);
   114→
   115→  async function generateProfile() {
   116→    if (!trade || experience === null || !location || !availability) return;
   117→
   118→    setGenerating(true);
   119→    setStep("generating");
   120→
   121→    try {
   122→      const response = await fetch("/api/ai/generate-profile", {
   123→        method: "POST",
   124→        headers: { "Content-Type": "application/json" },
   125→        body: JSON.stringify({
   126→          trade,
   127→          experience,
   128→          location,
   129→          availability,
   130→        }),
   131→      });
   132→
   133→      if (!response.ok) {
   134→        throw new Error("Failed to generate profile");
   135→      }
   136→
   137→      const data: GeneratedProfile = await response.json();
   138→
   139→      setGeneratedBio(data.bio);
   140→      setGeneratedSkills(data.skills);
   141→      setGeneratedCerts(data.certifications);
   142→
   143→      // Initialize editable versions
   144→      setEditedBio(data.bio);
   145→      setEditedSkills(data.skills);
   146→      setEditedCerts(data.certifications);
   147→
   148→      setStep("review");
   149→    } catch (error) {
   150→      console.error("Generation error:", error);
   151→      alert("Failed to generate profile. Please try again.");
   152→      setStep("availability");
   153→    } finally {
   154→      setGenerating(false);
   155→    }
   156→  }
   157→
   158→  async function handleSave() {
   159→    setSaving(true);
   160→
   161→    try {
   162→      const {
   163→        data: { user },
   164→      } = await supabase.auth.getUser();
   165→      if (!user) throw new Error("Not authenticated");
   166→
   167→      // Update or create worker profile
   168→      const { data: existingProfile } = await supabase
   169→        .from("worker_profiles")
   170→        .select("id")
   171→        .eq("profile_id", user.id)
   172→        .single();
   173→
   174→      const profileData = {
   175→        profile_id: user.id,
   176→        trades: [trade],
   177→        experience_years: experience,
   178→        availability,
   179→        certifications: editedCerts,
   180→        bio: editedBio,
   181→        ai_extracted: {
   182→          skills: editedSkills,
   183→          generated_at: new Date().toISOString(),
   184→          original_bio: generatedBio,
   185→          original_skills: generatedSkills,
   186→          original_certs: generatedCerts,
   187→        },
   188→      };
   189→
   190→      if (existingProfile) {
   191→        await supabase
   192→          .from("worker_profiles")
   193→          .update(profileData)
   194→          .eq("profile_id", user.id);
   195→      } else {
   196→        await supabase.from("worker_profiles").insert(profileData);
   197→      }
   198→
   199→      // Update main profile
   200→      await supabase
   201→        .from("profiles")
   202→        .update({
   203→          suburb: location,
   204→          onboarding_completed: true,
   205→        })
   206→        .eq("id", user.id);
   207→
   208→      router.push("/dashboard");
   209→      router.refresh();
   210→    } catch (error) {
   211→      console.error("Save error:", error);
   212→      alert("Failed to save profile. Please try again.");
   213→    } finally {
   214→      setSaving(false);
   215→    }
   216→  }
   217→
   218→  const stepOrder: Step[] = ["trade", "experience", "location", "availability", "generating", "review"];
   219→  const currentIndex = stepOrder.indexOf(step);
   220→  const totalSteps = 4; // Only count user-facing steps
   221→
   222→  if (loading) {
   223→    return (
   224→

   225→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   226→

   227→    );
   228→  }
   229→
   230→  return (
   231→

   232→      {/* Header */}
   233→
   234→

   235→          <Link href="/dashboard" className="flex items-center gap-2">
   236→

   237→              <HardHat className="h-5 w-5 text-primary-foreground" />
   238→

   239→            RateRight
   240→          </Link>
   241→

   242→
   243→
   244→
   245→        {/* Progress bar */}
   246→        {step !== "generating" && step !== "review" && (
   247→

   248→            {Array.from({ length: totalSteps }, (_, i) => (
   249→              <div
   250→                key={i}
   251→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   252→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   253→                }`}
   254→              />
   255→            ))}
   256→

   257→        )}
   258→
   259→        {/* Step: Trade */}
   260→        {step === "trade" && (
   261→

   262→            <StepHeader
   263→              icon={<Wrench className="h-5 w-5" />}
   264→              title="What's your trade?"
   265→              subtitle="Pick your primary trade"
   266→            />
   267→
   268→

   269→              {TRADES.map((t) => (
   270→                <button
   271→                  key={t}
   272→                  onClick={() => {
   273→                    setTrade(t);
   274→                    setStep("experience");
   275→                  }}
   276→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   277→                    trade === t
   278→                      ? "border-primary bg-primary/10 text-primary"
   279→                      : "border-border hover:border-primary/50"
   280→                  }`}
   281→                  style={{ minHeight: "64px" }}
   282→                >
   283→                  {t}
   284→                </button>
   285→              ))}
   286→

   287→

   288→        )}
   289→
   290→        {/* Step: Experience */}
   291→        {step === "experience" && (
   292→

   293→            <StepHeader
   294→              icon={<Clock className="h-5 w-5" />}
   295→              title="Your experience?"
   296→              subtitle="Years in construction"
   297→            />
   298→
   299→

   300→              {EXPERIENCE_OPTIONS.map((opt) => (
   301→                <button
   302→                  key={opt.value}
   303→                  onClick={() => {
   304→                    setExperience(opt.value);
   305→                    setStep("location");
   306→                  }}
   307→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 text-left text-base font-medium transition-colors active:scale-95 ${
   308→                    experience === opt.value
   309→                      ? "border-primary bg-primary/10 text-primary"
   310→                      : "border-border hover:border-primary/50"
   311→                  }`}
   312→                  style={{ minHeight: "64px" }}
   313→                >
   314→                  {opt.label}
   315→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   316→                </button>
   317→              ))}
   318→

   319→
   320→            <button
   321→              onClick={() => setStep("trade")}
   322→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   323→            >
   324→              <ArrowLeft className="h-4 w-4" />
   325→              Back
   326→            </button>
   327→

   328→        )}
   329→
   330→        {/* Step: Location */}
   331→        {step === "location" && (
   332→

   333→            <StepHeader
   334→              icon={<MapPin className="h-5 w-5" />}
   335→              title="Where are you based?"
   336→              subtitle="Your suburb or area"
   337→            />
   338→
   339→            <form
   340→              onSubmit={(e) => {
   341→                e.preventDefault();
   342→                if (location.trim()) setStep("availability");
   343→              }}
   344→              className="mt-6 space-y-4"
   345→            >
   346→

   347→                Suburb or Area
   348→                <Input
   349→                  id="location"
   350→                  placeholder="e.g. Parramatta, Blacktown"
   351→                  value={location}
   352→                  onChange={(e) => setLocation(e.target.value)}
   353→                  required
   354→                  autoFocus
   355→                  className="h-14 text-base"
   356→                />
   357→

   358→

   359→                We'll match you to nearby jobs
   360→

   361→
   362→

   363→                <Button
   364→                  type="button"
   365→                  variant="outline"
   366→                  onClick={() => setStep("experience")}
   367→                  className="h-14 w-14 p-0"
   368→                >
   369→                  <ArrowLeft className="h-5 w-5" />
   370→                </Button>
   371→                <Button
   372→                  type="submit"
   373→                  disabled={!location.trim()}
   374→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   375→                >
   376→                  Continue
   377→                  <ArrowRight className="h-5 w-5" />
   378→                </Button>
   379→

   380→            </form>
   381→

   382→        )}
   383→
   384→        {/* Step: Availability */}
   385→        {step === "availability" && (
   386→

   387→            <StepHeader
   388→              icon={<Calendar className="h-5 w-5" />}
   389→              title="Your availability?"
   390→              subtitle="When can you work?"
   391→            />
   392→
   393→

   394→              {AVAILABILITY_OPTIONS.map((opt) => (
   395→                <button
   396→                  key={opt.value}
   397→                  onClick={() => {
   398→                    setAvailability(opt.value);
   399→                    generateProfile();
   400→                  }}
   401→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   402→                    availability === opt.value
   403→                      ? "border-primary bg-primary/10"
   404→                      : "border-border hover:border-primary/50"
   405→                  }`}
   406→                  style={{ minHeight: "64px" }}
   407→                >
   408→

   409→                    <p className={`text-base font-semibold ${availability === opt.value ? "text-primary" : ""}`}>
   410→                      {opt.label}
   411→

   412→
{opt.desc}

   413→

   414→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   415→                </button>
   416→              ))}
   417→

   418→
   419→            <button
   420→              onClick={() => setStep("location")}
   421→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   422→            >
   423→              <ArrowLeft className="h-4 w-4" />
   424→              Back
   425→            </button>
   426→

   427→        )}
   428→
   429→        {/* Step: Generating */}
   430→        {step === "generating" && (
   431→

   432→

   433→              <Sparkles className="h-10 w-10 animate-pulse text-primary" />
   434→

   435→
Building your profile...

   436→

   437→              AI is generating your bio, skills, and certifications
   438→

   439→

   440→

   441→

   442→

   443→

   444→

   445→        )}
   446→
   447→        {/* Step: Review */}
   448→        {step === "review" && (
   449→

   450→

   451→

   452→                <Sparkles className="h-5 w-5" />
   453→

   454→

   455→
Review & Edit

   456→

   457→                  AI generated this — customize as needed
   458→

   459→

   460→

   461→
   462→            {/* Your Answers Summary */}
   463→            <Card className="mt-6">
   464→              <CardContent className="space-y-3 pt-6">
   465→

   466→

   467→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   468→                    {trade}
   469→

   470→                  <button
   471→                    onClick={() => setStep("trade")}
   472→                    className="text-xs text-primary hover:underline"
   473→                  >
   474→                    Change
   475→                  </button>
   476→

   477→

   478→

   479→                    <Clock className="h-4 w-4 text-muted-foreground" />
   480→
   481→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   482→
   483→

   484→                  <button
   485→                    onClick={() => setStep("experience")}
   486→                    className="text-xs text-primary hover:underline"
   487→                  >
   488→                    Change
   489→                  </button>
   490→

   491→

   492→

   493→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   494→                    {location}
   495→

   496→                  <button
   497→                    onClick={() => setStep("location")}
   498→                    className="text-xs text-primary hover:underline"
   499→                  >
   500→                    Change
   501→                  </button>
   502→

   503→              </CardContent>
   504→            </Card>
   505→
   506→            {/* Bio */}
   507→

   508→

   509→
   510→                  <Sparkles className="h-4 w-4 text-primary" />
   511→                  Your Bio
   512→
   513→                AI-generated
   514→

   515→              <textarea
   516→                value={editedBio}
   517→                onChange={(e) => setEditedBio(e.target.value)}
   518→                className="min-h-[120px] w-full rounded-xl border-2 border-border bg-background px-4 py-3 text-base focus:border-primary focus:outline-none"
   519→                placeholder="Your professional bio..."
   520→              />
   521→

   522→
   523→            {/* Skills */}
   524→

   525→

   526→
   527→                  <Zap className="h-4 w-4 text-primary" />
   528→                  Skills
   529→
   530→                Tap to remove
   531→

   532→

   533→                {editedSkills.map((skill, idx) => (
   534→                  <button
   535→                    key={idx}
   536→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   537→                    className="rounded-full border-2 border-primary bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20 active:scale-95"
   538→                  >
   539→                    {skill} ×
   540→                  </button>
   541→                ))}
   542→

   543→

   544→
   545→            {/* Certifications */}
   546→

   547→

   548→
   549→                  <Shield className="h-4 w-4 text-primary" />
   550→                  Certifications
   551→
   552→                Tap to remove
   553→

   554→

   555→                {editedCerts.map((cert, idx) => (
   556→                  <button
   557→                    key={idx}
   558→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   559→                    className="flex w-full items-center justify-between rounded-xl border-2 border-border bg-background px-4 py-3 text-left text-sm font-medium transition-colors hover:border-destructive hover:bg-destructive/5 active:scale-95"
   560→                  >
   561→                    {cert}
   562→                    Remove
   563→                  </button>
   564→                ))}
   565→

   566→

   567→

   568→        )}
   569→
   570→
   571→      {/* Bottom sticky CTA - only show on review step */}
   572→      {step === "review" && (
   573→

   574→

   575→            <Button
   576→              onClick={handleSave}
   577→              className="h-16 w-full gap-2 text-lg font-semibold"
   578→              disabled={saving}
   579→            >
   580→              {saving ? (
   581→                <>
   582→                  <Loader2 className="h-6 w-6 animate-spin" />
   583→                  Saving profile...
   584→                </>
   585→              ) : (
   586→                <>
   587→                  <CheckCircle2 className="h-6 w-6" />
   588→                  Save My Profile
   589→                </>
   590→              )}
   591→            </Button>
   592→

   593→

   594→      )}
   595→

   596→  );
   597→}
   598→
   599→function StepHeader({
   600→  icon,
   601→  title,
   602→  subtitle,
   603→}: {
   604→  icon: React.ReactNode;
   605→  title: string;
   606→  subtitle: string;
   607→}) {
   608→  return (
   609→

   610→

   611→        {icon}
   612→

   613→

   614→
{title}

   615→
{subtitle}

   616→

   617→

   618→  );
   619→}
   620→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import { createClient } from "@/lib/supabase/client";
     7→import { Button } from "@/components/ui/button";
     8→import { Input } from "@/components/ui/input";
     9→import { Label } from "@/components/ui/label";
    10→import { Card, CardContent } from "@/components/ui/card";
    11→import {
    12→  HardHat,
    13→  Keyboard,
    14→  Mic,
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  CheckCircle2,
    19→  Sparkles,
    20→  MapPin,
    21→  Clock,
    22→  Shield,
    23→  Edit3,
    24→  Wrench,
    25→  Calendar,
    26→  Globe2,
    27→} from "lucide-react";
    28→
    29→type Step = "auth" | "method" | "trade" | "experience" | "location" | "availability" | "certs" | "review";
    30→
    31→const TRADES = [
    32→  "Labourer",
    33→  "Steel Fixer",
    34→  "Formworker",
    35→  "Carpenter",
    36→  "Electrician",
    37→  "Plumber",
    38→  "Concreter",
    39→  "Bricklayer",
    40→  "Painter",
    41→  "Roofer",
    42→  "Scaffolder",
    43→  "Tiler",
    44→  "Plasterer",
    45→  "Welder",
    46→  "Crane Operator",
    47→  "Excavator Operator",
    48→];
    49→
    50→const EXPERIENCE_OPTIONS = [
    51→  { label: "Less than 1 year", value: 0 },
    52→  { label: "1-3 years", value: 2 },
    53→  { label: "3-5 years", value: 4 },
    54→  { label: "5-10 years", value: 7 },
    55→  { label: "10+ years", value: 12 },
    56→];
    57→
    58→const AVAILABILITY_OPTIONS = [
    59→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    60→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    61→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    62→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    63→];
    64→
    65→const CERT_OPTIONS = [
    66→  "White Card",
    67→  "Forklift Licence",
    68→  "EWP (Elevated Work Platform)",
    69→  "Working at Heights",
    70→  "First Aid",
    71→  "Traffic Control",
    72→  "Rigging",
    73→  "Dogging",
    74→  "Confined Spaces",
    75→];
    76→
    77→interface WorkerData {
    78→  trades: string[];
    79→  experience: number;
    80→  suburb: string;
    81→  availability: "full-time" | "casual" | "weekends" | "flexible";
    82→  certifications: string[];
    83→}
    84→
    85→function generateBio(data: WorkerData): string {
    86→  const tradeStr = data.trades.join(" and ");
    87→  const expStr =
    88→    data.experience === 0
    89→      ? "under 1 year"
    90→      : data.experience >= 12
    91→        ? "10+ years"
    92→        : `${data.experience} years`;
    93→  const certStr =
    94→    data.certifications.length > 0
    95→      ? ` Holds ${data.certifications.join(", ")}.`
    96→      : "";
    97→
    98→  return `Experienced ${tradeStr.toLowerCase()} with ${expStr} in the construction industry. Based in ${data.suburb}, available for ${data.availability} work.${certStr}`;
    99→}
   100→
   101→export default function WorkerSignupPage() {
   102→  const [step, setStep] = useState<Step>("auth");
   103→  const [isAuthenticated, setIsAuthenticated] = useState(false);
   104→  const [loading, setLoading] = useState(true);
   105→
   106→  // Auth state
   107→  const [email, setEmail] = useState("");
   108→  const [password, setPassword] = useState("");
   109→  const [authError, setAuthError] = useState<string | null>(null);
   110→  const [authLoading, setAuthLoading] = useState(false);
   111→
   112→  // Worker data
   113→  const [selectedTrades, setSelectedTrades] = useState<string[]>([]);
   114→  const [experience, setExperience] = useState<number | null>(null);
   115→  const [suburb, setSuburb] = useState("");
   116→  const [availability, setAvailability] = useState<WorkerData["availability"] | null>(null);
   117→  const [certifications, setCertifications] = useState<string[]>([]);
   118→
   119→  const [saving, setSaving] = useState(false);
   120→
   121→  const router = useRouter();
   122→  const supabase = createClient();
   123→
   124→  useEffect(() => {
   125→    async function checkAuth() {
   126→      const {
   127→        data: { user },
   128→      } = await supabase.auth.getUser();
   129→      if (user) {
   130→        setIsAuthenticated(true);
   131→        setStep("method");
   132→      }
   133→      setLoading(false);
   134→    }
   135→    checkAuth();
   136→  }, [supabase.auth]);
   137→
   138→  async function handleAuth(e: React.FormEvent) {
   139→    e.preventDefault();
   140→    setAuthLoading(true);
   141→    setAuthError(null);
   142→
   143→    const { error } = await supabase.auth.signUp({
   144→      email,
   145→      password,
   146→      options: {
   147→        data: { type: "worker" },
   148→        emailRedirectTo: `${window.location.origin}/auth/callback`,
   149→      },
   150→    });
   151→
   152→    if (error) {
   153→      if (error.message.includes("already registered")) {
   154→        const { error: loginError } = await supabase.auth.signInWithPassword({
   155→          email,
   156→          password,
   157→        });
   158→        if (loginError) {
   159→          setAuthError(loginError.message);
   160→          setAuthLoading(false);
   161→          return;
   162→        }
   163→      } else {
   164→        setAuthError(error.message);
   165→        setAuthLoading(false);
   166→        return;
   167→      }
   168→    }
   169→
   170→    setIsAuthenticated(true);
   171→    setStep("method");
   172→    setAuthLoading(false);
   173→  }
   174→
   175→  function toggleTrade(trade: string) {
   176→    setSelectedTrades((prev) =>
   177→      prev.includes(trade) ? prev.filter((t) => t !== trade) : [...prev, trade]
   178→    );
   179→  }
   180→
   181→  function toggleCert(cert: string) {
   182→    setCertifications((prev) =>
   183→      prev.includes(cert) ? prev.filter((c) => c !== cert) : [...prev, cert]
   184→    );
   185→  }
   186→
   187→  async function handleConfirm() {
   188→    if (!availability || experience === null) return;
   189→    setSaving(true);
   190→
   191→    try {
   192→      const {
   193→        data: { user },
   194→      } = await supabase.auth.getUser();
   195→      if (!user) {
   196→        alert("Not logged in. Please sign in again.");
   197→        setSaving(false);
   198→        return;
   199→      }
   200→
   201→      const workerData: WorkerData = {
   202→        trades: selectedTrades,
   203→        experience,
   204→        suburb,
   205→        availability,
   206→        certifications,
   207→      };
   208→
   209→      const bio = generateBio(workerData);
   210→
   211→      console.log("Updating profile...", { userId: user.id, suburb });
   212→      const profileResult = await supabase.from("profiles").update({
   213→        name: user.email?.split("@")[0] ?? "",
   214→        suburb,
   215→        onboarding_completed: true,
   216→      }).eq("id", user.id);
   217→
   218→      if (profileResult.error) {
   219→        console.error("Profile update failed:", profileResult.error);
   220→        alert(`Profile update failed: ${profileResult.error.message}`);
   221→        setSaving(false);
   222→        return;
   223→      }
   224→
   225→      console.log("Inserting worker profile...", { profile_id: user.id });
   226→      const workerResult = await supabase.from("worker_profiles").insert({
   227→        profile_id: user.id,
   228→        trades: selectedTrades,
   229→        experience_years: experience,
   230→        availability,
   231→        certifications,
   232→        bio,
   233→        ai_extracted: {
   234→          source: "typed_answers",
   235→          created_at: new Date().toISOString(),
   236→        },
   237→      });
   238→
   239→      if (workerResult.error) {
   240→        console.error("Worker profile insert failed:", workerResult.error);
   241→        alert(`Worker profile creation failed: ${workerResult.error.message}`);
   242→        setSaving(false);
   243→        return;
   244→      }
   245→
   246→      console.log("Profile created successfully!");
   247→      router.push("/dashboard");
   248→      router.refresh();
   249→    } catch (error: any) {
   250→      console.error("Signup error:", error);
   251→      alert(`Something went wrong: ${error.message || "Unknown error"}`);
   252→      setSaving(false);
   253→    }
   254→  }
   255→
   256→  const workerData: WorkerData | null =
   257→    selectedTrades.length > 0 && experience !== null && suburb && availability
   258→      ? { trades: selectedTrades, experience, suburb, availability, certifications }
   259→      : null;
   260→
   261→  const stepOrder: Step[] = ["auth", "method", "trade", "experience", "location", "availability", "certs", "review"];
   262→  const activeSteps = isAuthenticated ? stepOrder.slice(1) : stepOrder;
   263→  const currentIndex = activeSteps.indexOf(step);
   264→  const totalSteps = activeSteps.length;
   265→
   266→  if (loading) {
   267→    return (
   268→

   269→        <Loader2 className="h-8 w-8 animate-spin text-primary" />
   270→

   271→    );
   272→  }
   273→
   274→  return (
   275→

   276→      {/* Header */}
   277→
   278→

   279→          <Link href="/" className="flex items-center gap-2">
   280→

   281→              <HardHat className="h-5 w-5 text-primary-foreground" />
   282→

   283→            RateRight
   284→          </Link>
   285→

   286→
   287→
   288→
   289→        {/* Progress bar */}
   290→

   291→          {Array.from({ length: totalSteps }, (_, i) => (
   292→            <div
   293→              key={i}
   294→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   295→                i <= currentIndex ? "bg-primary" : "bg-muted"
   296→              }`}
   297→            />
   298→          ))}
   299→

   300→
   301→        {/* Step: Auth */}
   302→        {step === "auth" && (
   303→

   304→

   305→

   306→                <HardHat className="h-5 w-5" />
   307→

   308→

   309→
Create your account

   310→

   311→                  Always free for workers
   312→

   313→

   314→

   315→
   316→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   317→

   318→                Email
   319→                <Input
   320→                  id="email"
   321→                  type="email"
   322→                  placeholder="you@email.com"
   323→                  value={email}
   324→                  onChange={(e) => setEmail(e.target.value)}
   325→                  required
   326→                  autoComplete="email"
   327→                  className="h-12 text-base"
   328→                />
   329→

   330→

   331→                Password
   332→                <Input
   333→                  id="password"
   334→                  type="password"
   335→                  placeholder="Create a password"
   336→                  value={password}
   337→                  onChange={(e) => setPassword(e.target.value)}
   338→                  required
   339→                  minLength={6}
   340→                  autoComplete="new-password"
   341→                  className="h-12 text-base"
   342→                />
   343→

   344→
   345→              {authError && (
   346→
{authError}

   347→              )}
   348→
   349→              <Button
   350→                type="submit"
   351→                className="h-12 w-full gap-2 text-base font-semibold"
   352→                disabled={authLoading}
   353→              >
   354→                {authLoading ? (
   355→                  <>
   356→                    <Loader2 className="h-5 w-5 animate-spin" />
   357→                    Creating account...
   358→                  </>
   359→                ) : (
   360→                  <>
   361→                    Continue
   362→                    <ArrowRight className="h-5 w-5" />
   363→                  </>
   364→                )}
   365→              </Button>
   366→            </form>
   367→
   368→

   369→              Already have an account?{" "}
   370→              <Link
   371→                href="/auth/login"
   372→                className="text-primary underline hover:no-underline"
   373→              >
   374→                Sign in
   375→              </Link>
   376→

   377→

   378→        )}
   379→
   380→        {/* Step: Choose method */}
   381→        {step === "method" && (
   382→

   383→

   384→

   385→                <HardHat className="h-5 w-5" />
   386→

   387→

   388→
Build your profile

   389→

   390→                  How do you want to get started?
   391→

   392→

   393→

   394→
   395→

   396→              <button
   397→                onClick={() => setStep("trade")}
   398→                className="flex w-full items-center gap-4 rounded-xl border-2 border-primary bg-primary/5 p-5 text-left transition-colors hover:bg-primary/10"
   399→              >
   400→

   401→                  <Keyboard className="h-6 w-6" />
   402→

   403→

   404→
Type my answers

   405→

   406→                    5 quick questions, under 2 minutes
   407→

   408→

   409→                <ArrowRight className="ml-auto h-5 w-5 text-primary" />
   410→              </button>
   411→
   412→              <button
   413→                disabled
   414→                className="flex w-full items-center gap-4 rounded-xl border-2 border-muted p-5 text-left opacity-50"
   415→              >
   416→

   417→                  <Mic className="h-6 w-6" />
   418→

   419→

   420→
Send a voice note

   421→

   422→                    Coming soon — speak and AI builds your profile
   423→

   424→

   425→              </button>
   426→

   427→

   428→        )}
   429→
   430→        {/* Step: Select trade */}
   431→        {step === "trade" && (
   432→

   433→            <StepHeader
   434→              icon={<Wrench className="h-5 w-5" />}
   435→              title="What's your trade?"
   436→              subtitle="Select one or more"
   437→            />
   438→
   439→

   440→              {TRADES.map((trade) => (
   441→                <button
   442→                  key={trade}
   443→                  onClick={() => toggleTrade(trade)}
   444→                  className={`rounded-xl border-2 px-4 py-3 text-left text-sm font-medium transition-colors ${
   445→                    selectedTrades.includes(trade)
   446→                      ? "border-primary bg-primary/10 text-primary"
   447→                      : "border-border hover:border-primary/50"
   448→                  }`}
   449→                >
   450→                  {trade}
   451→                </button>
   452→              ))}
   453→

   454→
   455→

   456→              <Button
   457→                variant="outline"
   458→                onClick={() => setStep("method")}
   459→                className="h-12"
   460→              >
   461→                <ArrowLeft className="h-5 w-5" />
   462→              </Button>
   463→              <Button
   464→                onClick={() => setStep("experience")}
   465→                disabled={selectedTrades.length === 0}
   466→                className="h-12 flex-1 gap-2 text-base font-semibold"
   467→              >
   468→                Continue
   469→                <ArrowRight className="h-5 w-5" />
   470→              </Button>
   471→

   472→

   473→        )}
   474→
   475→        {/* Step: Experience */}
   476→        {step === "experience" && (
   477→

   478→            <StepHeader
   479→              icon={<Clock className="h-5 w-5" />}
   480→              title="How much experience?"
   481→              subtitle="In construction overall"
   482→            />
   483→
   484→

   485→              {EXPERIENCE_OPTIONS.map((opt) => (
   486→                <button
   487→                  key={opt.value}
   488→                  onClick={() => {
   489→                    setExperience(opt.value);
   490→                    setStep("location");
   491→                  }}
   492→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 py-4 text-left text-base font-medium transition-colors ${
   493→                    experience === opt.value
   494→                      ? "border-primary bg-primary/10 text-primary"
   495→                      : "border-border hover:border-primary/50"
   496→                  }`}
   497→                >
   498→                  {opt.label}
   499→                  <ArrowRight className="h-4 w-4 text-muted-foreground" />
   500→                </button>
   501→              ))}
   502→

   503→
   504→            <button
   505→              onClick={() => setStep("trade")}
   506→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   507→            >
   508→              <ArrowLeft className="h-4 w-4" />
   509→              Back
   510→            </button>
   511→

   512→        )}
   513→
   514→        {/* Step: Location */}
   515→        {step === "location" && (
   516→

   517→            <StepHeader
   518→              icon={<MapPin className="h-5 w-5" />}
   519→              title="Where are you based?"
   520→              subtitle="Your suburb or area"
   521→            />
   522→
   523→            <form
   524→              onSubmit={(e) => {
   525→                e.preventDefault();
   526→                if (suburb.trim()) setStep("availability");
   527→              }}
   528→              className="mt-6 space-y-4"
   529→            >
   530→              <Input
   531→                placeholder="e.g. Parramatta, Blacktown, Liverpool"
   532→                value={suburb}
   533→                onChange={(e) => setSuburb(e.target.value)}
   534→                required
   535→                autoFocus
   536→                className="h-12 text-base"
   537→              />
   538→

   539→                We'll match you to jobs near this location.
   540→

   541→
   542→

   543→                <Button
   544→                  type="button"
   545→                  variant="outline"
   546→                  onClick={() => setStep("experience")}
   547→                  className="h-12"
   548→                >
   549→                  <ArrowLeft className="h-5 w-5" />
   550→                </Button>
   551→                <Button
   552→                  type="submit"
   553→                  disabled={!suburb.trim()}
   554→                  className="h-12 flex-1 gap-2 text-base font-semibold"
   555→                >
   556→                  Continue
   557→                  <ArrowRight className="h-5 w-5" />
   558→                </Button>
   559→

   560→            </form>
   561→

   562→        )}
   563→
   564→        {/* Step: Availability */}
   565→        {step === "availability" && (
   566→

   567→            <StepHeader
   568→              icon={<Calendar className="h-5 w-5" />}
   569→              title="Your availability?"
   570→              subtitle="When can you work?"
   571→            />
   572→
   573→

   574→              {AVAILABILITY_OPTIONS.map((opt) => (
   575→                <button
   576→                  key={opt.value}
   577→                  onClick={() => {
   578→                    setAvailability(opt.value);
   579→                    setStep("certs");
   580→                  }}
   581→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors ${
   582→                    availability === opt.value
   583→                      ? "border-primary bg-primary/10"
   584→                      : "border-border hover:border-primary/50"
   585→                  }`}
   586→                >
   587→

   588→                    <p className={`text-base font-medium ${availability === opt.value ? "text-primary" : ""}`}>
   589→                      {opt.label}
   590→

   591→
{opt.desc}

   592→

   593→                  <ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground" />
   594→                </button>
   595→              ))}
   596→

   597→
   598→            <button
   599→              onClick={() => setStep("location")}
   600→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   601→            >
   602→              <ArrowLeft className="h-4 w-4" />
   603→              Back
   604→            </button>
   605→

   606→        )}
   607→
   608→        {/* Step: Certifications */}
   609→        {step === "certs" && (
   610→

   611→            <StepHeader
   612→              icon={<Shield className="h-5 w-5" />}
   613→              title="Your certifications"
   614→              subtitle="Select all that apply (optional)"
   615→            />
   616→
   617→

   618→              {CERT_OPTIONS.map((cert) => (
   619→                <button
   620→                  key={cert}
   621→                  onClick={() => toggleCert(cert)}
   622→                  className={`flex w-full items-center gap-3 rounded-xl border-2 px-5 py-3.5 text-left text-sm font-medium transition-colors ${
   623→                    certifications.includes(cert)
   624→                      ? "border-primary bg-primary/10 text-primary"
   625→                      : "border-border hover:border-primary/50"
   626→                  }`}
   627→                >
   628→

   635→                    {certifications.includes(cert) && (
   636→                      <CheckCircle2 className="h-3.5 w-3.5" />
   637→                    )}
   638→

   639→                  {cert}
   640→                </button>
   641→              ))}
   642→

   643→
   644→

   645→              <Button
   646→                variant="outline"
   647→                onClick={() => setStep("availability")}
   648→                className="h-12"
   649→              >
   650→                <ArrowLeft className="h-5 w-5" />
   651→              </Button>
   652→              <Button
   653→                onClick={() => setStep("review")}
   654→                className="h-12 flex-1 gap-2 text-base font-semibold"
   655→              >
   656→                Review my profile
   657→                <ArrowRight className="h-5 w-5" />
   658→              </Button>
   659→

   660→

   661→        )}
   662→
   663→        {/* Step: Review */}
   664→        {step === "review" && workerData && (
   665→

   666→

   667→

   668→                <CheckCircle2 className="h-5 w-5" />
   669→

   670→

   671→
Your profile

   672→

   673→                  Review and confirm
   674→

   675→

   676→

   677→
   678→            <Card className="mt-6">
   679→              <CardContent className="space-y-4 pt-6">
   680→                <ReviewField
   681→                  icon={<Wrench className="h-4 w-4" />}
   682→                  label="Trade"
   683→                  value={workerData.trades.join(", ")}
   684→                  onEdit={() => setStep("trade")}
   685→                />
   686→                <ReviewField
   687→                  icon={<Clock className="h-4 w-4" />}
   688→                  label="Experience"
   689→                  value={
   690→                    workerData.experience === 0
   691→                      ? "Less than 1 year"
   692→                      : workerData.experience >= 12
   693→                        ? "10+ years"
   694→                        : `${workerData.experience} years`
   695→                  }
   696→                  onEdit={() => setStep("experience")}
   697→                />
   698→                <ReviewField
   699→                  icon={<MapPin className="h-4 w-4" />}
   700→                  label="Location"
   701→                  value={workerData.suburb}
   702→                  onEdit={() => setStep("location")}
   703→                />
   704→                <ReviewField
   705→                  icon={<Calendar className="h-4 w-4" />}
   706→                  label="Availability"
   707→                  value={
   708→                    AVAILABILITY_OPTIONS.find(
   709→                      (o) => o.value === workerData.availability
   710→                    )?.label ?? workerData.availability
   711→                  }
   712→                  onEdit={() => setStep("availability")}
   713→                />
   714→                <ReviewField
   715→                  icon={<Shield className="h-4 w-4" />}
   716→                  label="Certifications"
   717→                  value={
   718→                    workerData.certifications.length > 0
   719→                      ? workerData.certifications.join(", ")
   720→                      : "None added"
   721→                  }
   722→                  onEdit={() => setStep("certs")}
   723→                />
   724→              </CardContent>
   725→            </Card>
   726→
   727→            {/* AI-generated bio */}
   728→

   729→

   730→                <Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
   731→

   732→

   733→                    AI-generated bio
   734→

   735→

   736→                    {generateBio(workerData)}
   737→

   738→

   739→

   740→

   741→
   742→            <Button
   743→              onClick={handleConfirm}
   744→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   745→              disabled={saving}
   746→            >
   747→              {saving ? (
   748→                <>
   749→                  <Loader2 className="h-5 w-5 animate-spin" />
   750→                  Creating profile...
   751→                </>
   752→              ) : (
   753→                <>
   754→                  <CheckCircle2 className="h-5 w-5" />
   755→                  Looks good — create my profile
   756→                </>
   757→              )}
   758→            </Button>
   759→
   760→            <button
   761→              onClick={() => setStep("certs")}
   762→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   763→            >
   764→              <ArrowLeft className="h-4 w-4" />
   765→              Go back
   766→            </button>
   767→

   768→        )}
   769→
   770→

   771→  );
   772→}
   773→
   774→function StepHeader({
   775→  icon,
   776→  title,
   777→  subtitle,
   778→}: {
   779→  icon: React.ReactNode;
   780→  title: string;
   781→  subtitle: string;
   782→}) {
   783→  return (
   784→

   785→

   786→        {icon}
   787→

   788→

   789→
{title}

   790→
{subtitle}

   791→

   792→

   793→  );
   794→}
   795→
   796→function ReviewField({
   797→  icon,
   798→  label,
   799→  value,
   800→  onEdit,
   801→}: {
   802→  icon: React.ReactNode;
   803→  label: string;
   804→  value: string;
   805→  onEdit: () => void;
   806→}) {
   807→  return (
   808→    <button
   809→      onClick={onEdit}
   810→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   811→    >
   812→
{icon}

   813→

   814→
{label}

   815→
{value}

   816→

   817→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   818→    </button>
   819→  );
   820→}
   821→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useEffect, useState, useRef } from "react";
     4→import Link from "next/link";
     5→import { createClient } from "@/lib/supabase/client";
     6→import { Card, CardContent } from "@/components/ui/card";
     7→import { Badge } from "@/components/ui/badge";
     8→import { Button } from "@/components/ui/button";
     9→import { BottomNav } from "@/components/bottom-nav";
    10→import {
    11→  MapPin,
    12→  Calendar,
    13→  DollarSign,
    14→  Loader2,
    15→  Search,
    16→  Briefcase,
    17→  Check,
    18→  X,
    19→} from "lucide-react";
    20→import type { Job } from "@/lib/types";
    21→
    22→const TRADE_FILTERS = [
    23→  "All",
    24→  "Steel Fixer",
    25→  "Formworker",
    26→  "Labourer",
    27→  "Concreter",
    28→  "Carpenter",
    29→  "Scaffolder",
    30→  "Other",
    31→];
    32→
    33→const TRADE_ID_TO_LABEL: Record<string, string> = {
    34→  "steel-fixer": "Steel Fixer",
    35→  "formworker": "Formworker",
    36→  "labourer": "Labourer",
    37→  "concreter": "Concreter",
    38→  "carpenter": "Carpenter",
    39→  "scaffolder": "Scaffolder",
    40→  "other": "Other",
    41→};
    42→
    43→function tradeLabelFromId(id: string): string {
    44→  return TRADE_ID_TO_LABEL[id] || id;
    45→}
    46→
    47→interface SwipeableJobCardProps {
    48→  job: Job;
    49→  hasApplied: boolean;
    50→  onApply: (job: Job) => void;
    51→  onClick: (job: Job) => void;
    52→}
    53→
    54→function SwipeableJobCard({ job, hasApplied, onApply, onClick }: SwipeableJobCardProps) {
    55→  const [swipeX, setSwipeX] = useState(0);
    56→  const [swiping, setSwiping] = useState(false);
    57→  const startX = useRef(0);
    58→  const currentX = useRef(0);
    59→
    60→  const handleTouchStart = (e: React.TouchEvent) => {
    61→    if (hasApplied) return;
    62→    startX.current = e.touches[0].clientX;
    63→    setSwiping(true);
    64→  };
    65→
    66→  const handleTouchMove = (e: React.TouchEvent) => {
    67→    if (!swiping || hasApplied) return;
    68→    currentX.current = e.touches[0].clientX;
    69→    const diff = currentX.current - startX.current;
    70→    // Only allow right swipe (positive diff)
    71→    if (diff > 0) {
    72→      setSwipeX(Math.min(diff, 150));
    73→    }
    74→  };
    75→
    76→  const handleTouchEnd = () => {
    77→    if (!swiping || hasApplied) return;
    78→    setSwiping(false);
    79→
    80→    // If swiped more than 100px, apply
    81→    if (swipeX > 100) {
    82→      onApply(job);
    83→    }
    84→
    85→    // Reset
    86→    setSwipeX(0);
    87→  };
    88→
    89→  const swipeProgress = Math.min(swipeX / 100, 1);
    90→  const backgroundColor = hasApplied
    91→    ? "transparent"
    92→    : `rgba(34, 197, 94, ${swipeProgress * 0.1})`;
    93→
    94→  return (
    95→

    96→      {/* Swipe indicator background */}
    97→      {!hasApplied && swipeX > 0 && (
    98→

   105→          <Check className="h-8 w-8 text-green-600" />
   106→
   107→            {swipeProgress >= 1 ? "Release to apply" : "Swipe to apply"}
   108→
   109→

   110→      )}
   111→
   112→      <Card
   113→        className="cursor-pointer active:scale-[0.98] transition-transform touch-pan-y"
   114→        style={{
   115→          transform: `translateX(${swipeX}px)`,
   116→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   117→          backgroundColor,
   118→        }}
   119→        onTouchStart={handleTouchStart}
   120→        onTouchMove={handleTouchMove}
   121→        onTouchEnd={handleTouchEnd}
   122→        onClick={() => !swiping && onClick(job)}
   123→      >
   124→        <CardContent className="p-5">
   125→

   126→

   127→

   128→                <Badge variant="secondary" className="text-sm shrink-0 py-1 px-3">
   129→                  {tradeLabelFromId(job.trade)}
   130→                </Badge>
   131→                {hasApplied && (
   132→                  <Badge className="text-sm bg-green-600 shrink-0 py-1 px-3">
   133→                    <Check className="h-3.5 w-3.5 mr-1" />
   134→                    Applied
   135→                  </Badge>
   136→                )}
   137→

   138→
{job.title}

   139→

   140→

   141→
   142→

   143→            {job.suburb && (
   144→
   145→                <MapPin className="h-4 w-4" />
   146→                {job.suburb}
   147→
   148→            )}
   149→            {(job.rate_min || job.rate_max) && (
   150→
   151→                <DollarSign className="h-4 w-4" />
   152→                {job.rate_min && job.rate_max
   153→                  ? `$${job.rate_min}–$${job.rate_max}`
   154→                  : job.rate_min
   155→                  ? `From $${job.rate_min}`
   156→                  : `Up to $${job.rate_max}`}
   157→                {job.rate_type === "hourly" ? "/hr" : "/day"}
   158→
   159→            )}
   160→            {job.start_date && (
   161→
   162→                <Calendar className="h-4 w-4" />
   163→                {new Date(job.start_date).toLocaleDateString("en-AU", {
   164→                  day: "numeric",
   165→                  month: "short",
   166→                })}
   167→
   168→            )}
   169→            {job.workers_needed > 1 && (
   170→
   171→                <Briefcase className="h-4 w-4" />
   172→                {job.workers_needed} workers
   173→
   174→            )}
   175→

   176→        </CardContent>
   177→      </Card>
   178→

   179→  );
   180→}
   181→
   182→export default function WorkerJobsPage() {
   183→  const supabase = createClient();
   184→  const [jobs, setJobs] = useState<Job[]>([]);
   185→  const [loading, setLoading] = useState(true);
   186→  const [activeTrade, setActiveTrade] = useState("All");
   187→  const [selectedJob, setSelectedJob] = useState<Job | null>(null);
   188→  const [applying, setApplying] = useState(false);
   189→  const [appliedJobs, setAppliedJobs] = useState<Set<string>>(new Set());
   190→  const [userId, setUserId] = useState<string | null>(null);
   191→
   192→  useEffect(() => {
   193→    async function load() {
   194→      const { data: { user } } = await supabase.auth.getUser();
   195→      if (user) setUserId(user.id);
   196→
   197→      // Fetch active jobs
   198→      const { data } = await supabase
   199→        .from("jobs")
   200→        .select("*")
   201→        .eq("status", "active")
   202→        .order("created_at", { ascending: false });
   203→
   204→      if (data) setJobs(data);
   205→
   206→      // Fetch already-applied jobs
   207→      if (user) {
   208→        const { data: matches } = await supabase
   209→          .from("matches")
   210→          .select("job_id")
   211→          .eq("worker_id", user.id);
   212→
   213→        if (matches) {
   214→          setAppliedJobs(new Set(matches.map((m: { job_id: string }) => m.job_id)));
   215→        }
   216→      }
   217→
   218→      setLoading(false);
   219→    }
   220→    load();
   221→  }, [supabase]);
   222→
   223→  const filteredJobs =
   224→    activeTrade === "All"
   225→      ? jobs
   226→      : jobs.filter(
   227→          (j) => tradeLabelFromId(j.trade).toLowerCase() === activeTrade.toLowerCase()
   228→        );
   229→
   230→  async function handleApply(job: Job) {
   231→    if (!userId || appliedJobs.has(job.id)) return;
   232→    setApplying(true);
   233→
   234→    // Haptic feedback (if supported)
   235→    if (navigator.vibrate) {
   236→      navigator.vibrate(50);
   237→    }
   238→
   239→    const { error } = await supabase.from("matches").insert({
   240→      job_id: job.id,
   241→      worker_id: userId,
   242→      status: "applied",
   243→      contractor_action: "pending",
   244→      worker_action: "accepted",
   245→    });
   246→
   247→    if (!error) {
   248→      setAppliedJobs((prev) => new Set(prev).add(job.id));
   249→    }
   250→
   251→    setApplying(false);
   252→    setSelectedJob(null);
   253→  }
   254→
   255→  if (loading) {
   256→    return (
   257→

   258→        <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
   259→

   260→    );
   261→  }
   262→
   263→  return (
   264→

   265→      {/* Header */}
   266→
   267→

   268→
Available Jobs

   269→          <Link href="/dashboard" className="text-xl font-bold tracking-tight">
   270→            RateRight
   271→          </Link>
   272→

   273→
   274→
   275→
   276→        {/* Trade filters — horizontal scroll */}
   277→

   278→          {TRADE_FILTERS.map((trade) => (
   279→            <button
   280→              key={trade}
   281→              onClick={() => setActiveTrade(trade)}
   282→              className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   283→                activeTrade === trade
   284→                  ? "bg-primary text-primary-foreground"
   285→                  : "bg-muted text-muted-foreground"
   286→              }`}
   287→            >
   288→              {trade}
   289→            </button>
   290→          ))}
   291→

   292→
   293→        {/* Swipe hint */}
   294→        {filteredJobs.length > 0 && !loading && (
   295→

   296→

   297→              👉 Swipe right to quick apply • Tap to view details
   298→

   299→

   300→        )}
   301→
   302→        {/* Job list */}
   303→        {filteredJobs.length === 0 ? (
   304→

   305→            <Search className="h-16 w-16 text-muted-foreground/50" />
   306→
No jobs found

   307→

   308→              {activeTrade === "All"
   309→                ? "No jobs posted yet. Check back soon."
   310→                : `No ${activeTrade} jobs right now. Try another trade.`}
   311→

   312→

   313→        ) : (
   314→

   315→            {filteredJobs.map((job) => (
   316→              <SwipeableJobCard
   317→                key={job.id}
   318→                job={job}
   319→                hasApplied={appliedJobs.has(job.id)}
   320→                onApply={handleApply}
   321→                onClick={setSelectedJob}
   322→              />
   323→            ))}
   324→

   325→        )}
   326→
   327→
   328→      {/* Job Detail Sheet (bottom sheet style) */}
   329→      {selectedJob && (
   330→

   331→
 setSelectedJob(null)}
   334→          />
   335→

   336→            {/* Handle */}
   337→

   338→

   339→

   340→
   341→

   342→              <Badge variant="secondary" className="mb-3 text-sm py-1.5 px-3">
   343→                {tradeLabelFromId(selectedJob.trade)}
   344→              </Badge>
   345→
{selectedJob.title}

   346→
   347→

   348→                {selectedJob.suburb && (
   349→
   350→                    <MapPin className="h-5 w-5" />
   351→                    {selectedJob.site_address || selectedJob.suburb}
   352→
   353→                )}
   354→                {(selectedJob.rate_min || selectedJob.rate_max) && (
   355→
   356→                    <DollarSign className="h-5 w-5" />
   357→                    {selectedJob.rate_min && selectedJob.rate_max
   358→                      ? `$${selectedJob.rate_min}–$${selectedJob.rate_max}`
   359→                      : selectedJob.rate_min
   360→                      ? `From $${selectedJob.rate_min}`
   361→                      : `Up to $${selectedJob.rate_max}`}
   362→                    {selectedJob.rate_type === "hourly" ? "/hr" : "/day"}
   363→
   364→                )}
   365→

   366→
   367→

   368→                {selectedJob.start_date && (
   369→
   370→                    <Calendar className="h-5 w-5" />
   371→                    Starts{" "}
   372→                    {new Date(selectedJob.start_date).toLocaleDateString("en-AU", {
   373→                      weekday: "short",
   374→                      day: "numeric",
   375→                      month: "short",
   376→                    })}
   377→
   378→                )}
   379→                {selectedJob.duration && (
   380→                  Duration: {selectedJob.duration}
   381→                )}
   382→                {selectedJob.workers_needed > 1 && (
   383→
   384→                    <Briefcase className="h-5 w-5" />
   385→                    {selectedJob.workers_needed} workers needed
   386→
   387→                )}
   388→

   389→
   390→              {selectedJob.description && (
   391→

   392→
About the Job

   393→

   394→                    {selectedJob.description}
   395→

   396→

   397→              )}
   398→
   399→              {selectedJob.certifications_required?.length > 0 && (
   400→

   401→
Required Certs

   402→

   403→                    {selectedJob.certifications_required.map((cert) => (
   404→                      <Badge key={cert} variant="outline" className="text-sm py-1.5 px-3">
   405→                        {cert}
   406→                      </Badge>
   407→                    ))}
   408→

   409→

   410→              )}
   411→

   412→
   413→            {/* Sticky Bottom CTA */}
   414→

   415→

   416→                {appliedJobs.has(selectedJob.id) ? (
   417→                  <Button disabled className="w-full h-16 text-lg font-semibold bg-green-600">
   418→                    <Check className="mr-2 h-6 w-6" />
   419→                    Already Applied
   420→                  </Button>
   421→                ) : (
   422→                  <Button
   423→                    onClick={() => handleApply(selectedJob)}
   424→                    className="w-full h-16 text-lg font-semibold"
   425→                    disabled={applying}
   426→                  >
   427→                    {applying ? (
   428→                      <>
   429→                        <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   430→                        Applying...
   431→                      </>
   432→                    ) : (
   433→                      <>
   434→                        Apply with One Tap
   435→                        <Check className="ml-2 h-6 w-6" />
   436→                      </>
   437→                    )}
   438→                  </Button>
   439→                )}
   440→

   441→                  Always free for workers. No fees ever.
   442→

   443→

   444→

   445→

   446→

   447→      )}
   448→
   449→      <BottomNav userType="worker" />
   450→

   451→  );
   452→}
   453→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState } from "react";
     4→import Link from "next/link";
     5→import { useRouter } from "next/navigation";
     6→import { ConversationPreview } from "@/components/ConversationPreview";
     7→import { Button } from "@/components/ui/button";
     8→import { Input } from "@/components/ui/input";
     9→import { Search, MessageSquare } from "lucide-react";
    10→
    11→// Mock conversation data
    12→const mockConversations = [
    13→  {
    14→    id: "1",
    15→    name: "Sarah Johnson",
    16→    lastMessage: "Great! I'll be there at 7am tomorrow",
    17→    lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
    18→    unreadCount: 2,
    19→    isOnline: true,
    20→  },
    21→  {
    22→    id: "2",
    23→    name: "Mike Chen",
    24→    lastMessage: "Thanks for the opportunity!",
    25→    lastMessageTime: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
    26→    unreadCount: 0,
    27→    isOnline: false,
    28→  },
    29→  {
    30→    id: "3",
    31→    name: "David Smith",
    32→    lastMessage: "Can we discuss the pay rate?",
    33→    lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
    34→    unreadCount: 1,
    35→    isOnline: true,
    36→  },
    37→  {
    38→    id: "4",
    39→    name: "Emma Wilson",
    40→    lastMessage: "Perfect, see you on Monday",
    41→    lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
    42→    unreadCount: 0,
    43→    isOnline: false,
    44→  },
    45→  {
    46→    id: "5",
    47→    name: "James Brown",
    48→    lastMessage: "I'll bring my own tools",
    49→    lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 48), // 2 days ago
    50→    unreadCount: 0,
    51→    isOnline: true,
    52→  },
    53→];
    54→
    55→export default function MessagesPage() {
    56→  const router = useRouter();
    57→  const [searchQuery, setSearchQuery] = useState("");
    58→  const [conversations] = useState(mockConversations);
    59→
    60→  const filteredConversations = conversations.filter((conversation) =>
    61→    conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
    62→  );
    63→
    64→  const totalUnread = conversations.reduce(
    65→    (sum, conv) => sum + conv.unreadCount,
    66→    0
    67→  );
    68→
    69→  return (
    70→

    71→      {/* Header */}
    72→
    73→

    74→
Messages

    75→          {totalUnread > 0 && (
    76→
    77→              {totalUnread > 99 ? "99+" : totalUnread}
    78→
    79→          )}
    80→

    81→
    82→
    83→      {/* Search Bar */}
    84→

    85→

    86→          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
    87→          <Input
    88→            type="search"
    89→            placeholder="Search conversations..."
    90→            value={searchQuery}
    91→            onChange={(e) => setSearchQuery(e.target.value)}
    92→            className="h-12 pl-10 text-base"
    93→          />
    94→

    95→

    96→
    97→      {/* Conversations List */}
    98→
    99→        {filteredConversations.length === 0 ? (
   100→

   101→            <MessageSquare className="h-16 w-16 text-muted-foreground mb-4" />
   102→
No conversations yet

   103→

   104→              {searchQuery
   105→                ? "No conversations match your search"
   106→                : "Start a conversation by hiring a worker or applying for a job"}
   107→

   108→            {!searchQuery && (
   109→              <Button asChild>
   110→                <Link href="/dashboard">Browse Jobs</Link>
   111→              </Button>
   112→            )}
   113→

   114→        ) : (
   115→

   116→            {filteredConversations.map((conversation) => (
   117→              <ConversationPreview
   118→                key={conversation.id}
   119→                {...conversation}
   120→                onClick={() => router.push(`/messages/${conversation.id}`)}
   121→              />
   122→            ))}
   123→

   124→        )}
   125→
   126→

   127→  );
   128→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"use client";
     2→
     3→import { useState, useRef, useEffect } from "react";
     4→import { useRouter, useParams } from "next/navigation";
     5→import { MessageBubble } from "@/components/MessageBubble";
     6→import { Button } from "@/components/ui/button";
     7→import { Input } from "@/components/ui/input";
     8→import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
     9→import { ArrowLeft, Send, Phone, MoreVertical } from "lucide-react";
    10→
    11→// Mock conversation data
    12→const mockConversation = {
    13→  id: "1",
    14→  name: "Sarah Johnson",
    15→  avatar: "",
    16→  isOnline: true,
    17→  messages: [
    18→    {
    19→      id: "1",
    20→      content: "Hi! I'm interested in the carpenter position you posted",
    21→      isOwn: false,
    22→      timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
    23→    },
    24→    {
    25→      id: "2",
    26→      content: "Great! Can you tell me about your experience?",
    27→      isOwn: true,
    28→      timestamp: new Date(Date.now() - 1000 * 60 * 60 * 1.5), // 1.5 hours ago
    29→    },
    30→    {
    31→      id: "3",
    32→      content: "I've been a carpenter for 8 years, specializing in framing and finishing work. I have my White Card and all my own tools",
    33→      isOwn: false,
    34→      timestamp: new Date(Date.now() - 1000 * 60 * 60 * 1), // 1 hour ago
    35→    },
    36→    {
    37→      id: "4",
    38→      content: "That sounds perfect! The job is in North Sydney, starts tomorrow at 7am. $45/hour. Are you available?",
    39→      isOwn: true,
    40→      timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 minutes ago
    41→    },
    42→    {
    43→      id: "5",
    44→      content: "Great! I'll be there at 7am tomorrow",
    45→      isOwn: false,
    46→      timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
    47→    },
    48→  ],
    49→};
    50→
    51→export default function ConversationPage() {
    52→  const router = useRouter();
    53→  const params = useParams();
    54→  const [message, setMessage] = useState("");
    55→  const [messages, setMessages] = useState(mockConversation.messages);
    56→  const messagesEndRef = useRef<HTMLDivElement>(null);
    57→
    58→  const scrollToBottom = () => {
    59→    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
    60→  };
    61→
    62→  useEffect(() => {
    63→    scrollToBottom();
    64→  }, [messages]);
    65→
    66→  const handleSendMessage = (e: React.FormEvent) => {
    67→    e.preventDefault();
    68→    if (message.trim()) {
    69→      const newMessage = {
    70→        id: Date.now().toString(),
    71→        content: message.trim(),
    72→        isOwn: true,
    73→        timestamp: new Date(),
    74→      };
    75→      setMessages([...messages, newMessage]);
    76→      setMessage("");
    77→    }
    78→  };
    79→
    80→  const initials = mockConversation.name
    81→    .split(" ")
    82→    .map((n) => n[0])
    83→    .join("")
    84→    .toUpperCase()
    85→    .slice(0, 2);
    86→
    87→  return (
    88→

    89→      {/* Header */}
    90→
    91→

    92→

    93→            <Button
    94→              variant="ghost"
    95→              size="icon"
    96→              onClick={() => router.push("/messages")}
    97→              className="h-10 w-10"
    98→            >
    99→              <ArrowLeft className="h-5 w-5" />
   100→            </Button>
   101→

   102→              <Avatar className="h-10 w-10">
   103→                <AvatarImage src={mockConversation.avatar} alt={mockConversation.name} />
   104→                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
   105→                  {initials}
   106→                </AvatarFallback>
   107→              </Avatar>
   108→

   109→
{mockConversation.name}

   110→

   111→                  {mockConversation.isOnline ? "Online" : "Offline"}
   112→

   113→

   114→

   115→

   116→

   117→            <Button variant="ghost" size="icon" className="h-10 w-10">
   118→              <Phone className="h-5 w-5" />
   119→            </Button>
   120→            <Button variant="ghost" size="icon" className="h-10 w-10">
   121→              <MoreVertical className="h-5 w-5" />
   122→            </Button>
   123→

   124→

   125→
   126→
   127→      {/* Messages */}
   128→
   129→

   130→

   131→            {messages.map((message) => (
   132→              <MessageBubble
   133→                key={message.id}
   134→                content={message.content}
   135→                isOwn={message.isOwn}
   136→                timestamp={message.timestamp}
   137→              />
   138→            ))}
   139→

   140→

   141→

   142→
   143→
   144→      {/* Message Input */}
   145→
   146→

   147→          <form onSubmit={handleSendMessage} className="flex gap-3">
   148→            <Input
   149→              type="text"
   150→              placeholder="Type a message..."
   151→              value={message}
   152→              onChange={(e) => setMessage(e.target.value)}
   153→              className="flex-1 h-12 text-base"
   154→            />
   155→            <Button
   156→              type="submit"
   157→              size="icon"
   158→              className="h-12 w-12 shrink-0"
   159→              disabled={!message.trim()}
   160→            >
   161→              <Send className="h-5 w-5" />
   162→            </Button>
   163→          </form>
   164→

   165→
   166→

   167→  );
   168→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me read all the component files:


     1→import * as React from "react"
     2→import { cva, type VariantProps } from "class-variance-authority"
     3→import { Slot } from "radix-ui"
     4→
     5→import { cn } from "@/lib/utils"
     6→
     7→const buttonVariants = cva(
     8→  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
     9→  {
    10→    variants: {
    11→      variant: {
    12→        default: "bg-primary text-primary-foreground hover:bg-primary/90",
    13→        destructive:
    14→          "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
    15→        outline:
    16→          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
    17→        secondary:
    18→          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
    19→        ghost:
    20→          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
    21→        link: "text-primary underline-offset-4 hover:underline",
    22→      },
    23→      size: {
    24→        default: "h-9 px-4 py-2 has-[>svg]:px-3",
    25→        xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
    26→        sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
    27→        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
    28→        icon: "size-9",
    29→        "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
    30→        "icon-sm": "size-8",
    31→        "icon-lg": "size-10",
    32→      },
    33→    },
    34→    defaultVariants: {
    35→      variant: "default",
    36→      size: "default",
    37→    },
    38→  }
    39→)
    40→
    41→function Button({
    42→  className,
    43→  variant = "default",
    44→  size = "default",
    45→  asChild = false,
    46→  ...props
    47→}: React.ComponentProps<"button"> &
    48→  VariantProps<typeof buttonVariants> & {
    49→    asChild?: boolean
    50→  }) {
    51→  const Comp = asChild ? Slot.Root : "button"
    52→
    53→  return (
    54→    <Comp
    55→      data-slot="button"
    56→      data-variant={variant}
    57→      data-size={size}
    58→      className={cn(buttonVariants({ variant, size, className }))}
    59→      {...props}
    60→    />
    61→  )
    62→}
    63→
    64→export { Button, buttonVariants }
    65→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>