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

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

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

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

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

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

   152→            {/* Avatar */}
   153→

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

   156→
   157→

   158→

   159→

   160→                  {match.worker?.name || "Worker"}
   161→

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

   166→
   167→              {/* AI Summary */}
   168→

   169→                {summary}
   170→

   171→
   172→              {/* Details */}
   173→

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

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

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

   203→              )}
   204→

   205→

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

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

   233→          )}
   234→
   235→          {isHired && !justHired && (
   236→

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

   246→          )}
   247→        </CardContent>
   248→      </Card>
   249→

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

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

   454→    );
   455→  }
   456→
   457→  return (
   458→

   459→
   460→        {jobs.length === 0 ? (
   461→

   462→            <HardHat className="h-16 w-16 text-muted-foreground/50" />
   463→
No jobs posted yet

   464→

   465→              Post a job to start seeing matched workers.
   466→

   467→            <Button asChild className="mt-8 h-16 px-8 text-lg font-semibold">
   468→              <Link href="/contractor/post-job">Post a Job</Link>
   469→            </Button>
   470→

   471→        ) : (
   472→          <>
   473→            {/* Job selector — horizontal scroll */}
   474→            {jobs.length > 1 && (
   475→

   476→                {jobs.map((job) => (
   477→                  <button
   478→                    key={job.id}
   479→                    onClick={() => setSelectedJobId(job.id)}
   480→                    className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   481→                      selectedJobId === job.id
   482→                        ? "bg-primary text-primary-foreground"
   483→                        : "bg-muted text-muted-foreground"
   484→                    }`}
   485→                  >
   486→                    {job.title}
   487→                  </button>
   488→                ))}
   489→

   490→            )}
   491→
   492→            {/* Selected job summary */}
   493→            {selectedJobId && (
   494→

   495→                {(() => {
   496→                  const job = jobs.find((j) => j.id === selectedJobId);
   497→                  if (!job) return null;
   498→                  return (
   499→

   500→                      <Badge variant="secondary">{TRADE_LABEL[job.trade] || job.trade}</Badge>
   501→                      {job.suburb && (
   502→
   503→                          <MapPin className="h-3.5 w-3.5" />
   504→                          {job.suburb}
   505→
   506→                      )}
   507→
   508→                        {matches.length} match{matches.length !== 1 ? "es" : ""}
   509→
   510→

   511→                  );
   512→                })()}
   513→

   514→            )}
   515→
   516→            {/* Matches */}
   517→            {loadingMatches ? (
   518→

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

   521→            ) : matches.length === 0 ? (
   522→

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

   525→

   526→                  Workers will show up here as they apply. We'll notify you.
   527→

   528→

   529→            ) : (
   530→

   531→                {error && (
   532→

   533→                    <AlertCircle className="h-4 w-4 shrink-0" />
   534→                    {error}
   535→

   536→                )}
   537→
   538→                {/* Swipe hint */}
   539→                {matches.length > 0 && matches.some(m => m.status !== "hired") && (
   540→

   541→

   542→                      👉 Swipe right to hire • Swipe left to pass
   543→

   544→

   545→                )}
   546→
   547→                {matches.map((match) => (
   548→                  <SwipeableMatchCard
   549→                    key={match.id}
   550→                    match={match}
   551→                    isHired={match.status === "hired"}
   552→                    justHired={hireSuccess === match.id}
   553→                    hiring={hiring === match.id}
   554→                    onHire={handleHire}
   555→                    onReject={handleReject}
   556→                  />
   557→                ))}
   558→

   559→            )}
   560→          </>
   561→        )}
   562→
   563→
   564→      <BottomNav userType="contractor" />
   565→
   566→      {/* Payment Modal */}
   567→      {selectedMatch && companyId && (
   568→        <HirePaymentModal
   569→          isOpen={isPaymentModalOpen}
   570→          onClose={() => {
   571→            setIsPaymentModalOpen(false);
   572→            setSelectedMatch(null);
   573→          }}
   574→          onSuccess={handlePaymentSuccess}
   575→          matchId={selectedMatch.id}
   576→          companyId={companyId}
   577→          amount={50.0}
   578→          workerName={selectedMatch.worker?.name || undefined}
   579→        />
   580→      )}
   581→

   582→  );
   583→}
   584→

<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>


Let me continue reading the remaining pages and API routes:


1→'use client';
     2→
     3→import { useEffect, useState } from 'react';
     4→import { useSearchParams, useRouter } from 'next/navigation';
     5→import Link from 'next/link';
     6→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     7→import { Button } from '@/components/ui/button';
     8→import { Card, CardContent } from '@/components/ui/card';
     9→import { createClient } from '@/lib/supabase/client';
    10→
    11→export default function PaymentSuccessPage() {
    12→  const searchParams = useSearchParams();
    13→  const router = useRouter();
    14→  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
    15→  const [message, setMessage] = useState('Confirming your payment...');
    16→  const supabase = createClient();
    17→
    18→  useEffect(() => {
    19→    const paymentIntent = searchParams.get('payment_intent');
    20→    const paymentIntentClientSecret = searchParams.get('payment_intent_client_secret');
    21→
    22→    if (!paymentIntent || !paymentIntentClientSecret) {
    23→      setStatus('error');
    24→      setMessage('Invalid payment confirmation');
    25→      return;
    26→    }
    27→
    28→    // Check payment status
    29→    async function checkPayment() {
    30→      try {
    31→        // Wait a moment for webhook to process
    32→        await new Promise(resolve => setTimeout(resolve, 2000));
    33→
    34→        // Check if payment was recorded
    35→        const { data: payment } = await supabase
    36→          .from('payments')
    37→          .select('status, match_id')
    38→          .eq('stripe_payment_id', paymentIntent)
    39→          .single();
    40→
    41→        if (payment?.status === 'charged') {
    42→          setStatus('success');
    43→          setMessage('Your payment was successful and the worker has been notified!');
    44→        } else {
    45→          // Payment might still be processing via webhook
    46→          setStatus('success');
    47→          setMessage('Payment received! Processing your hire...');
    48→        }
    49→      } catch (error) {
    50→        console.error('Error checking payment:', error);
    51→        setStatus('success'); // Assume success since Stripe redirected here
    52→        setMessage('Payment received! Processing your hire...');
    53→      }
    54→    }
    55→
    56→    checkPayment();
    57→  }, [searchParams, supabase]);
    58→
    59→  return (
    60→

    61→      <Card className="w-full max-w-md">
    62→        <CardContent className="pt-8 pb-6 text-center">
    63→          {status === 'loading' && (
    64→            <>
    65→              <Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
    66→
Confirming Payment

    67→
{message}

    68→            </>
    69→          )}
    70→
    71→          {status === 'success' && (
    72→            <>
    73→

    74→                <CheckCircle2 className="h-10 w-10 text-green-600" />
    75→

    76→
Payment Successful!

    77→
{message}

    78→

    79→                <Button asChild className="w-full h-12 text-base">
    80→                  <Link href="/contractor/matches">Back to Matches</Link>
    81→                </Button>
    82→                <Button asChild variant="outline" className="w-full h-12 text-base">
    83→                  <Link href="/dashboard">Go to Dashboard</Link>
    84→                </Button>
    85→

    86→            </>
    87→          )}
    88→
    89→          {status === 'error' && (
    90→            <>
    91→

    92→                <AlertCircle className="h-10 w-10 text-destructive" />
    93→

    94→
Payment Error

    95→
{message}

    96→

    97→                <Button asChild variant="outline" className="w-full h-12 text-base">
    98→                  <Link href="/contractor/matches">Back to Matches</Link>
    99→                </Button>
   100→

   101→            </>
   102→          )}
   103→        </CardContent>
   104→      </Card>
   105→

   106→  );
   107→}
   108→

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

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

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

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

   127→

   128→

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

   139→
{job.title}

   140→

   141→

   142→
   143→

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

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

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

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

   268→    );
   269→  }
   270→
   271→  return (
   272→

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

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

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

   294→

   295→              👉 Swipe right to quick apply • Tap to view details
   296→

   297→

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

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

   305→

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

   310→

   311→        ) : (
   312→

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

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

   329→
 setSelectedJob(null)}
   332→          />
   333→

   334→            {/* Handle */}
   335→

   336→

   337→

   338→
   339→

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

   344→
   345→

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

   364→
   365→

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

   387→
   388→              {selectedJob.description && (
   389→

   390→
About the Job

   391→

   392→                    {selectedJob.description}
   393→

   394→

   395→              )}
   396→
   397→              {selectedJob.certifications_required?.length > 0 && (
   398→

   399→
Required Certs

   400→

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

   407→

   408→              )}
   409→

   410→
   411→            {/* Sticky Bottom CTA */}
   412→

   413→

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

   439→                  Always free for workers. No fees ever.
   440→

   441→

   442→

   443→

   444→

   445→      )}
   446→
   447→      <BottomNav userType="worker" />
   448→

   449→  );
   450→}
   451→

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

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

   273→    );
   274→  }
   275→
   276→  return (
   277→

   278→      {/* Header */}
   279→
   280→

   281→          <Link href="/" className="flex items-center gap-2">
   282→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
   283→          </Link>
   284→

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

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

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

   303→

   304→

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

   307→

   308→
Create your account

   309→

   310→                  Always free for workers
   311→

   312→

   313→

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

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

   329→

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

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

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

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

   376→

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

   382→

   383→

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

   386→

   387→
Build your profile

   388→

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

   391→

   392→

   393→
   394→

   395→              <button
   396→                onClick={() => setStep("trade")}
   397→                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"
   398→              >
   399→

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

   402→

   403→
Type my answers

   404→

   405→                    5 quick questions, under 2 minutes
   406→

   407→

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

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

   418→

   419→
Send a voice note

   420→

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

   423→

   424→              </button>
   425→

   426→

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

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

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

   453→
   454→

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

   471→

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

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

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

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

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

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

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

   540→
   541→

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

   559→            </form>
   560→

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

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

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

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

   590→
{opt.desc}

   591→

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

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

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

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

   617→              {CERT_OPTIONS.map((cert) => (
   618→                <button
   619→                  key={cert}
   620→                  onClick={() => toggleCert(cert)}
   621→                  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 ${
   622→                    certifications.includes(cert)
   623→                      ? "border-primary bg-primary/10 text-primary"
   624→                      : "border-border hover:border-primary/50"
   625→                  }`}
   626→                >
   627→

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

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

   642→
   643→

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

   659→

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

   665→

   666→

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

   669→

   670→
Your profile

   671→

   672→                  Review and confirm
   673→

   674→

   675→

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

   728→

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

   731→

   732→                    AI-generated bio
   733→

   734→

   735→                    {generateBio(workerData)}
   736→

   737→

   738→

   739→

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

   767→        )}
   768→
   769→

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

   784→

   785→        {icon}
   786→

   787→

   788→
{title}

   789→
{subtitle}

   790→

   791→

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

   812→

   813→
{label}

   814→
{value}

   815→

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

<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 the worker profile and messages pages, plus API routes:


1→"use client";
     2→
     3→import { useState, useEffect } 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 { BottomNav } from "@/components/bottom-nav";
    10→import { Search, MessageSquare } from "lucide-react";
    11→import { createClient } from "@/lib/supabase/client";
    12→
    13→interface Conversation {
    14→  conversation_id: string;
    15→  other_participant_name: string;
    16→  other_participant_avatar: string | null;
    17→  last_message: string | null;
    18→  last_message_time: string | null;
    19→  unread_count: number;
    20→  other_participant_is_online: boolean;
    21→}
    22→
    23→export default function MessagesPage() {
    24→  const router = useRouter();
    25→  const [searchQuery, setSearchQuery] = useState("");
    26→  const [conversations, setConversations] = useState<Conversation[]>([]);
    27→  const [loading, setLoading] = useState(true);
    28→  const [userType, setUserType] = useState<"contractor" | "worker">("worker");
    29→  const supabase = createClient();
    30→
    31→  useEffect(() => {
    32→    fetchConversations();
    33→    fetchUserType();
    34→
    35→    // Subscribe to new messages for real-time updates
    36→    const channel = supabase
    37→      .channel('messages')
    38→      .on('postgres_changes',
    39→        {
    40→          event: 'INSERT',
    41→          schema: 'public',
    42→          table: 'messages'
    43→        },
    44→        () => {
    45→          fetchConversations();
    46→        }
    47→      )
    48→      .subscribe();
    49→
    50→    return () => {
    51→      supabase.removeChannel(channel);
    52→    };
    53→  }, []);
    54→
    55→  const fetchUserType = async () => {
    56→    const { data: { user } } = await supabase.auth.getUser();
    57→    if (user) {
    58→      const { data: profile } = await supabase
    59→        .from("profiles")
    60→        .select("type")
    61→        .eq("id", user.id)
    62→        .single();
    63→      if (profile?.type) {
    64→        setUserType(profile.type as "contractor" | "worker");
    65→      }
    66→    }
    67→  };
    68→
    69→  const fetchConversations = async () => {
    70→    try {
    71→      const response = await fetch('/api/messages');
    72→      if (response.ok) {
    73→        const data = await response.json();
    74→        setConversations(data.conversations || []);
    75→      }
    76→    } catch (error) {
    77→      console.error("Error fetching conversations:", error);
    78→    } finally {
    79→      setLoading(false);
    80→    }
    81→  };
    82→
    83→  const filteredConversations = conversations.filter((conversation) =>
    84→    conversation.other_participant_name?.toLowerCase().includes(searchQuery.toLowerCase())
    85→  );
    86→
    87→  if (loading) {
    88→    return (
    89→

    90→

    91→

    92→            <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
    93→            <Input
    94→              type="search"
    95→              placeholder="Search conversations..."
    96→              value={searchQuery}
    97→              onChange={(e) => setSearchQuery(e.target.value)}
    98→              className="h-12 pl-10 text-base"
    99→              disabled
   100→            />
   101→

   102→

   103→

   104→
Loading conversations...

   105→

   106→        <BottomNav userType={userType} />
   107→

   108→    );
   109→  }
   110→
   111→  return (
   112→

   113→      {/* Search Bar */}
   114→

   115→

   116→          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
   117→          <Input
   118→            type="search"
   119→            placeholder="Search conversations..."
   120→            value={searchQuery}
   121→            onChange={(e) => setSearchQuery(e.target.value)}
   122→            className="h-12 pl-10 text-base"
   123→          />
   124→

   125→

   126→
   127→      {/* Conversations List */}
   128→
   129→        {filteredConversations.length === 0 ? (
   130→

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

   133→

   134→              {searchQuery
   135→                ? "No conversations match your search"
   136→                : "Start a conversation by hiring a worker or applying for a job"}
   137→

   138→            {!searchQuery && (
   139→              <Button asChild>
   140→                <Link href="/dashboard">Go to Dashboard</Link>
   141→              </Button>
   142→            )}
   143→

   144→        ) : (
   145→

   146→            {filteredConversations.map((conversation) => (
   147→              <ConversationPreview
   148→                key={conversation.conversation_id}
   149→                id={conversation.conversation_id}
   150→                name={conversation.other_participant_name}
   151→                avatar={conversation.other_participant_avatar || undefined}
   152→                lastMessage={conversation.last_message || "No messages yet"}
   153→                lastMessageTime={conversation.last_message_time ? new Date(conversation.last_message_time) : new Date()}
   154→                unreadCount={conversation.unread_count}
   155→                isOnline={conversation.other_participant_is_online}
   156→                onClick={() => router.push(`/messages/${conversation.conversation_id}`)}
   157→              />
   158→            ))}
   159→

   160→        )}
   161→
   162→
   163→      <BottomNav userType={userType} />
   164→

   165→  );
   166→}
   167→

<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>