Thoroughly explore the following directories and read all layout.tsx and page.tsx files within them. I need the full content of each file found:
- /home/ccuser/rateright-v2/src/app/dashboard/
- /home/ccuser/rateright-v2/src/app/contractor/
- /home/ccuser/rateright-v2/src/app/worker/
- /home/ccuser/rateright-v2/src/app/messages/

Also check for any header or nav components in /home/ccuser/rateright-v2/src/components/ that might be duplicated across these pages.

List ALL files found in each directory (recursively) and show the full content of every layout.tsx and page.tsx file.


I'll thoroughly explore these directories to find all layout.tsx and page.tsx files, along with checking for shared header/nav components. Let me start with a comprehensive search.


Bash

find /home/ccuser/rateright-v2/src/app/dashboard -type f -name "*.tsx" | sort

Find all TypeScript files in dashboard directory


find /home/ccuser/rateright-v2/src/app/contractor -type f -name "*.tsx" | sort

Find all TypeScript files in contractor directory


/home/ccuser/rateright-v2/src/app/contractor/matches/page.tsx
/home/ccuser/rateright-v2/src/app/contractor/post-job/page.tsx
/home/ccuser/rateright-v2/src/app/contractor/signup/page.tsx


find /home/ccuser/rateright-v2/src/app/worker -type f -name "*.tsx" | sort

Find all TypeScript files in worker directory


/home/ccuser/rateright-v2/src/app/worker/jobs/page.tsx
/home/ccuser/rateright-v2/src/app/worker/profile/build/page.tsx
/home/ccuser/rateright-v2/src/app/worker/signup/page.tsx


find /home/ccuser/rateright-v2/src/app/messages -type f -name "*.tsx" | sort

Find all TypeScript files in messages directory


/home/ccuser/rateright-v2/src/app/messages/[conversationId]/page.tsx
/home/ccuser/rateright-v2/src/app/messages/page.tsx


find /home/ccuser/rateright-v2/src/components -type f \( -name "*header*" -o -name "*nav*" \) -iname "*.tsx" | sort

Find header and nav components in components directory


/home/ccuser/rateright-v2/src/components/bottom-nav-wrapper.tsx
/home/ccuser/rateright-v2/src/components/bottom-nav.tsx


Now let me check for layout.tsx files in these directories:


find /home/ccuser/rateright-v2/src/app/dashboard -type f -name "layout.tsx"

Find layout.tsx in dashboard


find /home/ccuser/rateright-v2/src/app/contractor -type f -name "layout.tsx"

Find layout.tsx in contractor


find /home/ccuser/rateright-v2/src/app/worker -type f -name "layout.tsx"

Find layout.tsx in worker


find /home/ccuser/rateright-v2/src/app/messages -type f -name "layout.tsx"

Find layout.tsx in messages


Let me get a comprehensive list of all files in these directories:


find /home/ccuser/rateright-v2/src/app/dashboard -type f | sort

List all files in dashboard directory


find /home/ccuser/rateright-v2/src/app/contractor -type f | sort

List all files in contractor directory


find /home/ccuser/rateright-v2/src/app/worker -type f | sort

List all files in worker directory


find /home/ccuser/rateright-v2/src/app/messages -type f | sort

List all files in messages directory


Now let me read all the layout.tsx and page.tsx files I found. I'll also check for layout files in parent directories:


1→import { redirect } from "next/navigation";
     2→import Link from "next/link";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import { Building2, HardHat, Plus, Briefcase, Users } from "lucide-react";
     8→import { SignOutButton } from "@/components/sign-out-button";
     9→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    10→
    11→export default async function DashboardPage() {
    12→  const supabase = await createClient();
    13→
    14→  const {
    15→    data: { user },
    16→  } = await supabase.auth.getUser();
    17→
    18→  if (!user) {
    19→    redirect("/auth/login");
    20→  }
    21→
    22→  const { data: profile } = await supabase
    23→    .from("profiles")
    24→    .select("*")
    25→    .eq("id", user.id)
    26→    .single();
    27→
    28→  const isContractor = profile?.type === "contractor";
    29→
    30→  // Fetch stats
    31→  let jobCount = 0;
    32→  let matchCount = 0;
    33→
    34→  if (isContractor) {
    35→    const { data: company } = await supabase
    36→      .from("companies")
    37→      .select("id")
    38→      .eq("profile_id", user.id)
    39→      .single();
    40→
    41→    if (company) {
    42→      const { count } = await supabase
    43→        .from("jobs")
    44→        .select("*", { count: "exact", head: true })
    45→        .eq("company_id", company.id)
    46→        .eq("status", "active");
    47→      jobCount = count || 0;
    48→
    49→      const { count: mCount } = await supabase
    50→        .from("matches")
    51→        .select("*, jobs!inner(company_id)", { count: "exact", head: true })
    52→        .eq("jobs.company_id", company.id)
    53→        .in("status", ["applied", "suggested"]);
    54→      matchCount = mCount || 0;
    55→    }
    56→  }
    57→
    58→  return (
    59→

    60→      {/* Header */}
    61→
    62→

    63→          <Link href="/dashboard" className="text-xl font-bold tracking-tight">
    64→            RateRight
    65→          </Link>
    66→

    67→            <Badge variant="secondary" className="text-sm py-1.5 px-3">
    68→              {isContractor ? "Contractor" : "Worker"}
    69→            </Badge>
    70→            <SignOutButton />
    71→

    72→

    73→
    74→
    75→
    76→

    77→          G'day{profile?.name ? `, ${profile.name}` : ""}!
    78→

    79→

    80→          {isContractor
    81→            ? "Ready to find your next worker?"
    82→            : "Let's get you on a job."}
    83→

    84→
    85→

    86→          {isContractor ? (
    87→            <>
    88→              {/* Quick stats */}
    89→

    90→                <Card>
    91→                  <CardContent className="flex flex-col items-center p-4">
    92→                    {jobCount}
    93→                    Active Jobs
    94→                  </CardContent>
    95→                </Card>
    96→                <Card>
    97→                  <CardContent className="flex flex-col items-center p-4">
    98→                    {matchCount}
    99→                    New Matches
   100→                  </CardContent>
   101→                </Card>
   102→

   103→
   104→              {/* Post a Job CTA */}
   105→              <Card className="border-primary/30 bg-primary/5">
   106→                <CardHeader className="pb-3">
   107→                  <CardTitle className="flex items-center gap-2 text-lg">
   108→                    <Plus className="h-6 w-6" />
   109→                    Post a Job
   110→                  </CardTitle>
   111→                  <CardDescription className="text-base">
   112→                    AI writes the post. You just tap.
   113→                  </CardDescription>
   114→                </CardHeader>
   115→                <CardContent>
   116→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   117→                    <Link href="/contractor/post-job">Post a Job</Link>
   118→                  </Button>
   119→                </CardContent>
   120→              </Card>
   121→
   122→              {/* View Matches */}
   123→              {matchCount > 0 && (
   124→                <Card>
   125→                  <CardHeader className="pb-3">
   126→                    <CardTitle className="flex items-center gap-2 text-lg">
   127→                      <Users className="h-6 w-6" />
   128→                      {matchCount} Worker{matchCount > 1 ? "s" : ""} Matched
   129→                    </CardTitle>
   130→                    <CardDescription className="text-base">
   131→                      Workers have applied to your jobs
   132→                    </CardDescription>
   133→                  </CardHeader>
   134→                  <CardContent>
   135→                    <Button asChild variant="outline" className="w-full h-14 text-base font-semibold">
   136→                      <Link href="/contractor/matches">View Matches</Link>
   137→                    </Button>
   138→                  </CardContent>
   139→                </Card>
   140→              )}
   141→
   142→              {/* Company profile */}
   143→              <Card>
   144→                <CardHeader className="pb-3">
   145→                  <CardTitle className="flex items-center gap-2 text-lg">
   146→                    <Building2 className="h-6 w-6" />
   147→                    Company Profile
   148→                  </CardTitle>
   149→                  <CardDescription className="text-base">
   150→                    Manage your company details
   151→                  </CardDescription>
   152→                </CardHeader>
   153→                <CardContent>
   154→                  <Button variant="outline" className="w-full h-14 text-base">
   155→                    Edit Profile
   156→                  </Button>
   157→                </CardContent>
   158→              </Card>
   159→            </>
   160→          ) : (
   161→            <>
   162→              {/* Browse Jobs CTA */}
   163→              <Card className="border-primary/30 bg-primary/5">
   164→                <CardHeader className="pb-3">
   165→                  <CardTitle className="flex items-center gap-2 text-lg">
   166→                    <Briefcase className="h-6 w-6" />
   167→                    Find Work
   168→                  </CardTitle>
   169→                  <CardDescription className="text-base">
   170→                    Browse available construction jobs near you
   171→                  </CardDescription>
   172→                </CardHeader>
   173→                <CardContent>
   174→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   175→                    <Link href="/worker/jobs">Browse Jobs</Link>
   176→                  </Button>
   177→                </CardContent>
   178→              </Card>
   179→
   180→              {/* Profile */}
   181→              <Card>
   182→                <CardHeader className="pb-3">
   183→                  <CardTitle className="flex items-center gap-2 text-lg">
   184→                    <HardHat className="h-6 w-6" />
   185→                    Your Profile
   186→                  </CardTitle>
   187→                  <CardDescription className="text-base">
   188→                    A strong profile gets you more matches
   189→                  </CardDescription>
   190→                </CardHeader>
   191→                <CardContent>
   192→                  <Button variant="outline" className="w-full h-14 text-base">
   193→                    Edit Profile
   194→                  </Button>
   195→                </CardContent>
   196→              </Card>
   197→            </>
   198→          )}
   199→

   200→
   201→
   202→      <BottomNavWrapper userType={profile?.type || "worker"} />
   203→

   204→  );
   205→}
   206→

<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→  Building2,
    13→  Search,
    14→  Sparkles,
    15→  CheckCircle2,
    16→  ArrowLeft,
    17→  ArrowRight,
    18→  Loader2,
    19→  Edit3,
    20→  Globe,
    21→  MapPin,
    22→  Users,
    23→  FileText,
    24→  HardHat,
    25→} from "lucide-react";
    26→import { guessCompanyDomain } from "@/lib/company-utils";
    27→import { validateABN, formatABN } from "@/lib/abn-utils";
    28→
    29→type Step = "auth" | "company" | "researching" | "review";
    30→
    31→interface CompanyData {
    32→  name: string;
    33→  abn: string;
    34→  address: string;
    35→  industry: string;
    36→  size: string;
    37→  website: string;
    38→  logoUrl?: string | null;
    39→  logoSource?: 'clearbit' | 'google' | 'none';
    40→}
    41→
    42→async function lookupABN(abn: string): Promise<CompanyData> {
    43→  // Clean the ABN - remove spaces
    44→  const cleanAbn = abn.replace(/\s/g, '');
    45→
    46→  // Call the ABN lookup API
    47→  const response = await fetch(`/api/abn-lookup?abn=${cleanAbn}`);
    48→
    49→  if (!response.ok) {
    50→    const errData = await response.json().catch(() => null);
    51→    throw new Error(errData?.error || 'Failed to lookup ABN');
    52→  }
    53→
    54→  const result = await response.json();
    55→
    56→  if (!result.success || !result.data) {
    57→    throw new Error(result.error || 'Unable to lookup ABN');
    58→  }
    59→
    60→  const data = result.data;
    61→
    62→  // Map the ABR data to our CompanyData format
    63→  const companyData: CompanyData = {
    64→    name: data.name || 'Unknown Company',
    65→    abn: formatABN(data.abn || cleanAbn),
    66→    address: [data.postcode, data.state].filter(Boolean).join(' ') || 'Address not available',
    67→    industry: 'Construction',
    68→    size: 'Unknown',
    69→    website: '',
    70→  };
    71→
    72→  // Try to find company logo
    73→  await fetchCompanyLogo(companyData);
    74→
    75→  return companyData;
    76→}
    77→
    78→async function fetchCompanyLogo(companyData: CompanyData): Promise<void> {
    79→  try {
    80→    // If we have an explicit website, try that first
    81→    if (companyData.website) {
    82→      const domain = extractDomainFromUrl(companyData.website);
    83→      if (domain) {
    84→        const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
    85→        if (logoResponse.ok) {
    86→          const logoData = await logoResponse.json();
    87→          companyData.logoUrl = logoData.logoUrl;
    88→          companyData.logoSource = logoData.source;
    89→          return;
    90→        }
    91→      }
    92→    }
    93→
    94→    // Otherwise, try to guess domains from company name
    95→    const possibleDomains = guessCompanyDomain(companyData.name);
    96→
    97→    // Try each domain until we find a logo
    98→    for (const domain of possibleDomains) {
    99→      const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
   100→      if (logoResponse.ok) {
   101→        const logoData = await logoResponse.json();
   102→        if (logoData.logoUrl) {
   103→          companyData.logoUrl = logoData.logoUrl;
   104→          companyData.logoSource = logoData.source;
   105→          return;
   106→        }
   107→      }
   108→    }
   109→
   110→    // No logo found
   111→    companyData.logoUrl = null;
   112→    companyData.logoSource = 'none';
   113→  } catch (error) {
   114→    console.error('Logo fetch failed:', error);
   115→    companyData.logoUrl = null;
   116→    companyData.logoSource = 'none';
   117→  }
   118→}
   119→
   120→function extractDomainFromUrl(url: string): string | null {
   121→  try {
   122→    // Add protocol if missing
   123→    if (!url.match(/^https?:\/\//)) {
   124→      url = 'https://' + url;
   125→    }
   126→
   127→    const urlObj = new URL(url);
   128→    return urlObj.hostname.replace(/^www\./, '');
   129→  } catch {
   130→    return null;
   131→  }
   132→}
   133→
   134→export default function ContractorSignupPage() {
   135→  const [step, setStep] = useState<Step>("auth");
   136→  const [isAuthenticated, setIsAuthenticated] = useState(false);
   137→  const [loading, setLoading] = useState(true);
   138→
   139→  // Auth state
   140→  const [email, setEmail] = useState("");
   141→  const [password, setPassword] = useState("");
   142→  const [authError, setAuthError] = useState<string | null>(null);
   143→  const [authLoading, setAuthLoading] = useState(false);
   144→
   145→  // Company state
   146→  const [companyInput, setCompanyInput] = useState("");
   147→  const [companyData, setCompanyData] = useState<CompanyData | null>(null);
   148→  const [companyError, setCompanyError] = useState<string | null>(null);
   149→  const [editField, setEditField] = useState<string | null>(null);
   150→  const [saving, setSaving] = useState(false);
   151→
   152→  const router = useRouter();
   153→  const supabase = createClient();
   154→
   155→  useEffect(() => {
   156→    async function checkAuth() {
   157→      const {
   158→        data: { user },
   159→      } = await supabase.auth.getUser();
   160→      if (user) {
   161→        setIsAuthenticated(true);
   162→        setStep("company");
   163→      }
   164→      setLoading(false);
   165→    }
   166→    checkAuth();
   167→  }, [supabase.auth]);
   168→
   169→  async function handleAuth(e: React.FormEvent) {
   170→    e.preventDefault();
   171→    setAuthLoading(true);
   172→    setAuthError(null);
   173→
   174→    const { error } = await supabase.auth.signUp({
   175→      email,
   176→      password,
   177→      options: {
   178→        data: { type: "contractor" },
   179→        emailRedirectTo: `${window.location.origin}/auth/callback`,
   180→      },
   181→    });
   182→
   183→    if (error) {
   184→      if (error.message.includes("already registered")) {
   185→        const { error: loginError } = await supabase.auth.signInWithPassword({
   186→          email,
   187→          password,
   188→        });
   189→        if (loginError) {
   190→          setAuthError(loginError.message);
   191→          setAuthLoading(false);
   192→          return;
   193→        }
   194→        // If login successful, check if email is verified
   195→        const { data: { user } } = await supabase.auth.getUser();
   196→        if (user && !user.email_confirmed_at) {
   197→          // Email not verified, redirect to verification page
   198→          router.push("/auth/verify-email");
   199→          return;
   200→        }
   201→      } else {
   202→        setAuthError(error.message);
   203→        setAuthLoading(false);
   204→        return;
   205→      }
   206→    }
   207→
   208→    // Check if email verification is required
   209→    const { data: { user } } = await supabase.auth.getUser();
   210→    if (user && !user.email_confirmed_at) {
   211→      // Email not verified, redirect to verification page
   212→      router.push("/auth/verify-email");
   213→      return;
   214→    }
   215→
   216→    setIsAuthenticated(true);
   217→    setStep("company");
   218→    setAuthLoading(false);
   219→  }
   220→
   221→  async function handleCompanySearch(e: React.FormEvent) {
   222→    e.preventDefault();
   223→    if (!companyInput.trim()) return;
   224→    setCompanyError(null);
   225→
   226→    const input = companyInput.trim();
   227→
   228→    // Validate ABN checksum using the shared utility
   229→    if (!validateABN(input)) {
   230→      setCompanyError("Please enter a valid 11-digit ABN. Check the number and try again.");
   231→      return;
   232→    }
   233→
   234→    setStep("researching");
   235→
   236→    try {
   237→      const data = await lookupABN(input);
   238→      setCompanyData(data);
   239→      setStep("review");
   240→    } catch (error) {
   241→      setCompanyError(error instanceof Error ? error.message : 'Failed to lookup ABN');
   242→      setStep("company");
   243→    }
   244→  }
   245→
   246→  async function handleConfirm() {
   247→    if (!companyData) return;
   248→    setSaving(true);
   249→
   250→    const {
   251→      data: { user },
   252→    } = await supabase.auth.getUser();
   253→    if (!user) return;
   254→
   255→    await supabase.from("profiles").update({
   256→      name: companyData.name,
   257→      onboarding_completed: true,
   258→    }).eq("id", user.id);
   259→
   260→    await supabase.from("companies").insert({
   261→      profile_id: user.id,
   262→      name: companyData.name,
   263→      abn: companyData.abn,
   264→      address: companyData.address,
   265→      industry: companyData.industry,
   266→      size: companyData.size,
   267→      website: companyData.website,
   268→      abr_verified: {
   269→        source: "abr_verified",
   270→        abn: companyData.abn,
   271→        verified_at: new Date().toISOString(),
   272→      },
   273→    });
   274→
   275→    router.push("/dashboard");
   276→    router.refresh();
   277→  }
   278→
   279→  function updateCompanyField(field: keyof CompanyData, value: string) {
   280→    if (!companyData) return;
   281→    setCompanyData({ ...companyData, [field]: value });
   282→    setEditField(null);
   283→  }
   284→
   285→  const totalSteps = isAuthenticated ? 3 : 4;
   286→  const currentStep =
   287→    step === "auth" ? 0 : step === "company" ? 1 : step === "researching" ? 2 : 3;
   288→
   289→  if (loading) {
   290→    return (
   291→

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

   294→    );
   295→  }
   296→
   297→  return (
   298→

   299→      {/* Header */}
   300→
   301→

   302→          <Link href="/" className="flex items-center gap-2">
   303→

   304→              <HardHat className="h-5 w-5 text-primary-foreground" />
   305→

   306→            RateRight
   307→          </Link>
   308→

   309→
   310→
   311→
   312→        {/* Progress bar */}
   313→

   314→          {Array.from({ length: totalSteps }, (_, i) => (
   315→            <div
   316→              key={i}
   317→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   318→                i <= currentStep - (isAuthenticated ? 1 : 0)
   319→                  ? "bg-primary"
   320→                  : "bg-muted"
   321→              }`}
   322→            />
   323→          ))}
   324→

   325→
   326→        {/* Step: Auth */}
   327→        {step === "auth" && (
   328→

   329→

   330→

   331→                <Building2 className="h-5 w-5" />
   332→

   333→

   334→
Create your account

   335→

   336→                  Quick signup to get started
   337→

   338→

   339→

   340→
   341→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   342→

   343→                Email
   344→                <Input
   345→                  id="email"
   346→                  type="email"
   347→                  placeholder="you@company.com.au"
   348→                  value={email}
   349→                  onChange={(e) => setEmail(e.target.value)}
   350→                  required
   351→                  autoComplete="email"
   352→                  className="h-12 text-base"
   353→                />
   354→

   355→

   356→                Password
   357→                <Input
   358→                  id="password"
   359→                  type="password"
   360→                  placeholder="Create a password"
   361→                  value={password}
   362→                  onChange={(e) => setPassword(e.target.value)}
   363→                  required
   364→                  minLength={6}
   365→                  autoComplete="new-password"
   366→                  className="h-12 text-base"
   367→                />
   368→

   369→
   370→              {authError && (
   371→
{authError}

   372→              )}
   373→
   374→              <Button
   375→                type="submit"
   376→                className="h-12 w-full gap-2 text-base font-semibold"
   377→                disabled={authLoading}
   378→              >
   379→                {authLoading ? (
   380→                  <>
   381→                    <Loader2 className="h-5 w-5 animate-spin" />
   382→                    Creating account...
   383→                  </>
   384→                ) : (
   385→                  <>
   386→                    Continue
   387→                    <ArrowRight className="h-5 w-5" />
   388→                  </>
   389→                )}
   390→              </Button>
   391→            </form>
   392→
   393→

   394→              Already have an account?{" "}
   395→              <Link
   396→                href="/auth/login"
   397→                className="text-primary underline hover:no-underline"
   398→              >
   399→                Sign in
   400→              </Link>
   401→

   402→

   403→        )}
   404→
   405→        {/* Step: Enter company name/ABN */}
   406→        {step === "company" && (
   407→

   408→

   409→

   410→                <Search className="h-5 w-5" />
   411→

   412→

   413→
Your company

   414→

   415→                  We'll verify it via ABR
   416→

   417→

   418→

   419→
   420→            <form onSubmit={handleCompanySearch} className="mt-6 space-y-4">
   421→

   422→                ABN
   423→                <Input
   424→                  id="company"
   425→                  type="text"
   426→                  placeholder="Enter your 11-digit ABN"
   427→                  value={companyInput}
   428→                  onChange={(e) => setCompanyInput(e.target.value)}
   429→                  required
   430→                  autoFocus
   431→                  className="h-12 text-base"
   432→                />
   433→

   434→                  Enter your 11-digit ABN for verification via the Australian Business Register.
   435→

   436→

   437→
   438→              {companyError && (
   439→
{companyError}

   440→              )}
   441→
   442→              <Button
   443→                type="submit"
   444→                className="h-12 w-full gap-2 text-base font-semibold"
   445→              >
   446→                <Search className="h-5 w-5" />
   447→                Verify my ABN
   448→              </Button>
   449→            </form>
   450→
   451→            {!isAuthenticated && (
   452→              <button
   453→                onClick={() => setStep("auth")}
   454→                className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   455→              >
   456→                <ArrowLeft className="h-4 w-4" />
   457→                Back
   458→              </button>
   459→            )}
   460→

   461→        )}
   462→
   463→        {/* Step: ABR Verification */}
   464→        {step === "researching" && (
   465→

   466→

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

   469→
Verifying your business

   470→

   471→              Checking ABR for your business details...
   472→

   473→
   474→

   475→              <ResearchStep label="Checking ABR..." />
   476→              <ResearchStep label="Verifying company..." />
   477→              <ResearchStep label="Done" />
   478→

   479→

   480→        )}
   481→
   482→        {/* Step: Review profile */}
   483→        {step === "review" && companyData && (
   484→

   485→

   486→

   487→                <CheckCircle2 className="h-5 w-5" />
   488→

   489→

   490→
Confirm your profile

   491→

   492→                  Tap any field to edit
   493→

   494→

   495→

   496→
   497→            <Card className="mt-6">
   498→              <CardContent className="space-y-4 pt-6">
   499→                <EditableField
   500→                  icon={<Building2 className="h-4 w-4" />}
   501→                  label="Company name"
   502→                  value={companyData.name}
   503→                  fieldKey="name"
   504→                  editField={editField}
   505→                  setEditField={setEditField}
   506→                  onSave={(v) => updateCompanyField("name", v)}
   507→                  logoUrl={companyData.logoUrl}
   508→                />
   509→                <EditableField
   510→                  icon={<FileText className="h-4 w-4" />}
   511→                  label="ABN"
   512→                  value={companyData.abn}
   513→                  fieldKey="abn"
   514→                  editField={editField}
   515→                  setEditField={setEditField}
   516→                  onSave={(v) => updateCompanyField("abn", v)}
   517→                />
   518→                <EditableField
   519→                  icon={<MapPin className="h-4 w-4" />}
   520→                  label="Address"
   521→                  value={companyData.address}
   522→                  fieldKey="address"
   523→                  editField={editField}
   524→                  setEditField={setEditField}
   525→                  onSave={(v) => updateCompanyField("address", v)}
   526→                />
   527→                <EditableField
   528→                  icon={<Building2 className="h-4 w-4" />}
   529→                  label="Industry"
   530→                  value={companyData.industry}
   531→                  fieldKey="industry"
   532→                  editField={editField}
   533→                  setEditField={setEditField}
   534→                  onSave={(v) => updateCompanyField("industry", v)}
   535→                />
   536→                <EditableField
   537→                  icon={<Users className="h-4 w-4" />}
   538→                  label="Company size"
   539→                  value={companyData.size}
   540→                  fieldKey="size"
   541→                  editField={editField}
   542→                  setEditField={setEditField}
   543→                  onSave={(v) => updateCompanyField("size", v)}
   544→                />
   545→                <EditableField
   546→                  icon={<Globe className="h-4 w-4" />}
   547→                  label="Website"
   548→                  value={companyData.website}
   549→                  fieldKey="website"
   550→                  editField={editField}
   551→                  setEditField={setEditField}
   552→                  onSave={(v) => updateCompanyField("website", v)}
   553→                />
   554→              </CardContent>
   555→            </Card>
   556→
   557→

   558→

   559→                <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-600" />
   560→

   561→

   562→                    Verified via Australian Business Register
   563→

   564→

   565→                    Your ABN has been verified. Tap any field to correct it.
   566→

   567→

   568→

   569→

   570→
   571→            <Button
   572→              onClick={handleConfirm}
   573→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   574→              disabled={saving}
   575→            >
   576→              {saving ? (
   577→                <>
   578→                  <Loader2 className="h-5 w-5 animate-spin" />
   579→                  Saving...
   580→                </>
   581→              ) : (
   582→                <>
   583→                  <CheckCircle2 className="h-5 w-5" />
   584→                  Looks good — create my profile
   585→                </>
   586→              )}
   587→            </Button>
   588→
   589→            <button
   590→              onClick={() => setStep("company")}
   591→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   592→            >
   593→              <ArrowLeft className="h-4 w-4" />
   594→              Search again
   595→            </button>
   596→

   597→        )}
   598→
   599→

   600→  );
   601→}
   602→
   603→function ResearchStep({ label }: { label: string }) {
   604→  return (
   605→

   606→      <Loader2 className="h-4 w-4 animate-spin text-primary" />
   607→      {label}
   608→

   609→  );
   610→}
   611→
   612→function EditableField({
   613→  icon,
   614→  label,
   615→  value,
   616→  fieldKey,
   617→  editField,
   618→  setEditField,
   619→  onSave,
   620→  logoUrl,
   621→}: {
   622→  icon: React.ReactNode;
   623→  label: string;
   624→  value: string;
   625→  fieldKey: string;
   626→  editField: string | null;
   627→  setEditField: (f: string | null) => void;
   628→  onSave: (value: string) => void;
   629→  logoUrl?: string | null;
   630→}) {
   631→  const [editValue, setEditValue] = useState(value);
   632→  const isEditing = editField === fieldKey;
   633→
   634→  if (isEditing) {
   635→    return (
   636→

   637→        {label}
   638→

   639→          {fieldKey === 'name' && logoUrl ? (
   640→

   641→              <img
   642→                src={logoUrl}
   643→                alt={`${value} logo`}
   644→                className="h-8 w-8 rounded-full object-cover"
   645→                onError={(e) => {
   646→                  e.currentTarget.style.display = 'none';
   647→                }}
   648→              />
   649→              <Input
   650→                value={editValue}
   651→                onChange={(e) => setEditValue(e.target.value)}
   652→                className="h-10 text-base"
   653→                autoFocus
   654→              />
   655→

   656→          ) : (
   657→            <Input
   658→              value={editValue}
   659→              onChange={(e) => setEditValue(e.target.value)}
   660→              className="h-10 text-base"
   661→              autoFocus
   662→            />
   663→          )}
   664→          <Button
   665→            size="sm"
   666→            onClick={() => onSave(editValue)}
   667→            className="h-10 shrink-0"
   668→          >
   669→            Save
   670→          </Button>
   671→

   672→

   673→    );
   674→  }
   675→
   676→  return (
   677→    <button
   678→      onClick={() => {
   679→        setEditValue(value);
   680→        setEditField(fieldKey);
   681→      }}
   682→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   683→    >
   684→      {fieldKey === 'name' && logoUrl ? (
   685→

   686→          <img
   687→            src={logoUrl}
   688→            alt={`${value} logo`}
   689→            className="h-6 w-6 rounded-full object-cover"
   690→            onError={(e) => {
   691→              // If image fails to load, fall back to icon
   692→              e.currentTarget.style.display = 'none';
   693→            }}
   694→          />
   695→

   696→      ) : (
   697→
{icon}

   698→      )}
   699→

   700→
{label}

   701→
{value}

   702→

   703→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   704→    </button>
   705→  );
   706→}
   707→

<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 { 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 { Card, CardContent } from "@/components/ui/card";
     9→import { Input } from "@/components/ui/input";
    10→import { Label } from "@/components/ui/label";
    11→import { Badge } from "@/components/ui/badge";
    12→import {
    13→  ArrowLeft,
    14→  ArrowRight,
    15→  Loader2,
    16→  Sparkles,
    17→  Check,
    18→  MapPin,
    19→  Calendar,
    20→  Users,
    21→  DollarSign,
    22→  HardHat,
    23→  Wrench,
    24→  Hammer,
    25→  Blocks,
    26→  Fence,
    27→  CircleDot,
    28→  Shovel,
    29→} from "lucide-react";
    30→
    31→const TRADE_CATEGORIES = [
    32→  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
    33→  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
    34→  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
    35→  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
    36→  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
    37→  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
    38→  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
    39→];
    40→
    41→const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
    42→  "steel-fixer": {
    43→    title: "Steel Fixer Needed",
    44→    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    45→    certs: ["White Card", "Working at Heights"],
    46→    rateMin: 45,
    47→    rateMax: 65,
    48→    rateType: "hourly",
    49→  },
    50→  "formworker": {
    51→    title: "Formworker Required",
    52→    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    53→    certs: ["White Card"],
    54→    rateMin: 42,
    55→    rateMax: 58,
    56→    rateType: "hourly",
    57→  },
    58→  "labourer": {
    59→    title: "General Labourer Needed",
    60→    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    61→    certs: ["White Card"],
    62→    rateMin: 32,
    63→    rateMax: 42,
    64→    rateType: "hourly",
    65→  },
    66→  "concreter": {
    67→    title: "Concreter Required",
    68→    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    69→    certs: ["White Card"],
    70→    rateMin: 45,
    71→    rateMax: 65,
    72→    rateType: "hourly",
    73→  },
    74→  "carpenter": {
    75→    title: "Carpenter Needed",
    76→    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    77→    certs: ["White Card", "Trade Certificate"],
    78→    rateMin: 45,
    79→    rateMax: 65,
    80→    rateType: "hourly",
    81→  },
    82→  "scaffolder": {
    83→    title: "Scaffolder Required",
    84→    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    85→    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    86→    rateMin: 48,
    87→    rateMax: 68,
    88→    rateType: "hourly",
    89→  },
    90→  "other": {
    91→    title: "Construction Worker Needed",
    92→    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    93→    certs: ["White Card"],
    94→    rateMin: 35,
    95→    rateMax: 55,
    96→    rateType: "hourly",
    97→  },
    98→};
    99→
   100→export default function PostJobPage() {
   101→  const router = useRouter();
   102→  const supabase = createClient();
   103→
   104→  const [step, setStep] = useState(1);
   105→  const [loading, setLoading] = useState(false);
   106→  const [aiLoading, setAiLoading] = useState(false);
   107→  const [error, setError] = useState("");
   108→
   109→  // Step 1: Trade
   110→  const [selectedTrade, setSelectedTrade] = useState("");
   111→
   112→  // Step 2: AI-filled job details + location/date/workers (COMBINED)
   113→  const [title, setTitle] = useState("");
   114→  const [description, setDescription] = useState("");
   115→  const [certs, setCerts] = useState<string[]>([]);
   116→  const [rateMin, setRateMin] = useState("");
   117→  const [rateMax, setRateMax] = useState("");
   118→  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
   119→  const [suburb, setSuburb] = useState("");
   120→  const [siteAddress, setSiteAddress] = useState("");
   121→  const [startDate, setStartDate] = useState("");
   122→  const [duration, setDuration] = useState("");
   123→  const [workersNeeded, setWorkersNeeded] = useState("1");
   124→
   125→  async function handleTradeSelect(tradeId: string) {
   126→    setSelectedTrade(tradeId);
   127→    setAiLoading(true);
   128→    setStep(2);
   129→
   130→    // Simulate AI generating job details
   131→    await new Promise((r) => setTimeout(r, 1500));
   132→
   133→    const suggestion = AI_SUGGESTIONS[tradeId];
   134→    if (suggestion) {
   135→      setTitle(suggestion.title);
   136→      setDescription(suggestion.description);
   137→      setCerts(suggestion.certs);
   138→      setRateMin(String(suggestion.rateMin));
   139→      setRateMax(String(suggestion.rateMax));
   140→      setRateType(suggestion.rateType);
   141→    }
   142→    setAiLoading(false);
   143→  }
   144→
   145→  async function handlePost() {
   146→    setLoading(true);
   147→    setError("");
   148→
   149→    try {
   150→      const { data: { user } } = await supabase.auth.getUser();
   151→      if (!user) {
   152→        router.push("/auth/login");
   153→        return;
   154→      }
   155→
   156→      // Get contractor's company
   157→      const { data: company } = await supabase
   158→        .from("companies")
   159→        .select("id")
   160→        .eq("profile_id", user.id)
   161→        .single();
   162→
   163→      if (!company) {
   164→        setError("Please complete your company profile first.");
   165→        setLoading(false);
   166→        return;
   167→      }
   168→
   169→      const { error: insertError } = await supabase.from("jobs").insert({
   170→        company_id: company.id,
   171→        title,
   172→        description,
   173→        trade: selectedTrade,
   174→        suburb,
   175→        site_address: siteAddress,
   176→        rate_min: parseFloat(rateMin) || null,
   177→        rate_max: parseFloat(rateMax) || null,
   178→        rate_type: rateType,
   179→        start_date: startDate || null,
   180→        duration: duration || null,
   181→        workers_needed: parseInt(workersNeeded) || 1,
   182→        certifications_required: certs,
   183→        status: "active",
   184→        ai_generated: true,
   185→      });
   186→
   187→      if (insertError) {
   188→        setError(insertError.message);
   189→        setLoading(false);
   190→        return;
   191→      }
   192→
   193→      setStep(3); // Success (was step 5, now step 3)
   194→    } catch {
   195→      setError("Something went wrong. Please try again.");
   196→      setLoading(false);
   197→    }
   198→  }
   199→
   200→  return (
   201→

   202→      {/* Header */}
   203→
   204→

   205→          {step > 1 && step < 3 ? (
   206→            <button
   207→              onClick={() => setStep(step - 1)}
   208→              className="mr-3 flex h-12 w-12 items-center justify-center rounded-full active:bg-muted"
   209→            >
   210→              <ArrowLeft className="h-6 w-6" />
   211→            </button>
   212→          ) : (
   213→            <Link
   214→              href="/dashboard"
   215→              className="mr-3 flex h-12 w-12 items-center justify-center rounded-full active:bg-muted"
   216→            >
   217→              <ArrowLeft className="h-6 w-6" />
   218→            </Link>
   219→          )}
   220→
Post a Job

   221→          {step < 3 && (
   222→
   223→              Step {step}/2
   224→
   225→          )}
   226→

   227→        {/* Progress bar */}
   228→        {step < 3 && (
   229→

   230→            <div
   231→              className="h-1 bg-primary transition-all duration-300"
   232→              style={{ width: `${(step / 2) * 100}%` }}
   233→            />
   234→

   235→        )}
   236→
   237→
   238→
   239→        {/* STEP 1: Select Trade */}
   240→        {step === 1 && (
   241→

   242→
What trade do you need?

   243→

   244→              Tap to select. AI will write the job post for you.
   245→

   246→

   247→              {TRADE_CATEGORIES.map((trade) => {
   248→                const Icon = trade.icon;
   249→                return (
   250→                  <button
   251→                    key={trade.id}
   252→                    onClick={() => handleTradeSelect(trade.id)}
   253→                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
   254→                  >
   255→                    <Icon className="h-10 w-10" />
   256→                    {trade.label}
   257→                  </button>
   258→                );
   259→              })}
   260→

   261→

   262→        )}
   263→
   264→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   265→        {step === 2 && (
   266→

   267→            {aiLoading ? (
   268→

   269→

   270→                  <Sparkles className="h-12 w-12 text-primary animate-pulse" />
   271→

   272→
AI writing your job post...

   273→

   274→                  Based on market rates and common requirements
   275→

   276→

   277→            ) : (
   278→

   279→

   280→                  <Sparkles className="h-5 w-5 text-primary" />
   281→
Job Details

   282→

   283→

   284→                  AI pre-filled these. Edit anything you like.
   285→

   286→
   287→

   288→                  {/* Job Details Section */}
   289→

   290→                    Job Title
   291→                    <Input
   292→                      id="title"
   293→                      value={title}
   294→                      onChange={(e) => setTitle(e.target.value)}
   295→                      className="mt-2 h-14 text-base"
   296→                    />
   297→

   298→
   299→

   300→                    Description
   301→                    <textarea
   302→                      id="description"
   303→                      value={description}
   304→                      onChange={(e) => setDescription(e.target.value)}
   305→                      rows={5}
   306→                      className="mt-2 w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
   307→                    />
   308→

   309→
   310→

   311→                    Pay Rate (AUD)
   312→

   313→

   314→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   315→                        <Input
   316→                          type="number"
   317→                          value={rateMin}
   318→                          onChange={(e) => setRateMin(e.target.value)}
   319→                          placeholder="Min"
   320→                          className="h-14 pl-10 text-base"
   321→                        />
   322→

   323→                      to
   324→

   325→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   326→                        <Input
   327→                          type="number"
   328→                          value={rateMax}
   329→                          onChange={(e) => setRateMax(e.target.value)}
   330→                          placeholder="Max"
   331→                          className="h-14 pl-10 text-base"
   332→                        />
   333→

   334→

   335→

   336→                      <button
   337→                        onClick={() => setRateType("hourly")}
   338→                        className={`rounded-xl border-2 py-3.5 text-base font-semibold transition-colors ${
   339→                          rateType === "hourly"
   340→                            ? "border-primary bg-primary/10 text-primary"
   341→                            : "border-border text-muted-foreground"
   342→                        }`}
   343→                      >
   344→                        Per Hour
   345→                      </button>
   346→                      <button
   347→                        onClick={() => setRateType("daily")}
   348→                        className={`rounded-xl border-2 py-3.5 text-base font-semibold transition-colors ${
   349→                          rateType === "daily"
   350→                            ? "border-primary bg-primary/10 text-primary"
   351→                            : "border-border text-muted-foreground"
   352→                        }`}
   353→                      >
   354→                        Per Day
   355→                      </button>
   356→

   357→

   358→
   359→                  {/* Location Section */}
   360→

   361→

   362→                      <MapPin className="h-5 w-5" />
   363→                      Location
   364→

   365→
   366→

   367→

   368→                        Site Address
   369→                        <Input
   370→                          id="siteAddress"
   371→                          value={siteAddress}
   372→                          onChange={(e) => setSiteAddress(e.target.value)}
   373→                          placeholder="e.g. 123 George St, Sydney"
   374→                          className="mt-2 h-14 text-base"
   375→                        />
   376→

   377→
   378→

   379→                        Suburb
   380→                        <Input
   381→                          id="suburb"
   382→                          value={suburb}
   383→                          onChange={(e) => setSuburb(e.target.value)}
   384→                          placeholder="e.g. Parramatta"
   385→                          className="mt-2 h-14 text-base"
   386→                        />
   387→

   388→

   389→

   390→
   391→                  {/* Date & Workers Section */}
   392→

   393→

   394→                      <Calendar className="h-5 w-5" />
   395→                      Schedule
   396→

   397→
   398→

   399→

   400→                        Start Date
   401→                        <Input
   402→                          id="startDate"
   403→                          type="date"
   404→                          value={startDate}
   405→                          onChange={(e) => setStartDate(e.target.value)}
   406→                          className="mt-2 h-14 text-base"
   407→                        />
   408→

   409→
   410→

   411→                        Duration
   412→                        <Input
   413→                          id="duration"
   414→                          value={duration}
   415→                          onChange={(e) => setDuration(e.target.value)}
   416→                          placeholder="e.g. 2 weeks, ongoing"
   417→                          className="mt-2 h-14 text-base"
   418→                        />
   419→

   420→
   421→

   422→
   423→                          <Users className="h-5 w-5" />
   424→                          Workers Needed
   425→
   426→

   427→                          <button
   428→                            onClick={() =>
   429→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   430→                            }
   431→                            className="flex h-16 w-16 items-center justify-center rounded-xl border-2 text-2xl font-bold active:scale-95 active:bg-muted transition-all"
   432→                          >
   433→                            -
   434→                          </button>
   435→
   436→                            {workersNeeded}
   437→
   438→                          <button
   439→                            onClick={() =>
   440→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   441→                            }
   442→                            className="flex h-16 w-16 items-center justify-center rounded-xl border-2 text-2xl font-bold active:scale-95 active:bg-muted transition-all"
   443→                          >
   444→                            +
   445→                          </button>
   446→

   447→

   448→

   449→

   450→
   451→                  {/* Required Certifications */}
   452→                  {certs.length > 0 && (
   453→

   454→                      Required Certifications
   455→

   456→                        {certs.map((cert) => (
   457→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   458→                            {cert}
   459→                            <button
   460→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   461→                              className="ml-2 text-muted-foreground hover:text-foreground"
   462→                            >
   463→                              ×
   464→                            </button>
   465→                          </Badge>
   466→                        ))}
   467→

   468→

   469→                  )}
   470→

   471→

   472→            )}
   473→

   474→        )}
   475→
   476→        {/* STEP 3: Success */}
   477→        {step === 3 && (
   478→

   479→

   480→              <Check className="h-12 w-12 text-green-600" />
   481→

   482→
Job Posted!

   483→

   484→              We're matching you with the best workers now.
   485→              You'll get notified when we find matches.
   486→

   487→

   488→              <Button
   489→                onClick={() => router.push("/contractor/matches")}
   490→                className="w-full h-16 text-lg font-semibold"
   491→              >
   492→                View Matches
   493→              </Button>
   494→              <Button
   495→                onClick={() => router.push("/dashboard")}
   496→                variant="outline"
   497→                className="w-full h-16 text-lg"
   498→              >
   499→                Back to Dashboard
   500→              </Button>
   501→

   502→

   503→        )}
   504→
   505→
   506→      {/* Sticky Bottom CTA (Step 2 only) */}
   507→      {step === 2 && !aiLoading && (
   508→

   509→

   510→            {error && (
   511→
{error}

   512→            )}
   513→            <Button
   514→              onClick={handlePost}
   515→              className="w-full h-16 text-lg font-semibold"
   516→              disabled={loading || !title || !suburb}
   517→            >
   518→              {loading ? (
   519→                <>
   520→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   521→                  Posting...
   522→                </>
   523→              ) : (
   524→                <>
   525→                  Post Job Now
   526→                  <Check className="ml-2 h-6 w-6" />
   527→                </>
   528→              )}
   529→            </Button>
   530→

   531→              Free to post. You only pay $50 when you hire someone.
   532→

   533→

   534→

   535→      )}
   536→

   537→  );
   538→}
   539→

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