Search all 9 files that were just fixed for duplicate logger imports. Check if any file has more than one `import { logger }` or `import.*logger` line. The files are:
- src/app/(authenticated)/error.tsx
- src/app/(authenticated)/worker/error.tsx
- src/app/(authenticated)/worker/profile/error.tsx
- src/app/(authenticated)/messages/error.tsx
- src/app/(authenticated)/contractor/payment-success/page.tsx
- src/app/(authenticated)/messages/[conversationId]/page.tsx
- src/app/(authenticated)/messages/page.tsx
- src/app/(authenticated)/worker/jobs/page.tsx
- src/app/(authenticated)/worker/profile/build/page.tsx

Just search and report, don't modify anything.


I'll search all 9 files for duplicate logger imports.


     1→'use client';
     2→
     3→import { useEffect } from 'react';
     4→import { logger } from '@/lib/logger';
     5→import { ErrorDisplay } from '@/components/error-display';
     6→
     7→export default function AuthenticatedError({
     8→  error,
     9→  reset,
    10→}: {
    11→  error: Error & { digest?: string };
    12→  reset: () => void;
    13→}) {
    14→  useEffect(() => {
    15→    // Log the error to an error reporting service
    16→    logger.error('Authenticated route error boundary caught:', error);
    17→  }, [error]);
    18→
    19→  // Check if it's an authentication error
    20→  const isAuthError = error.message?.includes('authentication') ||
    21→                      error.message?.includes('unauthorized') ||
    22→                      error.message?.includes('login');
    23→
    24→  return (
    25→    <div className="min-h-screen bg-background flex items-center justify-center p-4">
    26→      <ErrorDisplay
    27→        error={error}
    28→        onRetry={isAuthError ? undefined : reset}
    29→        showContactSupport={true}
    30→        variant={isAuthError ? 'authentication' : 'default'}
    31→      />
    32→    </div>
    33→  );
    34→}

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


     1→'use client';
     2→
     3→import { useEffect } from 'react';
     4→import { logger } from '@/lib/logger';
     5→import { ErrorDisplay } from '@/components/error-display';
     6→
     7→export default function WorkerError({
     8→  error,
     9→  reset,
    10→}: {
    11→  error: Error & { digest?: string };
    12→  reset: () => void;
    13→}) {
    14→  useEffect(() => {
    15→    logger.error('Worker route error:', error);
    16→  }, [error]);
    17→
    18→  return (
    19→    <div className="min-h-screen bg-background flex items-center justify-center p-4">
    20→      <ErrorDisplay
    21→        title="Something went wrong"
    22→        message="We ran into a problem loading this page. Please try again."
    23→        error={error}
    24→        onRetry={reset}
    25→        onGoHome={() => window.location.href = '/dashboard'}
    26→      />
    27→    </div>
    28→  );
    29→}
    30→

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


1→"use client";
     2→
     3→import { useEffect } from "react";
     4→import { logger } from "@/lib/logger";
     5→import { Button } from "@/components/ui/button";
     6→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     7→import { AlertCircle, RefreshCw, ArrowLeft } from "lucide-react";
     8→import Link from "next/link";
     9→import Image from "next/image";
    10→
    11→export default function Error({
    12→  error,
    13→  reset,
    14→}: {
    15→  error: Error & { digest?: string };
    16→  reset: () => void;
    17→}) {
    18→  useEffect(() => {
    19→    // Log the error to an error reporting service
    20→    logger.error("Worker profile error:", error);
    21→  }, [error]);
    22→
    23→  return (
    24→

    25→      {/* Header */}
    26→
    27→

    28→          <Link href="/dashboard" className="flex items-center gap-2">
    29→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
    30→          </Link>
    31→

    32→
    33→
    34→
    35→        <Card className="mt-8">
    36→          <CardHeader className="text-center">
    37→

    38→              <AlertCircle className="h-6 w-6 text-destructive" />
    39→

    40→            <CardTitle className="text-xl">Profile Build Error</CardTitle>
    41→            <CardDescription className="mt-2">
    42→              We couldn't load the profile builder. This might be a temporary issue.
    43→            </CardDescription>
    44→          </CardHeader>
    45→          <CardContent className="space-y-4">
    46→            {process.env.NODE_ENV === "development" && (
    47→

    48→
Error details:

    49→
{error.message}

    50→                {error.digest && (
    51→
Error ID: {error.digest}

    52→                )}
    53→

    54→            )}
    55→
    56→

    57→              <Button onClick={reset} className="w-full gap-2">
    58→                <RefreshCw className="h-4 w-4" />
    59→                Try again
    60→              </Button>
    61→
    62→              <Link href="/dashboard" className="w-full">
    63→                <Button variant="outline" className="w-full gap-2">
    64→                  <ArrowLeft className="h-4 w-4" />
    65→                  Back to Dashboard
    66→                </Button>
    67→              </Link>
    68→

    69→          </CardContent>
    70→        </Card>
    71→
    72→

    73→  );
    74→}

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


     1→'use client';
     2→
     3→import { useEffect } from 'react';
     4→import { logger } from '@/lib/logger';
     5→import { ErrorDisplay } from '@/components/error-display';
     6→
     7→export default function MessagesError({
     8→  error,
     9→  reset,
    10→}: {
    11→  error: Error & { digest?: string };
    12→  reset: () => void;
    13→}) {
    14→  useEffect(() => {
    15→    logger.error('Messages route error:', error);
    16→  }, [error]);
    17→
    18→  return (
    19→    <div className="min-h-screen bg-background flex items-center justify-center p-4">
    20→      <ErrorDisplay
    21→        title="Something went wrong"
    22→        message="We ran into a problem loading your messages. Please try again."
    23→        error={error}
    24→        onRetry={reset}
    25→        onGoHome={() => window.location.href = '/dashboard'}
    26→      />
    27→    </div>
    28→  );
    29→}
    30→

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


1→'use client';
     2→
     3→import { useEffect, useState } from 'react';
     4→import { useSearchParams, useRouter } from 'next/navigation';
     5→import { logger } from '@/lib/logger';
     6→import Link from 'next/link';
     7→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     8→import { Button } from '@/components/ui/button';
     9→import { Card, CardContent } from '@/components/ui/card';
    10→import { createClient } from '@/lib/supabase/client';
    11→
    12→export default function PaymentSuccessPage() {
    13→  const searchParams = useSearchParams();
    14→  const router = useRouter();
    15→  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
    16→  const [message, setMessage] = useState('Confirming your payment...');
    17→  const supabase = createClient();
    18→
    19→  useEffect(() => {
    20→    const paymentIntent = searchParams.get('payment_intent');
    21→    const paymentIntentClientSecret = searchParams.get('payment_intent_client_secret');
    22→
    23→    if (!paymentIntent || !paymentIntentClientSecret) {
    24→      setStatus('error');
    25→      setMessage('Invalid payment confirmation');
    26→      return;
    27→    }
    28→
    29→    // Check payment status
    30→    async function checkPayment() {
    31→      try {
    32→        // Wait a moment for webhook to process
    33→        await new Promise(resolve => setTimeout(resolve, 2000));
    34→
    35→        // Verify payment intent status with Stripe via API
    36→        const response = await fetch(`/api/payments/verify?payment_intent=${paymentIntent}`);
    37→
    38→        if (!response.ok) {
    39→          const error = await response.json();
    40→          setStatus('error');
    41→          setMessage(error.error || 'Failed to verify payment status');
    42→          return;
    43→        }
    44→
    45→        const { status: stripeStatus } = await response.json();
    46→
    47→        // Only show success if payment is actually succeeded
    48→        if (stripeStatus === 'succeeded') {
    49→          // Double-check our database has recorded it
    50→          const { data: payment } = await supabase
    51→            .from('payments')
    52→            .select('status, match_id')
    53→            .eq('stripe_payment_id', paymentIntent)
    54→            .single();
    55→
    56→          if (payment?.status === 'charged') {
    57→            setStatus('success');
    58→            setMessage('Your payment was successful and the worker has been notified!');
    59→          } else {
    60→            // Stripe succeeded but webhook hasn't processed yet
    61→            setStatus('success');
    62→            setMessage('Payment successful! Processing your hire...');
    63→          }
    64→        } else if (stripeStatus === 'processing') {
    65→          setStatus('success');
    66→          setMessage('Payment is being processed. You will be notified once complete.');
    67→        } else if (stripeStatus === 'requires_payment_method') {
    68→          setStatus('error');
    69→          setMessage('Payment failed. Please try again with a different payment method.');
    70→        } else if (stripeStatus === 'canceled') {
    71→          setStatus('error');
    72→          setMessage('Payment was canceled. Please try again.');
    73→        } else {
    74→          setStatus('error');
    75→          setMessage(`Payment status: ${stripeStatus}. Please contact support if you were charged.`);
    76→        }
    77→      } catch (error) {
    78→        logger.error('Error checking payment:', error);
    79→        setStatus('error');
    80→        setMessage('Unable to verify payment status. Please contact support if you were charged.');
    81→      }
    82→    }
    83→
    84→    checkPayment();
    85→  }, [searchParams, supabase]);
    86→
    87→  return (
    88→

    89→      <Card className="w-full max-w-md">
    90→        <CardContent className="pt-8 pb-6 text-center">
    91→          {status === 'loading' && (
    92→            <>
    93→              <Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
    94→
Confirming Payment

    95→
{message}

    96→            </>
    97→          )}
    98→
    99→          {status === 'success' && (
   100→            <>
   101→

   102→                <CheckCircle2 className="h-10 w-10 text-green-600" />
   103→

   104→
Payment Successful!

   105→
{message}

   106→

   107→                <Button asChild className="w-full h-12 text-base">
   108→                  <Link href="/contractor/matches">Back to Matches</Link>
   109→                </Button>
   110→                <Button asChild variant="outline" className="w-full h-12 text-base">
   111→                  <Link href="/dashboard">Go to Dashboard</Link>
   112→                </Button>
   113→

   114→            </>
   115→          )}
   116→
   117→          {status === 'error' && (
   118→            <>
   119→

   120→                <AlertCircle className="h-10 w-10 text-destructive" />
   121→

   122→
Payment Error

   123→
{message}

   124→

   125→                <Button asChild variant="outline" className="w-full h-12 text-base">
   126→                  <Link href="/contractor/matches">Back to Matches</Link>
   127→                </Button>
   128→

   129→            </>
   130→          )}
   131→        </CardContent>
   132→      </Card>
   133→

   134→  );
   135→}
   136→

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


1→"use client";
     2→
     3→import { useState, useRef, useEffect, use } from "react";
     4→import { useRouter } from "next/navigation";
     5→import { logger } from "@/lib/logger";
     6→import toast from "react-hot-toast";
     7→import { MessageBubble } from "@/components/MessageBubble";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
    11→import { ArrowLeft, Send, MoreVertical } from "lucide-react";
    12→import { createClient } from "@/lib/supabase/client";
    13→import { csrfFetch } from "@/lib/csrf-client";
    14→
    15→interface Message {
    16→  id: string;
    17→  content: string;
    18→  isOwn: boolean;
    19→  timestamp: Date;
    20→  status?: "sent" | "delivered" | "read";
    21→}
    22→
    23→interface OtherParticipant {
    24→  id: string;
    25→  name: string;
    26→  avatar: string | null;
    27→  type: string;
    28→  isOnline: boolean;
    29→}
    30→
    31→interface ConversationData {
    32→  id: string;
    33→  otherParticipant: OtherParticipant;
    34→  messages: Message[];
    35→}
    36→
    37→export default function ConversationPage({ params }: { params: Promise<{ conversationId: string }> }) {
    38→  const router = useRouter();
    39→  const resolvedParams = use(params);
    40→  const conversationId = resolvedParams.conversationId;
    41→
    42→  const [message, setMessage] = useState("");
    43→  const [messages, setMessages] = useState<Message[]>([]);
    44→  const [otherParticipant, setOtherParticipant] = useState<OtherParticipant | null>(null);
    45→  const [loading, setLoading] = useState(true);
    46→  const [sending, setSending] = useState(false);
    47→  const messagesEndRef = useRef<HTMLDivElement>(null);
    48→  const supabase = createClient();
    49→
    50→  const scrollToBottom = () => {
    51→    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
    52→  };
    53→
    54→  useEffect(() => {
    55→    fetchConversation();
    56→
    57→    // Subscribe to new messages in this conversation
    58→    const channel = supabase
    59→      .channel(`conversation:${conversationId}`)
    60→      .on('postgres_changes',
    61→        {
    62→          event: 'INSERT',
    63→          schema: 'public',
    64→          table: 'messages',
    65→          filter: `conversation_id=eq.${conversationId}`
    66→        },
    67→        (payload) => {
    68→          const newMessage = payload.new as { id: string; sender_id: string; content: string; created_at: string; status?: string };
    69→          // Only add if it's not from us (we already added it optimistically)
    70→          supabase.auth.getUser().then(({ data: { user } }) => {
    71→            if (newMessage.sender_id !== user?.id) {
    72→              setMessages(prev => [...prev, {
    73→                id: newMessage.id,
    74→                content: newMessage.content,
    75→                isOwn: false,
    76→                timestamp: new Date(newMessage.created_at),
    77→                status: newMessage.status as "sent" | "delivered" | "read"
    78→              }]);
    79→            }
    80→          });
    81→        }
    82→      )
    83→      .subscribe();
    84→
    85→    return () => {
    86→      supabase.removeChannel(channel);
    87→    };
    88→  }, [conversationId]);
    89→
    90→  useEffect(() => {
    91→    scrollToBottom();
    92→  }, [messages]);
    93→
    94→  const fetchConversation = async () => {
    95→    try {
    96→      const response = await fetch(`/api/messages/${conversationId}`);
    97→      if (response.ok) {
    98→        const data = await response.json();
    99→        const conversation: ConversationData = data.conversation;
   100→        setOtherParticipant(conversation.otherParticipant);
   101→        setMessages(conversation.messages.map(msg => ({
   102→          ...msg,
   103→          timestamp: new Date(msg.timestamp)
   104→        })));
   105→      } else if (response.status === 404) {
   106→        toast.error("Conversation not found");
   107→        router.push('/messages');
   108→      } else {
   109→        toast.error("Failed to load conversation");
   110→      }
   111→    } catch (error) {
   112→      logger.error("Error fetching conversation:", error);
   113→      toast.error("Failed to load conversation. Please try again.");
   114→    } finally {
   115→      setLoading(false);
   116→    }
   117→  };
   118→
   119→  const handleSendMessage = async (e: React.FormEvent) => {
   120→    e.preventDefault();
   121→    if (message.trim() && !sending) {
   122→      setSending(true);
   123→      const messageContent = message.trim();
   124→
   125→      // Optimistic update
   126→      const tempId = Date.now().toString();
   127→      const newMessage: Message = {
   128→        id: tempId,
   129→        content: messageContent,
   130→        isOwn: true,
   131→        timestamp: new Date(),
   132→        status: "sent"
   133→      };
   134→      setMessages(prev => [...prev, newMessage]);
   135→      setMessage("");
   136→
   137→      try {
   138→        const response = await csrfFetch(`/api/messages/${conversationId}/send`, {
   139→          method: 'POST',
   140→          headers: { 'Content-Type': 'application/json' },
   141→          body: JSON.stringify({ content: messageContent })
   142→        });
   143→
   144→        if (response.ok) {
   145→          const data = await response.json();
   146→          // Update the temp message with the real one
   147→          setMessages(prev => prev.map(msg =>
   148→            msg.id === tempId ? { ...data.message, timestamp: new Date(data.message.timestamp) } : msg
   149→          ));
   150→        } else {
   151→          // Remove the optimistic message on error
   152→          setMessages(prev => prev.filter(msg => msg.id !== tempId));
   153→          setMessage(messageContent); // Restore the message
   154→          toast.error("Failed to send message. Please try again.");
   155→        }
   156→      } catch (error) {
   157→        logger.error("Error sending message:", error);
   158→        setMessages(prev => prev.filter(msg => msg.id !== tempId));
   159→        setMessage(messageContent);
   160→        toast.error("Failed to send message. Please try again.");
   161→      } finally {
   162→        setSending(false);
   163→      }
   164→    }
   165→  };
   166→
   167→  const initials = otherParticipant?.name
   168→    ? otherParticipant.name
   169→        .split(" ")
   170→        .map((n) => n[0])
   171→        .join("")
   172→        .toUpperCase()
   173→        .slice(0, 2)
   174→    : "??";
   175→
   176→  if (loading) {
   177→    return (
   178→

   179→
   180→

   181→

   182→              <Button
   183→                variant="ghost"
   184→                size="icon"
   185→                onClick={() => router.push("/messages")}
   186→                className="h-10 w-10"
   187→                aria-label="Go back to messages"
   188→              >
   189→                <ArrowLeft className="h-5 w-5" />
   190→              </Button>
   191→

   192→

   193→

   194→

   195→

   196→

   197→

   198→

   199→

   200→
   201→

   202→
Loading messages...

   203→

   204→

   205→    );
   206→  }
   207→
   208→  return (
   209→

   210→      {/* Header */}
   211→
   212→

   213→

   214→            <Button
   215→              variant="ghost"
   216→              size="icon"
   217→              onClick={() => router.push("/messages")}
   218→              className="h-10 w-10"
   219→            >
   220→              <ArrowLeft className="h-5 w-5" />
   221→            </Button>
   222→

   223→              <Avatar className="h-10 w-10">
   224→                <AvatarImage src={otherParticipant?.avatar || ""} alt={otherParticipant?.name || ""} />
   225→                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
   226→                  {initials}
   227→                </AvatarFallback>
   228→              </Avatar>
   229→

   230→
{otherParticipant?.name || "Unknown"}

   231→

   232→                  {otherParticipant?.isOnline ? "Online" : "Offline"}
   233→

   234→

   235→

   236→

   237→

   238→            <Button variant="ghost" size="icon" className="h-10 w-10" aria-label="More options">
   239→              <MoreVertical className="h-5 w-5" />
   240→            </Button>
   241→

   242→

   243→
   244→
   245→      {/* Messages */}
   246→
   247→

   248→          {messages.length === 0 ? (
   249→

   250→
No messages yet. Say hello!

   251→

   252→          ) : (
   253→

   254→              {messages.map((msg) => (
   255→                <MessageBubble
   256→                  key={msg.id}
   257→                  content={msg.content}
   258→                  isOwn={msg.isOwn}
   259→                  timestamp={msg.timestamp}
   260→                  status={msg.status}
   261→                />
   262→              ))}
   263→

   264→

   265→          )}
   266→

   267→
   268→
   269→      {/* Message Input */}
   270→
   271→

   272→          <form onSubmit={handleSendMessage} className="flex gap-3">
   273→            <Input
   274→              type="text"
   275→              placeholder="Type a message..."
   276→              value={message}
   277→              onChange={(e) => setMessage(e.target.value)}
   278→              className="flex-1 h-12 text-base"
   279→              disabled={sending}
   280→            />
   281→            <Button
   282→              type="submit"
   283→              size="icon"
   284→              className="h-12 w-12 shrink-0"
   285→              disabled={!message.trim() || sending}
   286→              aria-label="Send message"
   287→            >
   288→              <Send className="h-5 w-5" />
   289→            </Button>
   290→          </form>
   291→

   292→
   293→

   294→  );
   295→}
   296→

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

    91→

    92→

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

   103→

   104→

   105→
Loading conversations...

   106→

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

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

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

   116→

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

   126→

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

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

   134→

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

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

   145→        ) : (
   146→

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

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

   166→  );
   167→}
   168→

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


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

    89→      {/* Swipe indicator background */}
    90→      {!hasApplied && swipeX > 0 && (
    91→

    98→          <Check className="h-8 w-8 text-green-600" />
    99→
   100→            {swipeProgress >= 1 ? "Release to apply" : "Swipe to apply"}
   101→
   102→

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

   119→

   120→

   121→                <Badge variant="secondary" className="text-sm shrink-0 py-1 px-3">
   122→                  {tradeLabelFromId(job.trade)}
   123→                </Badge>
   124→                {hasApplied && (
   125→                  <Badge className="text-sm bg-green-600 shrink-0 py-1 px-3">
   126→                    <Check className="h-3.5 w-3.5 mr-1" />
   127→                    Applied
   128→                  </Badge>
   129→                )}
   130→

   131→
{job.title}

   132→

   133→

   134→
   135→

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

   169→        </CardContent>
   170→      </Card>
   171→

   172→  );
   173→}
   174→
   175→export default function WorkerJobsPage() {
   176→  const supabase = createClient();
   177→  const [jobs, setJobs] = useState<Job[]>([]);
   178→  const [loading, setLoading] = useState(true);
   179→  const [activeTrade, setActiveTrade] = useState("All");
   180→  const [selectedJob, setSelectedJob] = useState<Job | null>(null);
   181→  const [applying, setApplying] = useState(false);
   182→  const [appliedJobs, setAppliedJobs] = useState<Set<string>>(new Set());
   183→  const [userId, setUserId] = useState<string | null>(null);
   184→  const [currentPage, setCurrentPage] = useState(1);
   185→  const [totalPages, setTotalPages] = useState(1);
   186→  const [totalCount, setTotalCount] = useState(0);
   187→  const ITEMS_PER_PAGE = 10;
   188→
   189→  useEffect(() => {
   190→    async function load() {
   191→      const { data: { user } } = await supabase.auth.getUser();
   192→      if (user) setUserId(user.id);
   193→
   194→      // Fetch active jobs with pagination
   195→      const from = (currentPage - 1) * ITEMS_PER_PAGE;
   196→      const to = from + ITEMS_PER_PAGE - 1;
   197→
   198→      // Get total count first
   199→      const { count } = await supabase
   200→        .from("jobs")
   201→        .select("*", { count: "exact", head: true })
   202→        .eq("status", "active");
   203→
   204→      if (count) {
   205→        setTotalCount(count);
   206→        setTotalPages(Math.ceil(count / ITEMS_PER_PAGE));
   207→      }
   208→
   209→      // Fetch paginated jobs
   210→      const { data } = await supabase
   211→        .from("jobs")
   212→        .select("id, title, trade, suburb, rate_min, rate_max, rate_type, start_date, workers_needed, status, created_at, location")
   213→        .eq("status", "active")
   214→        .order("created_at", { ascending: false })
   215→        .range(from, to);
   216→
   217→      if (data) {
   218→        // Convert PostgreSQL point to JSON format
   219→        const convertedJobs = data.map((job: any) => ({
   220→          ...job,
   221→          location: postgresPointToJson(job.location)
   222→        }));
   223→        setJobs(convertedJobs);
   224→      }
   225→
   226→      // Fetch already-applied jobs
   227→      if (user) {
   228→        const { data: matches } = await supabase
   229→          .from("matches")
   230→          .select("job_id")
   231→          .eq("worker_id", user.id);
   232→
   233→        if (matches) {
   234→          setAppliedJobs(new Set(matches.map((m: { job_id: string }) => m.job_id)));
   235→        }
   236→      }
   237→
   238→      setLoading(false);
   239→    }
   240→    load();
   241→  }, [supabase, currentPage]);
   242→
   243→  const filteredJobs =
   244→    activeTrade === "All"
   245→      ? jobs
   246→      : jobs.filter(
   247→          (j) => tradeLabelFromId(j.trade).toLowerCase() === activeTrade.toLowerCase()
   248→        );
   249→
   250→  async function handleApply(job: Job) {
   251→    if (!userId || appliedJobs.has(job.id)) return;
   252→    setApplying(true);
   253→
   254→    // Haptic feedback (if supported)
   255→    if (navigator.vibrate) {
   256→      navigator.vibrate(50);
   257→    }
   258→
   259→    const { error } = await supabase.from("matches").insert({
   260→      job_id: job.id,
   261→      worker_id: userId,
   262→      status: "applied",
   263→      contractor_action: "pending",
   264→      worker_action: "accepted",
   265→    });
   266→
   267→    if (error) {
   268→      logger.error("Failed to apply for job:", error);
   269→      toast.error("Failed to apply for job. Please try again.");
   270→      setApplying(false);
   271→      return;
   272→    }
   273→
   274→    setAppliedJobs((prev) => new Set(prev).add(job.id));
   275→    toast.success("Application submitted!");
   276→    setApplying(false);
   277→    setSelectedJob(null);
   278→  }
   279→
   280→  if (loading) {
   281→    return (
   282→

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

   285→    );
   286→  }
   287→
   288→  return (
   289→

   290→
   291→        {/* Trade filters — horizontal scroll */}
   292→

   293→          {TRADE_FILTERS.map((trade) => (
   294→            <button
   295→              key={trade}
   296→              onClick={() => setActiveTrade(trade)}
   297→              className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   298→                activeTrade === trade
   299→                  ? "bg-primary text-primary-foreground"
   300→                  : "bg-muted text-muted-foreground"
   301→              }`}
   302→            >
   303→              {trade}
   304→            </button>
   305→          ))}
   306→

   307→
   308→        {/* Swipe hint */}
   309→        {filteredJobs.length > 0 && !loading && (
   310→

   311→

   312→              👉 Swipe right to quick apply • Tap to view details
   313→

   314→

   315→        )}
   316→
   317→        {/* Job list */}
   318→        {filteredJobs.length === 0 ? (
   319→

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

   322→

   323→              {activeTrade === "All"
   324→                ? "No jobs posted yet. Check back soon."
   325→                : `No ${activeTrade} jobs right now. Try another trade.`}
   326→

   327→

   328→        ) : (
   329→          <>
   330→

   331→              {filteredJobs.map((job) => (
   332→                <SwipeableJobCard
   333→                  key={job.id}
   334→                  job={job}
   335→                  hasApplied={appliedJobs.has(job.id)}
   336→                  onApply={handleApply}
   337→                  onClick={setSelectedJob}
   338→                />
   339→              ))}
   340→

   341→
   342→            {/* Pagination Controls */}
   343→            {totalPages > 1 && (
   344→

   345→                <Button
   346→                  variant="outline"
   347→                  size="sm"
   348→                  onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
   349→                  disabled={currentPage === 1}
   350→                >
   351→                  Previous
   352→                </Button>
   353→
   354→

   355→                  {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
   356→                    let pageNum;
   357→                    if (totalPages <= 5) {
   358→                      pageNum = i + 1;
   359→                    } else if (currentPage <= 3) {
   360→                      pageNum = i + 1;
   361→                    } else if (currentPage >= totalPages - 2) {
   362→                      pageNum = totalPages - 4 + i;
   363→                    } else {
   364→                      pageNum = currentPage - 2 + i;
   365→                    }
   366→
   367→                    return (
   368→                      <Button
   369→                        key={pageNum}
   370→                        variant={currentPage === pageNum ? "default" : "outline"}
   371→                        size="sm"
   372→                        onClick={() => setCurrentPage(pageNum)}
   373→                        className="min-w-[36px]"
   374→                      >
   375→                        {pageNum}
   376→                      </Button>
   377→                    );
   378→                  })}
   379→

   380→
   381→                <Button
   382→                  variant="outline"
   383→                  size="sm"
   384→                  onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
   385→                  disabled={currentPage === totalPages}
   386→                >
   387→                  Next
   388→                </Button>
   389→

   390→            )}
   391→          </>
   392→        )}
   393→
   394→
   395→      {/* Job Detail Sheet (bottom sheet style) */}
   396→      {selectedJob && (
   397→

   398→
 setSelectedJob(null)}
   401→          />
   402→

   403→            {/* Handle */}
   404→

   405→

   406→

   407→
   408→

   409→              <Badge variant="secondary" className="mb-3 text-sm py-1.5 px-3">
   410→                {tradeLabelFromId(selectedJob.trade)}
   411→              </Badge>
   412→
{selectedJob.title}

   413→
   414→

   415→                {selectedJob.suburb && (
   416→
   417→                    <MapPin className="h-5 w-5" />
   418→                    {selectedJob.site_address || selectedJob.suburb}
   419→
   420→                )}
   421→                {(selectedJob.rate_min || selectedJob.rate_max) && (
   422→
   423→                    <DollarSign className="h-5 w-5" />
   424→                    {selectedJob.rate_min && selectedJob.rate_max
   425→                      ? `$${selectedJob.rate_min}–$${selectedJob.rate_max}`
   426→                      : selectedJob.rate_min
   427→                      ? `From $${selectedJob.rate_min}`
   428→                      : `Up to $${selectedJob.rate_max}`}
   429→                    {selectedJob.rate_type === "hourly" ? "/hr" : "/day"}
   430→
   431→                )}
   432→

   433→
   434→

   435→                {selectedJob.start_date && (
   436→
   437→                    <Calendar className="h-5 w-5" />
   438→                    Starts{" "}
   439→                    {new Date(selectedJob.start_date).toLocaleDateString("en-AU", {
   440→                      weekday: "short",
   441→                      day: "numeric",
   442→                      month: "short",
   443→                    })}
   444→
   445→                )}
   446→                {selectedJob.duration && (
   447→                  Duration: {selectedJob.duration}
   448→                )}
   449→                {selectedJob.workers_needed > 1 && (
   450→
   451→                    <HardHat className="h-5 w-5" />
   452→                    {selectedJob.workers_needed} workers needed
   453→
   454→                )}
   455→

   456→
   457→              {selectedJob.description && (
   458→

   459→
About the Job

   460→

   461→                    {selectedJob.description}
   462→

   463→

   464→              )}
   465→
   466→              {selectedJob.certifications_required?.length > 0 && (
   467→

   468→
Required Certs

   469→

   470→                    {selectedJob.certifications_required.map((cert) => (
   471→                      <Badge key={cert} variant="outline" className="text-sm py-1.5 px-3">
   472→                        {cert}
   473→                      </Badge>
   474→                    ))}
   475→

   476→

   477→              )}
   478→

   479→
   480→            {/* Sticky Bottom CTA */}
   481→

   482→

   483→                {appliedJobs.has(selectedJob.id) ? (
   484→                  <Button disabled className="w-full h-16 text-lg font-semibold bg-green-600">
   485→                    <Check className="mr-2 h-6 w-6" />
   486→                    Already Applied
   487→                  </Button>
   488→                ) : (
   489→                  <Button
   490→                    onClick={() => handleApply(selectedJob)}
   491→                    className="w-full h-16 text-lg font-semibold"
   492→                    disabled={applying}
   493→                  >
   494→                    {applying ? (
   495→                      <>
   496→                        <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   497→                        Applying...
   498→                      </>
   499→                    ) : (
   500→                      <>
   501→                        Apply with One Tap
   502→                        <Check className="ml-2 h-6 w-6" />
   503→                      </>
   504→                    )}
   505→                  </Button>
   506→                )}
   507→

   508→                  Always free for workers. No fees ever.
   509→

   510→

   511→

   512→

   513→

   514→      )}
   515→
   516→      <BottomNav userType="worker" />
   517→

   518→  );
   519→}
   520→

<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 { logger } from "@/lib/logger";
     4→
     5→// Voice Processing Optimization:
     6→// This component now uses the consolidated /api/ai/voice-complete endpoint
     7→// which combines transcription, parsing, and profile generation into a single call.
     8→// This reduces the voice processing time from ~5-12 seconds to ~3-7 seconds.
     9→// Previous flow: Whisper → Parse → Generate (3 sequential calls)
    10→// New flow: [Whisper] + Parse/Generate (1-2 calls, parallel where possible)
    11→
    12→import { useState, useEffect, useCallback } from "react";
    13→import { useRouter } from "next/navigation";
    14→import Link from "next/link";
    15→import toast from "react-hot-toast";
    16→import { createClient } from "@/lib/supabase/client";
    17→import { Button } from "@/components/ui/button";
    18→import { Input } from "@/components/ui/input";
    19→import { Label } from "@/components/ui/label";
    20→import { Card, CardContent } from "@/components/ui/card";
    21→import VoiceRecorder from "@/components/VoiceRecorder";
    22→import { csrfFetch, addCSRFTokenToFormData, initializeCSRF } from "@/lib/csrf-client";
    23→import {
    24→  HardHat,
    25→  ArrowLeft,
    26→  ArrowRight,
    27→  Loader2,
    28→  CheckCircle2,
    29→  Sparkles,
    30→  MapPin,
    31→  Clock,
    32→  Shield,
    33→  Edit3,
    34→  Wrench,
    35→  Calendar,
    36→  Zap,
    37→  Mic,
    38→} from "lucide-react";
    39→import { TRADES } from "@/lib/constants";
    40→
    41→type Step = "trade" | "experience" | "location" | "availability" | "generating" | "review";
    42→
    43→const EXPERIENCE_OPTIONS = [
    44→  { label: "Less than 1 year", value: 0 },
    45→  { label: "1-3 years", value: 2 },
    46→  { label: "3-5 years", value: 4 },
    47→  { label: "5-10 years", value: 7 },
    48→  { label: "10+ years", value: 12 },
    49→];
    50→
    51→const AVAILABILITY_OPTIONS = [
    52→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    53→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    54→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    55→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    56→];
    57→
    58→interface ProfileData {
    59→  trade: string;
    60→  experience: number;
    61→  location: string;
    62→  availability: "full-time" | "casual" | "weekends" | "flexible";
    63→}
    64→
    65→interface GeneratedProfile {
    66→  bio: string;
    67→  skills: string[];
    68→  certifications: string[];
    69→}
    70→
    71→function closestExperience(years: number): number {
    72→  const values = EXPERIENCE_OPTIONS.map((o) => o.value);
    73→  return values.reduce((prev, curr) =>
    74→    Math.abs(curr - years) < Math.abs(prev - years) ? curr : prev
    75→  );
    76→}
    77→
    78→export default function ProfileBuildPage() {
    79→  const [step, setStep] = useState<Step>("trade");
    80→  const [loading, setLoading] = useState(true);
    81→  const [generating, setGenerating] = useState(false);
    82→  const [saving, setSaving] = useState(false);
    83→
    84→  // Form data
    85→  const [trade, setTrade] = useState<string>("");
    86→  const [experience, setExperience] = useState<number | null>(null);
    87→  const [location, setLocation] = useState("");
    88→  const [availability, setAvailability] = useState<ProfileData["availability"] | null>(null);
    89→
    90→  // Generated data
    91→  const [generatedBio, setGeneratedBio] = useState("");
    92→  const [generatedSkills, setGeneratedSkills] = useState<string[]>([]);
    93→  const [generatedCerts, setGeneratedCerts] = useState<string[]>([]);
    94→
    95→  // Editable states
    96→  const [editedBio, setEditedBio] = useState("");
    97→  const [editedSkills, setEditedSkills] = useState<string[]>([]);
    98→  const [editedCerts, setEditedCerts] = useState<string[]>([]);
    99→
   100→  // Voice flow
   101→  const [showVoice, setShowVoice] = useState(false);
   102→  const [voiceProcessing, setVoiceProcessing] = useState(false);
   103→  const [voiceStatus, setVoiceStatus] = useState("");
   104→  const [voiceError, setVoiceError] = useState<string | null>(null);
   105→  const [voiceTranscript, setVoiceTranscript] = useState<string | null>(null);
   106→  const [allTrades, setAllTrades] = useState<string[]>([]);
   107→
   108→  const router = useRouter();
   109→  const supabase = createClient();
   110→
   111→  useEffect(() => {
   112→    async function checkAuth() {
   113→      const {
   114→        data: { user },
   115→      } = await supabase.auth.getUser();
   116→      if (!user) {
   117→        router.push("/auth/login");
   118→        return;
   119→      }
   120→      // Initialize CSRF token for API calls
   121→      await initializeCSRF();
   122→      setLoading(false);
   123→    }
   124→    checkAuth();
   125→  }, [supabase.auth, router]);
   126→
   127→  async function generateProfile() {
   128→    if (!trade || experience === null || !location || !availability) return;
   129→
   130→    setGenerating(true);
   131→    setStep("generating");
   132→
   133→    try {
   134→      const response = await csrfFetch("/api/ai/generate-profile", {
   135→        method: "POST",
   136→        headers: { "Content-Type": "application/json" },
   137→        body: JSON.stringify({
   138→          trade,
   139→          trades: allTrades.length > 0 ? allTrades : [trade],
   140→          experience,
   141→          location,
   142→          availability,
   143→          voiceTranscript: voiceTranscript || undefined,
   144→        }),
   145→      });
   146→
   147→      if (!response.ok) {
   148→        const errorData = await response.json().catch(() => ({}));
   149→        if (response.status === 401) {
   150→          toast.error("Session expired. Please log in again.");
   151→          router.push("/auth/login");
   152→          return;
   153→        }
   154→        if (response.status === 429) {
   155→          toast.error("Too many requests. Please wait a few minutes and try again.");
   156→          setStep("availability");
   157→          return;
   158→        }
   159→        throw new Error(errorData.error || "Failed to generate profile");
   160→      }
   161→
   162→      const data: GeneratedProfile = await response.json();
   163→
   164→      setGeneratedBio(data.bio);
   165→      setGeneratedSkills(data.skills);
   166→      setGeneratedCerts(data.certifications);
   167→
   168→      // Initialize editable versions
   169→      setEditedBio(data.bio);
   170→      setEditedSkills(data.skills);
   171→      setEditedCerts(data.certifications);
   172→
   173→      setStep("review");
   174→    } catch (error) {
   175→      logger.error("Generation error:", error);
   176→      const message = error instanceof Error ? error.message : "Failed to generate profile";
   177→      toast.error(`${message}. Please try again.`);
   178→      setStep("availability");
   179→    } finally {
   180→      setGenerating(false);
   181→    }
   182→  }
   183→
   184→  const handleVoiceComplete = useCallback(async (blob: Blob) => {
   185→    setVoiceProcessing(true);
   186→    setVoiceError(null);
   187→    setVoiceStatus("Processing your voice...");
   188→
   189→    try {
   190→      // Use new consolidated endpoint for voice processing
   191→      const formData = new FormData();
   192→      formData.append("audio", blob, "recording.webm");
   193→      addCSRFTokenToFormData(formData);
   194→
   195→      const response = await fetch("/api/ai/voice-complete", {
   196→        method: "POST",
   197→        body: formData,
   198→      });
   199→
   200→      if (!response.ok) {
   201→        if (response.status === 429) {
   202→          throw new Error("Too many requests. Please wait a few minutes.");
   203→        }
   204→        const errData = await response.json().catch(() => ({}));
   205→        throw new Error(errData.error || "Failed to process voice input");
   206→      }
   207→
   208→      const result = await response.json();
   209→
   210→      // Store transcript for potential use in bio generation
   211→      if (result.transcript) {
   212→        setVoiceTranscript(result.transcript);
   213→      }
   214→
   215→      // Handle fallback case where AI processing failed but we have transcript
   216→      if (result.fallback) {
   217→        setVoiceError(result.error || "Could not automatically parse your details. Please complete manually.");
   218→        setShowVoice(false);
   219→        setVoiceStatus("");
   220→        setStep("trade");
   221→        return;
   222→      }
   223→
   224→      const parsed = result.parsed_data;
   225→      const generated = result.generated_profile;
   226→
   227→      // Pre-fill fields — handle multiple trades
   228→      if (parsed.trades && parsed.trades.length > 0) {
   229→        setAllTrades(parsed.trades);
   230→        setTrade(parsed.trades[0]); // primary trade for the UI
   231→      } else if (parsed.trade) {
   232→        setAllTrades([parsed.trade]);
   233→        setTrade(parsed.trade);
   234→      }
   235→      if (parsed.experience_years !== null && parsed.experience_years !== undefined) {
   236→        setExperience(closestExperience(parsed.experience_years));
   237→      }
   238→      if (parsed.location) setLocation(parsed.location);
   239→      if (parsed.availability) setAvailability(parsed.availability);
   240→
   241→      // Store generated profile data if available
   242→      if (generated && generated.bio) {
   243→        setGeneratedBio(generated.bio);
   244→        setGeneratedSkills(generated.skills || []);
   245→        setGeneratedCerts(generated.certifications || []);
   246→
   247→        // Initialize editable versions
   248→        setEditedBio(generated.bio);
   249→        setEditedSkills(generated.skills || []);
   250→        setEditedCerts(generated.certifications || []);
   251→      }
   252→
   253→      // Check what we got and move to the right step
   254→      setShowVoice(false);
   255→      setVoiceStatus("");
   256→
   257→      const hasTrade = !!parsed.trade;
   258→      const hasExp = parsed.experience_years !== null && parsed.experience_years !== undefined;
   259→      const hasLoc = !!parsed.location;
   260→      const hasAvail = !!parsed.availability;
   261→      const hasGeneratedProfile = generated && generated.bio;
   262→
   263→      if (hasTrade && hasExp && hasLoc && hasAvail && hasGeneratedProfile) {
   264→        // All fields filled + profile generated — go straight to review
   265→        setStep("review");
   266→      } else if (!hasTrade) {
   267→        setStep("trade");
   268→      } else if (!hasExp) {
   269→        setStep("experience");
   270→      } else if (!hasLoc) {
   271→        setStep("location");
   272→      } else {
   273→        setStep("availability");
   274→      }
   275→    } catch (error) {
   276→      logger.error("Voice processing error:", error);
   277→      setVoiceError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
   278→      setVoiceStatus("");
   279→    } finally {
   280→      setVoiceProcessing(false);
   281→    }
   282→  }, []);
   283→
   284→  // When step changes to "generating" from voice flow, trigger profile generation
   285→  // Note: This is now less frequently triggered since the new voice-complete endpoint
   286→  // handles both parsing and generation in a single call, improving performance
   287→  useEffect(() => {
   288→    if (step === "generating" && !generating && trade && experience !== null && location && availability) {
   289→      generateProfile();
   290→    }
   291→    // eslint-disable-next-line react-hooks/exhaustive-deps
   292→  }, [step]);
   293→
   294→  async function handleSave() {
   295→    setSaving(true);
   296→
   297→    try {
   298→      const {
   299→        data: { user },
   300→      } = await supabase.auth.getUser();
   301→      if (!user) throw new Error("Not authenticated");
   302→
   303→      // Update or create worker profile
   304→      const { data: existingProfile } = await supabase
   305→        .from("worker_profiles")
   306→        .select("id")
   307→        .eq("profile_id", user.id)
   308→        .single();
   309→
   310→      const profileData = {
   311→        profile_id: user.id,
   312→        trades: allTrades.length > 0 ? allTrades : [trade],
   313→        experience_years: experience,
   314→        availability,
   315→        certifications: editedCerts,
   316→        bio: editedBio,
   317→        ai_extracted: {
   318→          skills: editedSkills,
   319→          generated_at: new Date().toISOString(),
   320→          original_bio: generatedBio,
   321→          original_skills: generatedSkills,
   322→          original_certs: generatedCerts,
   323→        },
   324→      };
   325→
   326→      if (existingProfile) {
   327→        await supabase
   328→          .from("worker_profiles")
   329→          .update(profileData)
   330→          .eq("profile_id", user.id);
   331→      } else {
   332→        await supabase.from("worker_profiles").insert(profileData);
   333→      }
   334→
   335→      // Update main profile
   336→      await supabase
   337→        .from("profiles")
   338→        .update({
   339→          suburb: location,
   340→          onboarding_completed: true,
   341→        })
   342→        .eq("id", user.id);
   343→
   344→      router.push("/dashboard");
   345→      router.refresh();
   346→    } catch (error) {
   347→      logger.error("Save error:", error);
   348→      toast.error("Failed to save profile. Please try again.");
   349→    } finally {
   350→      setSaving(false);
   351→    }
   352→  }
   353→
   354→  const stepOrder: Step[] = ["trade", "experience", "location", "availability", "generating", "review"];
   355→  const currentIndex = stepOrder.indexOf(step);
   356→  const totalSteps = 4; // Only count user-facing steps
   357→
   358→  if (loading) {
   359→    return (
   360→

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

   363→    );
   364→  }
   365→
   366→  return (
   367→

   368→
   369→        {/* Progress bar */}
   370→        {step !== "generating" && step !== "review" && (
   371→

   372→            {Array.from({ length: totalSteps }, (_, i) => (
   373→              <div
   374→                key={i}
   375→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   376→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   377→                }`}
   378→              />
   379→            ))}
   380→

   381→        )}
   382→
   383→        {/* Step: Trade */}
   384→        {step === "trade" && (
   385→

   386→            <StepHeader
   387→              icon={<Wrench className="h-5 w-5" />}
   388→              title="What's your trade?"
   389→              subtitle="Pick your primary trade"
   390→            />
   391→
   392→            {/* Voice option */}
   393→            {!showVoice && (
   394→              <button
   395→                onClick={() => setShowVoice(true)}
   396→                className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 px-5 py-4 text-base font-medium text-primary transition-colors hover:border-primary hover:bg-primary/10 active:scale-95"
   397→                style={{ minHeight: "64px" }}
   398→              >
   399→                <Mic className="h-6 w-6" />
   400→                Or tell us with your voice
   401→              </button>
   402→            )}
   403→
   404→            {showVoice && (
   405→              <Card className="mt-6">
   406→                <CardContent className="flex flex-col items-center gap-4 pt-6">
   407→

   408→                    Say something like: “I'm a chippie with 8 years experience in Parramatta, looking for full-time work”
   409→

   410→
   411→                  {voiceProcessing ? (
   412→

   413→                      <Loader2 className="h-10 w-10 animate-spin text-primary" />
   414→
{voiceStatus}

   415→

   416→                  ) : (
   417→                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />
   418→                  )}
   419→
   420→                  {voiceError && (
   421→
{voiceError}

   422→                  )}
   423→
   424→                  <button
   425→                    onClick={() => {
   426→                      setShowVoice(false);
   427→                      setVoiceError(null);
   428→                    }}
   429→                    className="text-sm text-muted-foreground hover:text-foreground"
   430→                  >
   431→                    Cancel — I'll tap instead
   432→                  </button>
   433→                </CardContent>
   434→              </Card>
   435→            )}
   436→
   437→

   438→              {TRADES.map((t) => (
   439→                <button
   440→                  key={t}
   441→                  onClick={() => {
   442→                    setTrade(t);
   443→                    setAllTrades([t]);
   444→                    setStep("experience");
   445→                  }}
   446→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   447→                    trade === t
   448→                      ? "border-primary bg-primary/10 text-primary"
   449→                      : "border-border hover:border-primary/50"
   450→                  }`}
   451→                  style={{ minHeight: "64px" }}
   452→                >
   453→                  {t}
   454→                </button>
   455→              ))}
   456→

   457→

   458→        )}
   459→
   460→        {/* Step: Experience */}
   461→        {step === "experience" && (
   462→

   463→            <StepHeader
   464→              icon={<Clock className="h-5 w-5" />}
   465→              title="Your experience?"
   466→              subtitle="Years in construction"
   467→            />
   468→
   469→

   470→              {EXPERIENCE_OPTIONS.map((opt) => (
   471→                <button
   472→                  key={opt.value}
   473→                  onClick={() => {
   474→                    setExperience(opt.value);
   475→                    setStep("location");
   476→                  }}
   477→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 text-left text-base font-medium transition-colors active:scale-95 ${
   478→                    experience === opt.value
   479→                      ? "border-primary bg-primary/10 text-primary"
   480→                      : "border-border hover:border-primary/50"
   481→                  }`}
   482→                  style={{ minHeight: "64px" }}
   483→                >
   484→                  {opt.label}
   485→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   486→                </button>
   487→              ))}
   488→

   489→
   490→            <button
   491→              onClick={() => setStep("trade")}
   492→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   493→            >
   494→              <ArrowLeft className="h-4 w-4" />
   495→              Back
   496→            </button>
   497→

   498→        )}
   499→
   500→        {/* Step: Location */}
   501→        {step === "location" && (
   502→

   503→            <StepHeader
   504→              icon={<MapPin className="h-5 w-5" />}
   505→              title="Where are you based?"
   506→              subtitle="Your suburb or area"
   507→            />
   508→
   509→            <form
   510→              onSubmit={(e) => {
   511→                e.preventDefault();
   512→                if (location.trim()) setStep("availability");
   513→              }}
   514→              className="mt-6 space-y-4"
   515→            >
   516→

   517→                Suburb or Area
   518→                <Input
   519→                  id="location"
   520→                  placeholder="e.g. Parramatta, Blacktown"
   521→                  value={location}
   522→                  onChange={(e) => setLocation(e.target.value)}
   523→                  required
   524→                  autoFocus
   525→                  className="h-14 text-base"
   526→                />
   527→

   528→

   529→                We'll match you to nearby jobs
   530→

   531→
   532→

   533→                <Button
   534→                  type="button"
   535→                  variant="outline"
   536→                  onClick={() => setStep("experience")}
   537→                  className="h-14 w-14 p-0"
   538→                >
   539→                  <ArrowLeft className="h-5 w-5" />
   540→                </Button>
   541→                <Button
   542→                  type="submit"
   543→                  disabled={!location.trim()}
   544→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   545→                >
   546→                  Continue
   547→                  <ArrowRight className="h-5 w-5" />
   548→                </Button>
   549→

   550→            </form>
   551→

   552→        )}
   553→
   554→        {/* Step: Availability */}
   555→        {step === "availability" && (
   556→

   557→            <StepHeader
   558→              icon={<Calendar className="h-5 w-5" />}
   559→              title="Your availability?"
   560→              subtitle="When can you work?"
   561→            />
   562→
   563→

   564→              {AVAILABILITY_OPTIONS.map((opt) => (
   565→                <button
   566→                  key={opt.value}
   567→                  onClick={() => {
   568→                    setAvailability(opt.value);
   569→                    generateProfile();
   570→                  }}
   571→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   572→                    availability === opt.value
   573→                      ? "border-primary bg-primary/10"
   574→                      : "border-border hover:border-primary/50"
   575→                  }`}
   576→                  style={{ minHeight: "64px" }}
   577→                >
   578→

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

   582→
{opt.desc}

   583→

   584→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   585→                </button>
   586→              ))}
   587→

   588→
   589→            <button
   590→              onClick={() => setStep("location")}
   591→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   592→            >
   593→              <ArrowLeft className="h-4 w-4" />
   594→              Back
   595→            </button>
   596→

   597→        )}
   598→
   599→        {/* Step: Generating */}
   600→        {step === "generating" && (
   601→

   602→

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

   605→
Building your profile...

   606→

   607→              AI is generating your bio, skills, and certifications
   608→

   609→

   610→

   611→

   612→

   613→

   614→

   615→        )}
   616→
   617→        {/* Step: Review */}
   618→        {step === "review" && (
   619→

   620→

   621→

   622→                <Sparkles className="h-5 w-5" />
   623→

   624→

   625→
Review & Edit

   626→

   627→                  AI generated this — customise as needed
   628→

   629→

   630→

   631→
   632→            {/* Your Answers Summary */}
   633→            <Card className="mt-6">
   634→              <CardContent className="space-y-3 pt-6">
   635→

   636→

   637→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   638→
   639→                      {allTrades.length > 1 ? allTrades.join(", ") : trade}
   640→
   641→

   642→                  <button
   643→                    onClick={() => setStep("trade")}
   644→                    className="text-xs text-primary hover:underline"
   645→                  >
   646→                    Change
   647→                  </button>
   648→

   649→

   650→

   651→                    <Clock className="h-4 w-4 text-muted-foreground" />
   652→
   653→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   654→
   655→

   656→                  <button
   657→                    onClick={() => setStep("experience")}
   658→                    className="text-xs text-primary hover:underline"
   659→                  >
   660→                    Change
   661→                  </button>
   662→

   663→

   664→

   665→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   666→                    {location}
   667→

   668→                  <button
   669→                    onClick={() => setStep("location")}
   670→                    className="text-xs text-primary hover:underline"
   671→                  >
   672→                    Change
   673→                  </button>
   674→

   675→              </CardContent>
   676→            </Card>
   677→
   678→            {/* Bio */}
   679→

   680→

   681→
   682→                  <Sparkles className="h-4 w-4 text-primary" />
   683→                  Your Bio
   684→
   685→                AI-generated
   686→

   687→              <textarea
   688→                value={editedBio}
   689→                onChange={(e) => setEditedBio(e.target.value)}
   690→                className="min-h-[120px] w-full rounded-xl border-2 border-border bg-background px-4 py-3 text-base focus:border-primary focus:outline-none"
   691→                placeholder="Your professional bio..."
   692→              />
   693→

   694→
   695→            {/* Skills */}
   696→

   697→

   698→
   699→                  <Zap className="h-4 w-4 text-primary" />
   700→                  Skills
   701→
   702→                Tap to remove
   703→

   704→

   705→                {editedSkills.map((skill, idx) => (
   706→                  <button
   707→                    key={idx}
   708→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   709→                    className="rounded-full border-2 border-primary bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20 active:scale-95"
   710→                  >
   711→                    {skill} ×
   712→                  </button>
   713→                ))}
   714→

   715→

   716→
   717→            {/* Certifications */}
   718→

   719→

   720→
   721→                  <Shield className="h-4 w-4 text-primary" />
   722→                  Certifications
   723→
   724→                Tap to remove
   725→

   726→

   727→                {editedCerts.map((cert, idx) => (
   728→                  <button
   729→                    key={idx}
   730→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   731→                    className="flex w-full items-center justify-between rounded-xl border-2 border-border bg-background px-4 py-3 text-left text-sm font-medium transition-colors hover:border-destructive hover:bg-destructive/5 active:scale-95"
   732→                  >
   733→                    {cert}
   734→                    Remove
   735→                  </button>
   736→                ))}
   737→

   738→

   739→

   740→        )}
   741→
   742→
   743→      {/* Bottom sticky CTA - only show on review step */}
   744→      {step === "review" && (
   745→

   746→

   747→            <Button
   748→              onClick={handleSave}
   749→              className="h-16 w-full gap-2 text-lg font-semibold"
   750→              disabled={saving}
   751→            >
   752→              {saving ? (
   753→                <>
   754→                  <Loader2 className="h-6 w-6 animate-spin" />
   755→                  Saving profile...
   756→                </>
   757→              ) : (
   758→                <>
   759→                  <CheckCircle2 className="h-6 w-6" />
   760→                  Save My Profile
   761→                </>
   762→              )}
   763→            </Button>
   764→

   765→

   766→      )}
   767→

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

   782→

   783→        {icon}
   784→

   785→

   786→
{title}

   787→
{subtitle}

   788→

   789→

   790→  );
   791→}
   792→

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


I've completed the search across all 9 files. Here's what I found:

## Duplicate Logger Imports Analysis

**GOOD NEWS:** All 9 files have **clean logger imports with NO duplicates**.

Here's the breakdown:

| File | Logger Import Line | Count | Status |
|------|-------------------|-------|--------|
| `src/app/(authenticated)/error.tsx` | `import { logger } from '@/lib/logger';` (line 4) | 1 | Clean |
| `src/app/(authenticated)/worker/error.tsx` | `import { logger } from '@/lib/logger';` (line 4) | 1 | Clean |
| `src/app/(authenticated)/worker/profile/error.tsx` | `import { logger } from "@/lib/logger";` (line 4) | 1 | Clean |
| `src/app/(authenticated)/messages/error.tsx` | `import { logger } from '@/lib/logger';` (line 4) | 1 | Clean |
| `src/app/(authenticated)/contractor/payment-success/page.tsx` | `import { logger } from '@/lib/logger';` (line 5) | 1 | Clean |
| `src/app/(authenticated)/messages/[conversationId]/page.tsx` | `import { logger } from "@/lib/logger";` (line 5) | 1 | Clean |
| `src/app/(authenticated)/messages/page.tsx` | `import { logger } from "@/lib/logger";` (line 5) | 1 | Clean |
| `src/app/(authenticated)/worker/jobs/page.tsx` | `import { logger } from "@/lib/logger";` (line 5) | 1 | Clean |
| `src/app/(authenticated)/worker/profile/build/page.tsx` | `import { logger } from "@/lib/logger";` (line 3) | 1 | Clean |

**Summary:** Each file has exactly one logger import, all pointing to `@/lib/logger`. No duplicate imports detected across any of the files.