     1→import { formatABN } from "@/lib/abn-utils";
     2→import { NextRequest, NextResponse } from 'next/server';
     3→import { isValidABN, cleanABN } from '@/lib/abn-utils';
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit';
     5→import { sanitizeInput } from '@/lib/sanitize';
     6→import { z } from 'zod';
     7→
     8→/**
     9→ * ABN Lookup API Route — GET /api/abn-lookup?abn=XXXXXXXXXXX
    10→ *
    11→ * Validates the ABN locally (checksum), then calls the ABR JSON endpoint
    12→ * to fetch entity details.
    13→ *
    14→ * ABR JSON docs: https://abr.business.gov.au/json/
    15→ * Registration:  https://abr.business.gov.au/Tools/WebServices
    16→ *
    17→ * Required env var:
    18→ *   ABR_GUID — the authentication GUID issued after registration
    19→ *
    20→ * Optional env var:
    21→ *   ABR_DEV_MODE — set to 'true' to enable development mode with mock data
    22→ */
    23→
    24→// ── ABR configuration ──────────────────────────────────────────────
    25→const ABR_JSON_URL = 'https://abr.business.gov.au/json/AbnDetails.aspx';
    26→const ABR_GUID = process.env.ABR_GUID ?? '';
    27→const ABR_DEV_MODE = process.env.ABR_DEV_MODE === 'true';
    28→
    29→// ── Types ──────────────────────────────────────────────────────────
    30→
    31→/** Shape returned by the ABR JSON endpoint (JSONP-unwrapped). */
    32→interface ABRJsonResponse {
    33→  Abn: string;
    34→  AbnStatus: string;
    35→  AbnStatusEffectiveFrom: string;
    36→  Acn: string;
    37→  AddressDate: string | null;
    38→  AddressPostcode: string;
    39→  AddressState: string;
    40→  BusinessName: Array<string | { Value: string; EffectiveFrom: string }>;
    41→  EntityName: string;
    42→  EntityTypeCode: string;
    43→  EntityTypeName: string;
    44→  Gst: string | null;
    45→  Message: string;
    46→}
    47→
    48→/** Director verification result */
    49→interface DirectorVerification {
    50→  status: 'pending' | 'verified' | 'mismatch' | 'not_available';
    51→  message: string;
    52→  requiresManualReview?: boolean;
    53→}
    54→
    55→/** Company data from ABR */
    56→interface CompanyData {
    57→  name: string;
    58→  abn: string;
    59→  status: string;
    60→  statusDate: string;
    61→  state: string;
    62→  postcode: string;
    63→  type: string;
    64→  typeCode: string;
    65→  gstRegistered: boolean;
    66→  acn?: string;
    67→  directorVerification?: DirectorVerification;
    68→}
    69→
    70→/** Our normalised response. */
    71→interface ABNLookupResult {
    72→  success: boolean;
    73→  data?: CompanyData;
    74→  error?: string;
    75→  devMode?: boolean;
    76→}
    77→
    78→// ── Development Mode Mock Data ─────────────────────────────────────
    79→
    80→/**
    81→ * Mock company data for development when ABR_GUID is not configured.
    82→ * Uses obviously fake ABNs to prevent confusion with real data.
    83→ */
    84→const MOCK_COMPANIES: Record<string, CompanyData> = {
    85→  // Test ABN for development - passes checksum but is obviously fake
    86→  '51824753556': {
    87→    name: 'TEST COMPANY PTY LTD (Dev Mode)',
    88→    abn: '51 824 753 556',
    89→    status: 'Active',
    90→    statusDate: '2020-01-01',
    91→    state: 'NSW',
    92→    postcode: '2000',
    93→    type: 'Australian Private Company',
    94→    typeCode: 'PRV',
    95→    gstRegistered: true,
    96→    directorVerification: {
    97→      status: 'not_available',
    98→      message: 'Director verification unavailable in development mode',
    99→    },
   100→  },
   101→  // Another test ABN
   102→  '34241177887': {
   103→    name: 'EXAMPLE BUILDERS PTY LTD (Dev Mode)',
   104→    abn: '34 241 177 887',
   105→    status: 'Active',
   106→    statusDate: '2019-06-15',
   107→    state: 'VIC',
   108→    postcode: '3000',
   109→    type: 'Australian Private Company',
   110→    typeCode: 'PRV',
   111→    gstRegistered: true,
   112→    directorVerification: {
   113→      status: 'not_available',
   114→      message: 'Director verification unavailable in development mode',
   115→    },
   116→  },
   117→};
   118→
   119→/**
   120→ * Generate mock data for any valid ABN in development mode.
   121→ */
   122→function generateMockData(abn: string): CompanyData {
   123→  const formatted = formatABN(abn);
   124→  return {
   125→    name: `MOCK COMPANY ${abn.slice(-4)} PTY LTD (Dev Mode)`,
   126→    abn: formatted,
   127→    status: 'Active',
   128→    statusDate: new Date().toISOString().split('T')[0],
   129→    state: 'NSW',
   130→    postcode: '2000',
   131→    type: 'Australian Private Company',
   132→    typeCode: 'PRV',
   133→    gstRegistered: Math.random() > 0.3,
   134→    directorVerification: {
   135→      status: 'not_available',
   136→      message: 'Director verification unavailable in development mode. Register at https://abr.business.gov.au/Tools/WebServices',
   137→    },
   138→  };
   139→}
   140→
   141→// ── Helpers ────────────────────────────────────────────────────────
   142→
   143→function buildABRUrl(abn: string): string {
   144→  const params = new URLSearchParams({
   145→    abn,
   146→    guid: ABR_GUID,
   147→    callback: 'cbr',
   148→  });
   149→  return `${ABR_JSON_URL}?${params.toString()}`;
   150→}
   151→
   152→function stripJSONP(jsonp: string): ABRJsonResponse {
   153→  const match = jsonp.match(/^cbr\((.*)\)$/);
   154→  if (!match) throw new Error('Unexpected ABR response format');
   155→  return JSON.parse(match[1]);
   156→}
   157→
   158→function normaliseABRResponse(abr: ABRJsonResponse): CompanyData {
   159→  const name = abr.EntityName ||
   160→    (Array.isArray(abr.BusinessName) && abr.BusinessName[0]) ||
   161→    'Unknown';
   162→
   163→  return {
   164→    name: typeof name === 'string' ? name : (name as any).Value || 'Unknown',
   165→    abn: formatABN(abr.Abn),
   166→    status: abr.AbnStatus,
   167→    statusDate: abr.AbnStatusEffectiveFrom,
   168→    state: abr.AddressState,
   169→    postcode: abr.AddressPostcode,
   170→    type: abr.EntityTypeName,
   171→    typeCode: abr.EntityTypeCode,
   172→    gstRegistered: abr.Gst !== null,
   173→    acn: abr.Acn || undefined,
   174→    // Director verification will be done separately via ASIC
   175→    directorVerification: {
   176→      status: 'pending',
   177→      message: 'Director verification pending. ASIC lookup required.',
   178→      requiresManualReview: true,
   179→    },
   180→  };
   181→}
   182→
   183→/**
   184→ * ASIC Director Verification (Stub)
   185→ *
   186→ * ASIC Connect API requires paid subscription.
   187→ * For MVP, we flag all signups for manual review.
   188→ *
   189→ * Future implementation:
   190→ * - Call ASIC Connect API to get director list
   191→ * - Cross-reference user email/name with director records
   192→ * - Auto-verify if match found
   193→ * - Flag for manual review if no match
   194→ *
   195→ * @see https://connectonline.asic.gov.au/
   196→ */
   197→async function verifyDirector(
   198→  abn: string,
   199→  acn?: string,
   200→  userEmail?: string
   201→): Promise<DirectorVerification> {
   202→  // TODO: Implement ASIC Connect API integration
   203→  // For now, flag all for manual review
   204→
   205→  console.log(`[ASIC] Director verification requested for ABN ${abn}, ACN ${acn || 'N/A'}`);
   206→
   207→  return {
   208→    status: 'pending',
   209→    message: 'Director verification requires manual review. ASIC integration pending.',
   210→    requiresManualReview: true,
   211→  };
   212→}
   213→
   214→// ── Route Handler ──────────────────────────────────────────────────
   215→
   216→// Rate limit: 10 requests per 15 minutes per IP (external API call)
   217→const ABN_LOOKUP_RATE_LIMIT = 10;
   218→const ABN_LOOKUP_WINDOW_MS = 15 * 60 * 1000;
   219→
   220→// Email validation schema
   221→const emailSchema = z.string().email().max(254);
   222→
   223→export async function GET(request: NextRequest) {
   224→  try {
   225→    // --- 0. Rate limiting ---------------------------------------------------
   226→    const clientIP = getClientIP(request);
   227→    const rateLimit = checkRateLimit(`abn-lookup:${clientIP}`, ABN_LOOKUP_RATE_LIMIT, ABN_LOOKUP_WINDOW_MS);
   228→
   229→    if (!rateLimit.allowed) {
   230→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
   231→      return createRateLimitResponse(clientIP, retryAfter);
   232→    }
   233→
   234→    const searchParams = request.nextUrl.searchParams;
   235→    const rawABN = searchParams.get('abn');
   236→    const rawEmail = searchParams.get('email');
   237→
   238→    // --- 1. Validate input --------------------------------------------------
   239→    if (!rawABN) {
   240→      return NextResponse.json({
   241→        success: false,
   242→        error: 'Missing query parameter: abn'
   243→      }, { status: 400 });
   244→    }
   245→
   246→    const abn = cleanABN(rawABN);
   247→    if (!isValidABN(abn)) {
   248→      return NextResponse.json({
   249→        success: false,
   250→        error: 'Invalid ABN format. Please check the number and try again.'
   251→      }, { status: 400 });
   252→    }
   253→
   254→    // Validate email format if provided
   255→    let userEmail: string | null = null;
   256→    if (rawEmail) {
   257→      const sanitizedEmail = sanitizeInput(rawEmail, { maxLength: 254 });
   258→      const emailValidation = emailSchema.safeParse(sanitizedEmail);
   259→
   260→      if (!emailValidation.success) {
   261→        return NextResponse.json({
   262→          success: false,
   263→          error: 'Invalid email format. Please provide a valid email address.'
   264→        }, { status: 400 });
   265→      }
   266→
   267→      userEmail = emailValidation.data;
   268→    }
   269→
   270→    // --- 2. Check if ABR_GUID is configured --------------------------------
   271→    const isGuidConfigured = ABR_GUID &&
   272→      ABR_GUID !== 'REGISTER-TOMORROW-AT-ABR-BUSINESS-GOV-AU' &&
   273→      ABR_GUID !== 'temporary-abr-guid-for-build';
   274→
   275→    if (!isGuidConfigured) {
   276→      // Development mode: return mock data
   277→      if (ABR_DEV_MODE) {
   278→        console.log(`[ABN Lookup] Dev mode - returning mock data for ${abn}`);
   279→        const mockData = MOCK_COMPANIES[abn] || generateMockData(abn);
   280→        return NextResponse.json({
   281→          success: true,
   282→          data: mockData,
   283→          devMode: true,
   284→        });
   285→      }
   286→
   287→      // Production without GUID: return error with validation info
   288→      return NextResponse.json({
   289→        success: false,
   290→        error: 'ABR service not configured',
   291→        devMode: false,
   292→      }, { status: 503 });
   293→    }
   294→
   295→    // --- 3. Call ABR web service -------------------------------------------
   296→    const url = buildABRUrl(abn);
   297→    const abrResponse = await fetch(url, {
   298→      method: 'GET',
   299→      headers: { 'User-Agent': 'RateRight/1.0' },
   300→      signal: AbortSignal.timeout(10000),
   301→    });
   302→
   303→    if (!abrResponse.ok) {
   304→      const text = await abrResponse.text();
   305→      console.error(`[ABN Lookup] ABR error ${abrResponse.status}:`, text);
   306→      return NextResponse.json({
   307→        success: false,
   308→        error: `ABR service error: ${abrResponse.status}`,
   309→      }, { status: 502 });
   310→    }
   311→
   312→    const jsonpText = await abrResponse.text();
   313→    const abrData = stripJSONP(jsonpText);
   314→
   315→    // --- 4. Handle ABR business logic ---------------------------------------
   316→    if (abrData.Message) {
   317→      return NextResponse.json({
   318→        success: false,
   319→        error: abrData.Message
   320→      }, { status: 404 });
   321→    }
   322→
   323→    const result = normaliseABRResponse(abrData);
   324→
   325→    // --- 5. Director verification (async, non-blocking) --------------------
   326→    if (result.acn || userEmail) {
   327→      result.directorVerification = await verifyDirector(
   328→        abn,
   329→        result.acn,
   330→        userEmail || undefined
   331→      );
   332→    }
   333→
   334→    return NextResponse.json({
   335→      success: true,
   336→      data: result,
   337→    });
   338→
   339→  } catch (err: any) {
   340→    console.error('[ABN Lookup] Uncaught error:', err);
   341→    return NextResponse.json({
   342→      success: false,
   343→      error: 'Internal server error',
   344→    }, { status: 500 });
   345→  }
   346→}
   347→

<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→import { createClient } from "@/lib/supabase/server";
     2→import { NextRequest, NextResponse } from "next/server";
     3→import { withCSRFProtection } from "@/lib/csrf";
     4→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     5→
     6→// 20 conversation starts per 15 min window per IP (prevents spam conversation creation)
     7→const START_RATE_LIMIT = 20;
     8→const START_WINDOW_MS = 15 * 60 * 1000;
     9→
    10→const handler = async (request: NextRequest) => {
    11→  try {
    12→    const clientIP = getClientIP(request);
    13→    const rateLimit = checkRateLimit(`messages-start:${clientIP}`, START_RATE_LIMIT, START_WINDOW_MS);
    14→
    15→    if (!rateLimit.allowed) {
    16→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    17→      return createRateLimitResponse(clientIP, retryAfter);
    18→    }
    19→
    20→    const supabase = await createClient();
    21→
    22→    // Check if user is authenticated
    23→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    24→
    25→    if (authError || !user) {
    26→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    27→    }
    28→
    29→    const { recipientId, initialMessage } = await request.json();
    30→
    31→    if (!recipientId) {
    32→      return NextResponse.json(
    33→        { error: "Recipient ID is required" },
    34→        { status: 400 }
    35→      );
    36→    }
    37→
    38→    // Verify recipient exists
    39→    const { data: recipient, error: recipientError } = await supabase
    40→      .from('profiles')
    41→      .select('id, name')
    42→      .eq('id', recipientId)
    43→      .single();
    44→
    45→    if (recipientError || !recipient) {
    46→      return NextResponse.json(
    47→        { error: "Recipient not found" },
    48→        { status: 404 }
    49→      );
    50→    }
    51→
    52→    // Get or create conversation using the database function
    53→    const { data: conversationId, error: conversationError } = await supabase
    54→      .rpc('get_or_create_conversation', {
    55→        user1_id: user.id,
    56→        user2_id: recipientId
    57→      });
    58→
    59→    if (conversationError) {
    60→      console.error("Error creating conversation:", conversationError);
    61→      return NextResponse.json(
    62→        { error: "Failed to create conversation" },
    63→        { status: 500 }
    64→      );
    65→    }
    66→
    67→    // If there's an initial message, send it
    68→    if (initialMessage?.trim()) {
    69→      const { error: messageError } = await supabase
    70→        .from('messages')
    71→        .insert({
    72→          conversation_id: conversationId,
    73→          sender_id: user.id,
    74→          content: initialMessage.trim()
    75→        });
    76→
    77→      if (messageError) {
    78→        console.error("Error sending initial message:", messageError);
    79→        // Don't fail the whole request if message fails
    80→      }
    81→    }
    82→
    83→    return NextResponse.json({
    84→      conversationId,
    85→      message: "Conversation created successfully"
    86→    });
    87→  } catch (error) {
    88→    console.error("Error in start conversation API:", error);
    89→    return NextResponse.json(
    90→      { error: "Internal server error" },
    91→      { status: 500 }
    92→    );
    93→  }
    94→}
    95→
    96→// Apply CSRF protection to the handler
    97→export const POST = withCSRFProtection(handler);
    98→

<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→import { createClient } from "@/lib/supabase/server";
     2→import { NextRequest, NextResponse } from "next/server";
     3→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     4→
     5→// 100 conversation list fetches per 15 min window per IP
     6→const MESSAGE_RATE_LIMIT = 100;
     7→const MESSAGE_WINDOW_MS = 15 * 60 * 1000;
     8→
     9→export async function GET(request: NextRequest) {
    10→  try {
    11→    const clientIP = getClientIP(request);
    12→    const rateLimit = checkRateLimit(`messages-list:${clientIP}`, MESSAGE_RATE_LIMIT, MESSAGE_WINDOW_MS);
    13→
    14→    if (!rateLimit.allowed) {
    15→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    16→      return createRateLimitResponse(clientIP, retryAfter);
    17→    }
    18→
    19→    const supabase = await createClient();
    20→
    21→    // Check if user is authenticated
    22→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    23→
    24→    if (authError || !user) {
    25→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    26→    }
    27→
    28→    // Update last_seen_at
    29→    await supabase
    30→      .from('profiles')
    31→      .update({ last_seen_at: new Date().toISOString() })
    32→      .eq('id', user.id);
    33→
    34→    // Get conversation previews using the database function
    35→    const { data: conversations, error } = await supabase
    36→      .rpc('get_conversation_previews', { user_id: user.id });
    37→
    38→    if (error) {
    39→      console.error("Error fetching conversations:", error);
    40→      return NextResponse.json(
    41→        { error: "Failed to fetch conversations" },
    42→        { status: 500 }
    43→      );
    44→    }
    45→
    46→    return NextResponse.json({ conversations: conversations || [] });
    47→  } catch (error) {
    48→    console.error("Error in messages API:", error);
    49→    return NextResponse.json(
    50→      { error: "Internal server error" },
    51→      { status: 500 }
    52→    );
    53→  }
    54→}

<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→import { NextRequest, NextResponse } from "next/server";
     2→import { createClient, getSupabaseAdmin } from "@/lib/supabase/server";
     3→import { checkRateLimit, getClientIP, createRateLimitResponse } from "@/lib/rate-limit";
     4→
     5→// Rate limit: 10 requests per 15 minutes
     6→const RATE_LIMIT = 10;
     7→const WINDOW_MS = 15 * 60 * 1000;
     8→
     9→/**
    10→ * POST /api/match/find-matches
    11→ *
    12→ * Finds and creates suggested matches for a job using DB-side scoring.
    13→ * Body: { job_id: string, limit?: number, min_score?: number }
    14→ *
    15→ * Scoring weights (calculated in database):
    16→ * - Trade match: 40%
    17→ * - Availability: 20%
    18→ * - Experience: 15%
    19→ * - Certifications: 15%
    20→ * - Location proximity: 10%
    21→ * - Ratings: 10%
    22→ *
    23→ * NOTE: This uses the find_matching_workers() RPC function which scales
    24→ * efficiently to 10k+ workers by performing all scoring calculations
    25→ * database-side with proper indexes.
    26→ */
    27→export async function POST(request: NextRequest) {
    28→  try {
    29→    // Rate limiting
    30→    const clientIP = getClientIP(request);
    31→    const rateLimit = checkRateLimit(`match:${clientIP}`, RATE_LIMIT, WINDOW_MS);
    32→    if (!rateLimit.allowed) {
    33→      const retryAfter = Math.ceil((rateLimit.resetTime - Date.now()) / 1000);
    34→      return createRateLimitResponse(clientIP, retryAfter);
    35→    }
    36→
    37→    // Auth check
    38→    const supabase = await createClient();
    39→    const { data: { user }, error: authError } = await supabase.auth.getUser();
    40→    if (authError || !user) {
    41→      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    42→    }
    43→
    44→    const { job_id, limit = 20, min_score = 20 } = await request.json();
    45→    if (!job_id) {
    46→      return NextResponse.json({ error: "job_id is required" }, { status: 400 });
    47→    }
    48→
    49→    // Validate limit (prevent abuse)
    50→    const validLimit = Math.min(Math.max(parseInt(limit as string) || 20, 1), 50);
    51→    const validMinScore = Math.min(Math.max(parseInt(min_score as string) || 20, 0), 100);
    52→
    53→    // Use admin client to bypass RLS for matching operations
    54→    const admin = getSupabaseAdmin();
    55→
    56→    // Verify job exists and user owns it
    57→    const { data: job, error: jobError } = await admin
    58→      .from("jobs")
    59→      .select("id, status, companies!inner(profile_id)")
    60→      .eq("id", job_id)
    61→      .single();
    62→
    63→    if (jobError || !job) {
    64→      return NextResponse.json({ error: "Job not found" }, { status: 404 });
    65→    }
    66→
    67→    // Verify the requesting user owns this job's company
    68→    const companies = job.companies as unknown as { profile_id: string } | null;
    69→    if (!companies || companies.profile_id !== user.id) {
    70→      return NextResponse.json({ error: "Not authorized for this job" }, { status: 403 });
    71→    }
    72→
    73→    if (job.status !== "active") {
    74→      return NextResponse.json({ error: "Job is not active" }, { status: 400 });
    75→    }
    76→
    77→    // Call the DB-side scoring RPC function
    78→    // This is the key optimization - all scoring happens in the database
    79→    const { data: scoredWorkers, error: rpcError } = await admin.rpc(
    80→      "find_matching_workers",
    81→      {
    82→        p_job_id: job_id,
    83→        p_limit: validLimit,
    84→        p_min_score: validMinScore,
    85→      }
    86→    );
    87→
    88→    if (rpcError) {
    89→      console.error("RPC error finding matches:", rpcError);
    90→      return NextResponse.json(
    91→        { error: "Failed to find matches", details: rpcError.message },
    92→        { status: 500 }
    93→      );
    94→    }
    95→
    96→    if (!scoredWorkers || scoredWorkers.length === 0) {
    97→      return NextResponse.json({
    98→        matches: [],
    99→        message: "No matching workers found",
   100→        matches_created: 0,
   101→      });
   102→    }
   103→
   104→    // Prepare match rows for insertion
   105→    const matchRows = scoredWorkers.map((match: {
   106→      worker_id: string;
   107→      score: number;
   108→      summary: string;
   109→    }) => ({
   110→      job_id,
   111→      worker_id: match.worker_id,
   112→      status: "suggested" as const,
   113→      match_score: match.score,
   114→      ai_summary: match.summary,
   115→      contractor_action: "pending" as const,
   116→      worker_action: "pending" as const,
   117→    }));
   118→
   119→    // Insert matches into the database
   120→    const { error: insertError } = await admin
   121→      .from("matches")
   122→      .upsert(matchRows, { onConflict: "job_id,worker_id", ignoreDuplicates: true });
   123→
   124→    if (insertError) {
   125→      console.error("Error inserting matches:", insertError);
   126→      return NextResponse.json({ error: "Failed to save matches" }, { status: 500 });
   127→    }
   128→
   129→    return NextResponse.json({
   130→      matches: scoredWorkers.map((m: {
   131→        worker_id: string;
   132→        score: number;
   133→        summary: string;
   134→        reasons: string[];
   135→        profile: Record<string, unknown>;
   136→      }) => ({
   137→        worker_id: m.worker_id,
   138→        score: m.score,
   139→        summary: m.summary,
   140→        reasons: m.reasons,
   141→        profile: m.profile,
   142→      })),
   143→      matches_created: scoredWorkers.length,
   144→    });
   145→  } catch (error) {
   146→    console.error("Match finding error:", error);
   147→    return NextResponse.json(
   148→      { error: "Failed to find matches" },
   149→      { status: 500 }
   150→    );
   151→  }
   152→}
   153→

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


Bash

find /home/ccuser/the-50-dollar-app/src -name "*.tsx" -o -name "*.ts" | grep -E "(component|page)" | head -30

Find component and page files


Let me continue reading key pages and components to complete the audit. I'll focus on critical user-facing pages and components:


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

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

   315→    );
   316→  }
   317→
   318→  return (
   319→

   320→      {/* Header */}
   321→
   322→

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

   327→
   328→
   329→
   330→        {/* Progress bar */}
   331→

   332→          {Array.from({ length: totalSteps }, (_, i) => (
   333→            <div
   334→              key={i}
   335→              className={`h-1.5 flex-1 rounded-full transition-colors ${
   336→                i <= currentStep - (isAuthenticated ? 1 : 0)
   337→                  ? "bg-primary"
   338→                  : "bg-muted"
   339→              }`}
   340→            />
   341→          ))}
   342→

   343→
   344→        {/* Step: Auth */}
   345→        {step === "auth" && (
   346→

   347→

   348→

   349→                <Building2 className="h-5 w-5" />
   350→

   351→

   352→
Create your account

   353→

   354→                  Quick signup to get started
   355→

   356→

   357→

   358→
   359→            <form onSubmit={handleAuth} className="mt-6 space-y-4">
   360→

   361→                Email
   362→                <Input
   363→                  id="email"
   364→                  type="email"
   365→                  placeholder="you@company.com.au"
   366→                  value={email}
   367→                  onChange={(e) => setEmail(e.target.value)}
   368→                  required
   369→                  autoComplete="email"
   370→                  className="h-12 text-base"
   371→                />
   372→

   373→

   374→                Password
   375→                <Input
   376→                  id="password"
   377→                  type="password"
   378→                  placeholder="Create a password"
   379→                  value={password}
   380→                  onChange={(e) => setPassword(e.target.value)}
   381→                  required
   382→                  minLength={6}
   383→                  autoComplete="new-password"
   384→                  className="h-12 text-base"
   385→                />
   386→

   387→
   388→              <Button
   389→                type="submit"
   390→                className="h-12 w-full gap-2 text-base font-semibold"
   391→                disabled={authLoading}
   392→              >
   393→                {authLoading ? (
   394→                  <>
   395→                    <Loader2 className="h-5 w-5 animate-spin" />
   396→                    Creating account...
   397→                  </>
   398→                ) : (
   399→                  <>
   400→                    Continue
   401→                    <ArrowRight className="h-5 w-5" />
   402→                  </>
   403→                )}
   404→              </Button>
   405→            </form>
   406→
   407→

   408→              Already have an account?{" "}
   409→              <Link
   410→                href="/auth/login"
   411→                className="text-primary underline hover:no-underline"
   412→              >
   413→                Sign in
   414→              </Link>
   415→

   416→

   417→        )}
   418→
   419→        {/* Step: Enter company name/ABN */}
   420→        {step === "company" && (
   421→

   422→

   423→

   424→                <Search className="h-5 w-5" />
   425→

   426→

   427→
Your company

   428→

   429→                  We'll verify it via ABR
   430→

   431→

   432→

   433→
   434→            <form onSubmit={handleCompanySearch} className="mt-6 space-y-4">
   435→

   436→                ABN
   437→                <Input
   438→                  id="company"
   439→                  type="text"
   440→                  placeholder="Enter your 11-digit ABN"
   441→                  value={companyInput}
   442→                  onChange={(e) => setCompanyInput(e.target.value)}
   443→                  required
   444→                  autoFocus
   445→                  className="h-12 text-base"
   446→                />
   447→

   448→                  Enter your 11-digit ABN for verification via the Australian Business Register.
   449→

   450→

   451→
   452→              <Button
   453→                type="submit"
   454→                className="h-12 w-full gap-2 text-base font-semibold"
   455→              >
   456→                <Search className="h-5 w-5" />
   457→                Verify my ABN
   458→              </Button>
   459→            </form>
   460→
   461→            {!isAuthenticated && (
   462→              <button
   463→                onClick={() => setStep("auth")}
   464→                className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   465→              >
   466→                <ArrowLeft className="h-4 w-4" />
   467→                Back
   468→              </button>
   469→            )}
   470→

   471→        )}
   472→
   473→        {/* Step: ABR Verification */}
   474→        {step === "researching" && (
   475→

   476→

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

   479→
Verifying your business

   480→

   481→              Checking ABR for your business details...
   482→

   483→
   484→

   485→              <ResearchStep label="Checking ABR..." />
   486→              <ResearchStep label="Verifying company..." />
   487→              <ResearchStep label="Done" />
   488→

   489→

   490→        )}
   491→
   492→        {/* Step: Review profile */}
   493→        {step === "review" && companyData && (
   494→

   495→

   496→

   497→                <CheckCircle2 className="h-5 w-5" />
   498→

   499→

   500→
Confirm your profile

   501→

   502→                  Tap any field to edit
   503→

   504→

   505→

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

   568→

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

   571→

   572→                    Verified via Australian Business Register
   573→

   574→

   575→                    Your ABN has been verified. Tap any field to correct it.
   576→

   577→

   578→

   579→

   580→
   581→            <Button
   582→              onClick={handleConfirm}
   583→              className="mt-6 h-14 w-full gap-2 text-base font-semibold"
   584→              disabled={saving}
   585→            >
   586→              {saving ? (
   587→                <>
   588→                  <Loader2 className="h-5 w-5 animate-spin" />
   589→                  Saving...
   590→                </>
   591→              ) : (
   592→                <>
   593→                  <CheckCircle2 className="h-5 w-5" />
   594→                  Looks good — create my profile
   595→                </>
   596→              )}
   597→            </Button>
   598→
   599→            <button
   600→              onClick={() => setStep("company")}
   601→              className="mt-4 flex w-full items-center justify-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   602→            >
   603→              <ArrowLeft className="h-4 w-4" />
   604→              Search again
   605→            </button>
   606→

   607→        )}
   608→
   609→

   610→  );
   611→}
   612→
   613→function ResearchStep({ label }: { label: string }) {
   614→  return (
   615→

   616→      <Loader2 className="h-4 w-4 animate-spin text-primary" />
   617→      {label}
   618→

   619→  );
   620→}
   621→
   622→function EditableField({
   623→  icon,
   624→  label,
   625→  value,
   626→  fieldKey,
   627→  editField,
   628→  setEditField,
   629→  onSave,
   630→  logoUrl,
   631→}: {
   632→  icon: React.ReactNode;
   633→  label: string;
   634→  value: string;
   635→  fieldKey: string;
   636→  editField: string | null;
   637→  setEditField: (f: string | null) => void;
   638→  onSave: (value: string) => void;
   639→  logoUrl?: string | null;
   640→}) {
   641→  const [editValue, setEditValue] = useState(value);
   642→  const isEditing = editField === fieldKey;
   643→
   644→  if (isEditing) {
   645→    return (
   646→

   647→        {label}
   648→

   649→          {fieldKey === 'name' && logoUrl ? (
   650→

   651→              <img
   652→                src={logoUrl}
   653→                alt={`${value} logo`}
   654→                className="h-8 w-8 rounded-full object-cover"
   655→                onError={(e) => {
   656→                  e.currentTarget.style.display = 'none';
   657→                }}
   658→              />
   659→              <Input
   660→                value={editValue}
   661→                onChange={(e) => setEditValue(e.target.value)}
   662→                className="h-10 text-base"
   663→                autoFocus
   664→              />
   665→

   666→          ) : (
   667→            <Input
   668→              value={editValue}
   669→              onChange={(e) => setEditValue(e.target.value)}
   670→              className="h-10 text-base"
   671→              autoFocus
   672→            />
   673→          )}
   674→          <Button
   675→            size="sm"
   676→            onClick={() => onSave(editValue)}
   677→            className="h-10 shrink-0"
   678→          >
   679→            Save
   680→          </Button>
   681→

   682→

   683→    );
   684→  }
   685→
   686→  return (
   687→    <button
   688→      onClick={() => {
   689→        setEditValue(value);
   690→        setEditField(fieldKey);
   691→      }}
   692→      className="flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-muted"
   693→    >
   694→      {fieldKey === 'name' && logoUrl ? (
   695→

   696→          <img
   697→            src={logoUrl}
   698→            alt={`${value} logo`}
   699→            className="h-6 w-6 rounded-full object-cover"
   700→            onError={(e) => {
   701→              // If image fails to load, fall back to icon
   702→              e.currentTarget.style.display = 'none';
   703→            }}
   704→          />
   705→

   706→      ) : (
   707→
{icon}

   708→      )}
   709→

   710→
{label}

   711→
{value}

   712→

   713→      <Edit3 className="mt-1 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
   714→    </button>
   715→  );
   716→}
   717→

<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 toast from "react-hot-toast";
     7→import { createClient } from "@/lib/supabase/client";
     8→import { Button } from "@/components/ui/button";
     9→import { Card, CardContent } from "@/components/ui/card";
    10→import { Input } from "@/components/ui/input";
    11→import { Label } from "@/components/ui/label";
    12→import { Badge } from "@/components/ui/badge";
    13→import {
    14→  ArrowLeft,
    15→  ArrowRight,
    16→  Loader2,
    17→  Sparkles,
    18→  Check,
    19→  MapPin,
    20→  Calendar,
    21→  Users,
    22→  DollarSign,
    23→  HardHat,
    24→  Wrench,
    25→  Hammer,
    26→  Blocks,
    27→  Fence,
    28→  CircleDot,
    29→  Shovel,
    30→} from "lucide-react";
    31→import { jsonToPostgresPoint } from "@/lib/location-utils";
    32→import { csrfFetch } from "@/lib/csrf-client";
    33→
    34→const TRADE_CATEGORIES = [
    35→  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
    36→  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
    37→  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
    38→  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
    39→  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
    40→  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
    41→  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
    42→];
    43→
    44→const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
    45→  "steel-fixer": {
    46→    title: "Steel Fixer Needed",
    47→    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.",
    48→    certs: ["White Card", "Working at Heights"],
    49→    rateMin: 45,
    50→    rateMax: 65,
    51→    rateType: "hourly",
    52→  },
    53→  "formworker": {
    54→    title: "Formworker Required",
    55→    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.",
    56→    certs: ["White Card"],
    57→    rateMin: 42,
    58→    rateMax: 58,
    59→    rateType: "hourly",
    60→  },
    61→  "labourer": {
    62→    title: "General Labourer Needed",
    63→    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.",
    64→    certs: ["White Card"],
    65→    rateMin: 32,
    66→    rateMax: 42,
    67→    rateType: "hourly",
    68→  },
    69→  "concreter": {
    70→    title: "Concreter Required",
    71→    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.",
    72→    certs: ["White Card"],
    73→    rateMin: 45,
    74→    rateMax: 65,
    75→    rateType: "hourly",
    76→  },
    77→  "carpenter": {
    78→    title: "Carpenter Needed",
    79→    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    80→    certs: ["White Card", "Trade Certificate"],
    81→    rateMin: 45,
    82→    rateMax: 65,
    83→    rateType: "hourly",
    84→  },
    85→  "scaffolder": {
    86→    title: "Scaffolder Required",
    87→    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    88→    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    89→    rateMin: 48,
    90→    rateMax: 68,
    91→    rateType: "hourly",
    92→  },
    93→  "other": {
    94→    title: "Construction Worker Needed",
    95→    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    96→    certs: ["White Card"],
    97→    rateMin: 35,
    98→    rateMax: 55,
    99→    rateType: "hourly",
   100→  },
   101→};
   102→
   103→export default function PostJobPage() {
   104→  const router = useRouter();
   105→  const supabase = createClient();
   106→
   107→  const [step, setStep] = useState(1);
   108→  const [loading, setLoading] = useState(false);
   109→  const [aiLoading, setAiLoading] = useState(false);
   110→
   111→  // Step 1: Trade
   112→  const [selectedTrade, setSelectedTrade] = useState("");
   113→
   114→  // Step 2: AI-filled job details + location/date/workers (COMBINED)
   115→  const [title, setTitle] = useState("");
   116→  const [description, setDescription] = useState("");
   117→  const [certs, setCerts] = useState<string[]>([]);
   118→  const [rateMin, setRateMin] = useState("");
   119→  const [rateMax, setRateMax] = useState("");
   120→  const [rateType, setRateType] = useState<"hourly" | "daily">("hourly");
   121→  const [suburb, setSuburb] = useState("");
   122→  const [siteAddress, setSiteAddress] = useState("");
   123→  const [startDate, setStartDate] = useState("");
   124→  const [duration, setDuration] = useState("");
   125→  const [workersNeeded, setWorkersNeeded] = useState("1");
   126→
   127→  async function handleTradeSelect(tradeId: string) {
   128→    setSelectedTrade(tradeId);
   129→    setAiLoading(true);
   130→    setStep(2);
   131→
   132→    // Try real AI generation, fall back to hardcoded suggestions
   133→    try {
   134→      const res = await csrfFetch("/api/ai/generate-job", {
   135→        method: "POST",
   136→        headers: { "Content-Type": "application/json" },
   137→        body: JSON.stringify({ trade: tradeId }),
   138→      });
   139→
   140→      if (res.ok) {
   141→        const data = await res.json();
   142→        setTitle(data.title);
   143→        setDescription(data.description);
   144→        setCerts(data.certifications);
   145→        setRateMin(String(data.rateMin));
   146→        setRateMax(String(data.rateMax));
   147→        setRateType(data.rateType);
   148→        setAiLoading(false);
   149→        return;
   150→      }
   151→    } catch {
   152→      // Fall through to hardcoded fallback
   153→    }
   154→
   155→    // Fallback: use hardcoded suggestions
   156→    const suggestion = AI_SUGGESTIONS[tradeId];
   157→    if (suggestion) {
   158→      setTitle(suggestion.title);
   159→      setDescription(suggestion.description);
   160→      setCerts(suggestion.certs);
   161→      setRateMin(String(suggestion.rateMin));
   162→      setRateMax(String(suggestion.rateMax));
   163→      setRateType(suggestion.rateType);
   164→    }
   165→    setAiLoading(false);
   166→  }
   167→
   168→  async function handlePost() {
   169→    setLoading(true);
   170→
   171→    try {
   172→      const { data: { user } } = await supabase.auth.getUser();
   173→      if (!user) {
   174→        toast.error("Session expired. Please log in again.");
   175→        router.push("/auth/login");
   176→        return;
   177→      }
   178→
   179→      // Get contractor's company
   180→      const { data: company } = await supabase
   181→        .from("companies")
   182→        .select("id")
   183→        .eq("profile_id", user.id)
   184→        .single();
   185→
   186→      if (!company) {
   187→        toast.error("Please complete your company profile first.");
   188→        setLoading(false);
   189→        return;
   190→      }
   191→
   192→      const { error: insertError } = await supabase.from("jobs").insert({
   193→        company_id: company.id,
   194→        title,
   195→        description,
   196→        trade: selectedTrade,
   197→        suburb,
   198→        site_address: siteAddress,
   199→        rate_min: parseFloat(rateMin) || null,
   200→        rate_max: parseFloat(rateMax) || null,
   201→        rate_type: rateType,
   202→        start_date: startDate || null,
   203→        duration: duration || null,
   204→        workers_needed: parseInt(workersNeeded) || 1,
   205→        certifications_required: certs,
   206→        status: "active",
   207→        ai_generated: true,
   208→      });
   209→
   210→      if (insertError) {
   211→        toast.error(`Failed to post job: ${insertError.message}`);
   212→        setLoading(false);
   213→        return;
   214→      }
   215→
   216→      setStep(3); // Success (was step 5, now step 3)
   217→    } catch (error) {
   218→      console.error("Job post error:", error);
   219→      toast.error("Something went wrong. Please try again.");
   220→      setLoading(false);
   221→    }
   222→  }
   223→
   224→  return (
   225→

   226→
   227→        {/* STEP 1: Select Trade */}
   228→        {step === 1 && (
   229→

   230→
What trade do you need?

   231→

   232→              Tap to select. AI will write the job post for you.
   233→

   234→

   235→              {TRADE_CATEGORIES.map((trade) => {
   236→                const Icon = trade.icon;
   237→                return (
   238→                  <button
   239→                    key={trade.id}
   240→                    onClick={() => handleTradeSelect(trade.id)}
   241→                    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}`}
   242→                  >
   243→                    <Icon className="h-10 w-10" />
   244→                    {trade.label}
   245→                  </button>
   246→                );
   247→              })}
   248→

   249→

   250→        )}
   251→
   252→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   253→        {step === 2 && (
   254→

   255→            {aiLoading ? (
   256→

   257→

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

   260→
AI writing your job post...

   261→

   262→                  Based on market rates and common requirements
   263→

   264→

   265→            ) : (
   266→

   267→

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

   270→

   271→

   272→                  AI pre-filled these. Edit anything you like.
   273→

   274→
   275→

   276→                  {/* Job Details Section */}
   277→

   278→                    Job Title
   279→                    <Input
   280→                      id="title"
   281→                      value={title}
   282→                      onChange={(e) => setTitle(e.target.value)}
   283→                      className="mt-2 h-14 text-base"
   284→                    />
   285→

   286→
   287→

   288→                    Description
   289→                    <textarea
   290→                      id="description"
   291→                      value={description}
   292→                      onChange={(e) => setDescription(e.target.value)}
   293→                      rows={5}
   294→                      className="mt-2 flex 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 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
   295→                    />
   296→

   297→
   298→

   299→                    Pay Rate (AUD)
   300→

   301→

   302→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   303→                        <Input
   304→                          type="number"
   305→                          value={rateMin}
   306→                          onChange={(e) => setRateMin(e.target.value)}
   307→                          placeholder="Min"
   308→                          className="h-14 pl-10 text-base"
   309→                        />
   310→

   311→                      to
   312→

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

   322→

   323→

   324→                      <Button
   325→                        type="button"
   326→                        variant={rateType === "hourly" ? "default" : "outline"}
   327→                        onClick={() => setRateType("hourly")}
   328→                        className="h-12 text-base font-semibold"
   329→                      >
   330→                        Per Hour
   331→                      </Button>
   332→                      <Button
   333→                        type="button"
   334→                        variant={rateType === "daily" ? "default" : "outline"}
   335→                        onClick={() => setRateType("daily")}
   336→                        className="h-12 text-base font-semibold"
   337→                      >
   338→                        Per Day
   339→                      </Button>
   340→

   341→

   342→
   343→                  {/* Location Section */}
   344→

   345→

   346→                      <MapPin className="h-5 w-5" />
   347→                      Location
   348→

   349→
   350→

   351→

   352→                        Site Address
   353→                        <Input
   354→                          id="siteAddress"
   355→                          value={siteAddress}
   356→                          onChange={(e) => setSiteAddress(e.target.value)}
   357→                          placeholder="e.g. 123 George St, Sydney"
   358→                          className="mt-2 h-14 text-base"
   359→                        />
   360→

   361→
   362→

   363→                        Suburb
   364→                        <Input
   365→                          id="suburb"
   366→                          value={suburb}
   367→                          onChange={(e) => setSuburb(e.target.value)}
   368→                          placeholder="e.g. Parramatta"
   369→                          className="mt-2 h-14 text-base"
   370→                        />
   371→

   372→

   373→

   374→
   375→                  {/* Date & Workers Section */}
   376→

   377→

   378→                      <Calendar className="h-5 w-5" />
   379→                      Schedule
   380→

   381→
   382→

   383→

   384→                        Start Date
   385→                        <Input
   386→                          id="startDate"
   387→                          type="date"
   388→                          value={startDate}
   389→                          onChange={(e) => setStartDate(e.target.value)}
   390→                          className="mt-2 h-14 text-base"
   391→                        />
   392→

   393→
   394→

   395→                        Duration
   396→                        <Input
   397→                          id="duration"
   398→                          value={duration}
   399→                          onChange={(e) => setDuration(e.target.value)}
   400→                          placeholder="e.g. 2 weeks, ongoing"
   401→                          className="mt-2 h-14 text-base"
   402→                        />
   403→

   404→
   405→

   406→
   407→                          <Users className="h-5 w-5" />
   408→                          Workers Needed
   409→
   410→

   411→                          <Button
   412→                            type="button"
   413→                            variant="outline"
   414→                            onClick={() =>
   415→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   416→                            }
   417→                            className="h-14 w-14 text-2xl font-bold"
   418→                          >
   419→                            -
   420→                          </Button>
   421→
   422→                            {workersNeeded}
   423→
   424→                          <Button
   425→                            type="button"
   426→                            variant="outline"
   427→                            onClick={() =>
   428→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   429→                            }
   430→                            className="h-14 w-14 text-2xl font-bold"
   431→                          >
   432→                            +
   433→                          </Button>
   434→

   435→

   436→

   437→

   438→
   439→                  {/* Required Certifications */}
   440→                  {certs.length > 0 && (
   441→

   442→                      Required Certifications
   443→

   444→                        {certs.map((cert) => (
   445→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   446→                            {cert}
   447→                            <button
   448→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   449→                              className="ml-2 text-muted-foreground hover:text-foreground"
   450→                            >
   451→                              ×
   452→                            </button>
   453→                          </Badge>
   454→                        ))}
   455→

   456→

   457→                  )}
   458→

   459→

   460→            )}
   461→

   462→        )}
   463→
   464→        {/* STEP 3: Success */}
   465→        {step === 3 && (
   466→          <Card className="mt-8">
   467→            <CardContent className="flex flex-col items-center py-10 text-center">
   468→

   469→                <Check className="h-10 w-10 text-green-600" />
   470→

   471→
Job Posted!

   472→

   473→                We're matching you with the best workers now.
   474→                You'll get notified when we find matches.
   475→

   476→

   477→                <Button
   478→                  onClick={() => router.push("/contractor/matches")}
   479→                  className="w-full h-12 text-base font-semibold"
   480→                >
   481→                  View Matches
   482→                </Button>
   483→                <Button
   484→                  onClick={() => router.push("/dashboard")}
   485→                  variant="outline"
   486→                  className="w-full h-12 text-base"
   487→                >
   488→                  Back to Dashboard
   489→                </Button>
   490→

   491→            </CardContent>
   492→          </Card>
   493→        )}
   494→
   495→
   496→      {/* Sticky Bottom CTA (Step 2 only) */}
   497→      {step === 2 && !aiLoading && (
   498→

   499→

   500→            <Button
   501→              onClick={handlePost}
   502→              className="w-full h-12 text-base font-semibold"
   503→              disabled={loading || !title || !suburb}
   504→            >
   505→              {loading ? (
   506→                <>
   507→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   508→                  Posting...
   509→                </>
   510→              ) : (
   511→                <>
   512→                  Post Job Now
   513→                  <Check className="ml-2 h-6 w-6" />
   514→                </>
   515→              )}
   516→            </Button>
   517→

   518→              Free to post. You only pay $50 when you hire someone.
   519→

   520→

   521→

   522→      )}
   523→

   524→  );
   525→}
   526→

<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→      }).select();
   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>