Read and review all page components and shared components in /home/ccuser/the-50-dollar-app/src/. Read:
1. All page.tsx files under src/app/(authenticated)/
2. All files under src/components/
3. The worker signup flow (all components under worker/signup/components/)
4. The contractor signup and post-job flows
5. The dashboard, messages, ratings pages

Check for:
- Missing loading/error states
- Unhandled promise rejections
- Missing form validation on the client side
- UX issues (dead links, missing redirects)
- Duplicate code between -rootfix and non-rootfix directories
- State management issues
- Missing optimistic updates
- Accessibility issues (missing aria labels, keyboard navigation)

Return a detailed list of all issues found, organized by area.


I'll start by exploring the directory structure and then systematically read all the relevant files.


Bash

find /home/ccuser/the-50-dollar-app/src/app -name "page.tsx" | sort

Find all page.tsx files


find /home/ccuser/the-50-dollar-app/src/components -type f | sort

Find all component files


find /home/ccuser/the-50-dollar-app/src/app -type f -name "*.tsx" -o -name "*.ts" | sort

Find all tsx/ts files in app directory


Now let me read all the page files and components systematically. I'll start with the authenticated pages and shared components in parallel.


1→import { redirect } from "next/navigation";
     2→import Link from "next/link";
     3→import { createClient } from "@/lib/supabase/server";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import { Building2, HardHat, Plus, Users, Star } from "lucide-react";
     8→import { SignOutButton } from "@/components/sign-out-button";
     9→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    10→
    11→export default async function DashboardPage() {
    12→  const supabase = await createClient();
    13→
    14→  const {
    15→    data: { user },
    16→  } = await supabase.auth.getUser();
    17→
    18→  if (!user) {
    19→    redirect("/auth/login");
    20→  }
    21→
    22→  const { data: profile } = await supabase
    23→    .from("profiles")
    24→    .select("id, type, name, onboarding_completed")
    25→    .eq("id", user.id)
    26→    .single();
    27→
    28→  const isContractor = profile?.type === "contractor";
    29→
    30→  // Fetch stats
    31→  let jobCount = 0;
    32→  let matchCount = 0;
    33→  let pendingRatings: { matchId: string; otherName: string }[] = [];
    34→
    35→  if (isContractor) {
    36→    const { data: company } = await supabase
    37→      .from("companies")
    38→      .select("id")
    39→      .eq("profile_id", user.id)
    40→      .single();
    41→
    42→    if (company) {
    43→      const { count } = await supabase
    44→        .from("jobs")
    45→        .select("*", { count: "exact", head: true })
    46→        .eq("company_id", company.id)
    47→        .eq("status", "active");
    48→      jobCount = count || 0;
    49→
    50→      const { count: mCount } = await supabase
    51→        .from("matches")
    52→        .select("*, jobs!inner(company_id)", { count: "exact", head: true })
    53→        .eq("jobs.company_id", company.id)
    54→        .in("status", ["applied", "suggested"]);
    55→      matchCount = mCount || 0;
    56→    }
    57→  }
    58→
    59→  // Fetch hired matches that haven't been rated yet
    60→  const { data: hiredMatches } = await supabase
    61→    .from("matches")
    62→    .select("id, worker_id, jobs(title)")
    63→    .eq("status", "hired");
    64→
    65→  if (hiredMatches && hiredMatches.length > 0) {
    66→    const matchIds = hiredMatches.map((m) => m.id);
    67→    const workerIds = hiredMatches.map((m) => m.worker_id).filter(Boolean);
    68→
    69→    // Batch fetch all ratings and profiles in parallel
    70→    const [{ data: existingRatings }, { data: profiles }] = await Promise.all([
    71→      supabase
    72→        .from("ratings")
    73→        .select("id, match_id")
    74→        .in("match_id", matchIds)
    75→        .eq("from_id", user.id),
    76→      workerIds.length > 0 && isContractor
    77→        ? supabase.from("profiles").select("id, name").in("id", workerIds)
    78→        : Promise.resolve({ data: [] }),
    79→    ]);
    80→
    81→    // Build lookup maps
    82→    const ratedMatchIds = new Set(existingRatings?.map((r) => r.match_id) || []);
    83→    const profileMap = new Map(profiles?.map((p) => [p.id, p.name]) || []);
    84→
    85→    // Build pending ratings list
    86→    for (const match of hiredMatches) {
    87→      if (!ratedMatchIds.has(match.id)) {
    88→        const otherId = isContractor ? match.worker_id : null;
    89→        const name = otherId && profileMap.has(otherId) ? profileMap.get(otherId)! : "your hire";
    90→        pendingRatings.push({ matchId: match.id, otherName: name });
    91→      }
    92→    }
    93→  }
    94→
    95→  return (
    96→

    97→
    98→

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

   101→

   102→          {isContractor
   103→            ? "Ready to find your next worker?"
   104→            : "Let's get you on a job."}
   105→

   106→
   107→

   108→          {/* Pending ratings prompt */}
   109→          {pendingRatings.length > 0 && (
   110→            <Card className="border-yellow-500/30 bg-yellow-500/5">
   111→              <CardHeader className="pb-3">
   112→                <CardTitle className="flex items-center gap-2 text-lg">
   113→                  <Star className="h-6 w-6 text-yellow-500" />
   114→                  Rate your experience
   115→                </CardTitle>
   116→                <CardDescription className="text-base">
   117→                  Your feedback helps build trust on RateRight
   118→                </CardDescription>
   119→              </CardHeader>
   120→              <CardContent className="space-y-3">
   121→                {pendingRatings.slice(0, 3).map((pr) => (
   122→

   123→                    <Button asChild variant="outline" className="flex-1 h-12 text-base justify-start">
   124→                      <Link href={`/rate/${pr.matchId}`}>
   125→                        <Star className="h-4 w-4 mr-2 text-yellow-500" />
   126→                        Rate {pr.otherName}
   127→                      </Link>
   128→                    </Button>
   129→

   130→                ))}
   131→                {pendingRatings.length > 3 && (
   132→

   133→                    +{pendingRatings.length - 3} more to rate
   134→

   135→                )}
   136→              </CardContent>
   137→            </Card>
   138→          )}
   139→
   140→          {isContractor ? (
   141→            <>
   142→              {/* Quick stats */}
   143→

   144→                <Card>
   145→                  <CardContent className="flex flex-col items-center p-4">
   146→                    {jobCount}
   147→                    Active Jobs
   148→                  </CardContent>
   149→                </Card>
   150→                <Card>
   151→                  <CardContent className="flex flex-col items-center p-4">
   152→                    {matchCount}
   153→                    New Matches
   154→                  </CardContent>
   155→                </Card>
   156→

   157→
   158→              {/* Post a Job CTA */}
   159→              <Card className="border-primary/30 bg-primary/5">
   160→                <CardHeader className="pb-3">
   161→                  <CardTitle className="flex items-center gap-2 text-lg">
   162→                    <Plus className="h-6 w-6" />
   163→                    Post a Job
   164→                  </CardTitle>
   165→                  <CardDescription className="text-base">
   166→                    AI writes the post. You just tap.
   167→                  </CardDescription>
   168→                </CardHeader>
   169→                <CardContent>
   170→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   171→                    <Link href="/contractor/post-job">Post a Job</Link>
   172→                  </Button>
   173→                </CardContent>
   174→              </Card>
   175→
   176→              {/* View Matches */}
   177→              {matchCount > 0 && (
   178→                <Card>
   179→                  <CardHeader className="pb-3">
   180→                    <CardTitle className="flex items-center gap-2 text-lg">
   181→                      <Users className="h-6 w-6" />
   182→                      {matchCount} Worker{matchCount > 1 ? "s" : ""} Matched
   183→                    </CardTitle>
   184→                    <CardDescription className="text-base">
   185→                      Workers have applied to your jobs
   186→                    </CardDescription>
   187→                  </CardHeader>
   188→                  <CardContent>
   189→                    <Button asChild variant="outline" className="w-full h-12 text-base font-semibold">
   190→                      <Link href="/contractor/matches">View Matches</Link>
   191→                    </Button>
   192→                  </CardContent>
   193→                </Card>
   194→              )}
   195→
   196→              {/* Company profile */}
   197→              <Card>
   198→                <CardHeader className="pb-3">
   199→                  <CardTitle className="flex items-center gap-2 text-lg">
   200→                    <Building2 className="h-6 w-6" />
   201→                    Company Profile
   202→                  </CardTitle>
   203→                  <CardDescription className="text-base">
   204→                    Manage your company details
   205→                  </CardDescription>
   206→                </CardHeader>
   207→                <CardContent>
   208→                  <Button asChild variant="outline" className="w-full h-12 text-base">
   209→                    <Link href="/contractor/signup">Edit Profile</Link>
   210→                  </Button>
   211→                </CardContent>
   212→              </Card>
   213→            </>
   214→          ) : (
   215→            <>
   216→              {/* Browse Jobs CTA */}
   217→              <Card className="border-primary/30 bg-primary/5">
   218→                <CardHeader className="pb-3">
   219→                  <CardTitle className="flex items-center gap-2 text-lg">
   220→                    <HardHat className="h-6 w-6" />
   221→                    Find Work
   222→                  </CardTitle>
   223→                  <CardDescription className="text-base">
   224→                    Browse available construction jobs near you
   225→                  </CardDescription>
   226→                </CardHeader>
   227→                <CardContent>
   228→                  <Button asChild className="w-full h-16 text-lg font-semibold">
   229→                    <Link href="/worker/jobs">Browse Jobs</Link>
   230→                  </Button>
   231→                </CardContent>
   232→              </Card>
   233→
   234→              {/* Profile */}
   235→              <Card>
   236→                <CardHeader className="pb-3">
   237→                  <CardTitle className="flex items-center gap-2 text-lg">
   238→                    <HardHat className="h-6 w-6" />
   239→                    Your Profile
   240→                  </CardTitle>
   241→                  <CardDescription className="text-base">
   242→                    {profile?.onboarding_completed
   243→                      ? "View and manage your profile"
   244→                      : "A strong profile gets you more matches"}
   245→                  </CardDescription>
   246→                </CardHeader>
   247→                <CardContent>
   248→                  <Button asChild variant="outline" className="w-full h-12 text-base">
   249→                    <Link href={profile?.onboarding_completed ? "/worker/profile" : "/worker/profile/build"}>
   250→                      {profile?.onboarding_completed ? "View Profile" : "Build Profile"}
   251→                    </Link>
   252→                  </Button>
   253→                </CardContent>
   254→              </Card>
   255→            </>
   256→          )}
   257→

   258→
   259→
   260→      <BottomNavWrapper userType={profile?.type || "worker"} />
   261→

   262→  );
   263→}
   264→

<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 { redirect } from 'next/navigation'
     2→import { createClient } from '@/lib/supabase/server'
     3→import Link from 'next/link'
     4→import Image from 'next/image'
     5→import { SignOutButton } from '@/components/sign-out-button'
     6→import { NotificationBell } from '@/components/notification-bell'
     7→
     8→export default async function AuthenticatedLayout({
     9→  children,
    10→}: {
    11→  children: React.ReactNode
    12→}) {
    13→  const supabase = await createClient()
    14→  const { data: { user } } = await supabase.auth.getUser()
    15→
    16→  if (!user) {
    17→    redirect('/auth/login')
    18→  }
    19→
    20→  return (
    21→

    22→      {/* Header */}
    23→
    24→

    25→          <Link href="/dashboard">
    26→            <Image src="/logo_black.svg" alt="RateRight" width={120} height={34} className="h-7 w-auto" />
    27→          </Link>
    28→

    29→            {user.email}
    30→            <NotificationBell />
    31→            <SignOutButton />
    32→

    33→

    34→
    35→
    36→      {/* Main Content */}
    37→
    38→        {children}
    39→
    40→

    41→  )
    42→}
    43→

<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 { 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 { useRouter } from "next/navigation";
     4→import {
     5→  Bell,
     6→  BriefcaseBusiness,
     7→  CheckCircle2,
     8→  MessageSquare,
     9→  Star,
    10→  Megaphone,
    11→  Loader2,
    12→} from "lucide-react";
    13→import { Button } from "@/components/ui/button";
    14→import { Card, CardContent } from "@/components/ui/card";
    15→import { cn } from "@/lib/utils";
    16→import { formatDistanceToNow } from "date-fns";
    17→import { useNotifications } from "@/hooks/use-notifications";
    18→import type { NotificationType } from "@/lib/types";
    19→
    20→const typeConfig: Record<NotificationType, { icon: typeof Bell; color: string; label: string }> = {
    21→  match: { icon: BriefcaseBusiness, color: "text-blue-600 bg-blue-100", label: "Match" },
    22→  hire_request: { icon: BriefcaseBusiness, color: "text-orange-600 bg-orange-100", label: "Hire Request" },
    23→  hire_confirmed: { icon: CheckCircle2, color: "text-green-600 bg-green-100", label: "Hired!" },
    24→  rating: { icon: Star, color: "text-yellow-600 bg-yellow-100", label: "Rating" },
    25→  job_posted: { icon: Megaphone, color: "text-purple-600 bg-purple-100", label: "New Job" },
    26→  system: { icon: MessageSquare, color: "text-gray-600 bg-gray-100", label: "System" },
    27→};
    28→
    29→export default function NotificationsPage() {
    30→  const router = useRouter();
    31→  const { notifications, unreadCount, loading, markAsRead, markAllAsRead } = useNotifications({
    32→    limit: 50,
    33→  });
    34→
    35→  function handleNotificationClick(notification: (typeof notifications)[0]) {
    36→    markAsRead(notification.id);
    37→
    38→    // Deep link based on notification data
    39→    const data = notification.data as Record<string, string> | null;
    40→    if (data?.conversationId) {
    41→      router.push(`/messages/${data.conversationId}`);
    42→    } else if (notification.type === "hire_confirmed" || notification.type === "match") {
    43→      router.push("/dashboard");
    44→    } else if (notification.type === "job_posted") {
    45→      router.push("/worker/jobs");
    46→    }
    47→  }
    48→
    49→  if (loading) {
    50→    return (
    51→

    52→

    53→
Notifications

    54→

    55→

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

    58→

    59→    );
    60→  }
    61→
    62→  return (
    63→

    64→

    65→

    66→
Notifications

    67→          {unreadCount > 0 && (
    68→
    69→              {unreadCount}
    70→
    71→          )}
    72→

    73→        {unreadCount > 0 && (
    74→          <Button variant="ghost" size="sm" onClick={markAllAsRead}>
    75→            Mark all read
    76→          </Button>
    77→        )}
    78→

    79→
    80→      {notifications.length === 0 ? (
    81→

    82→

    83→            <Bell className="h-6 w-6 text-muted-foreground" />
    84→

    85→
No notifications yet

    86→

    87→            You'll see updates about jobs, matches, and messages here.
    88→

    89→

    90→      ) : (
    91→

    92→          {notifications.map((notification) => {
    93→            const config = typeConfig[notification.type];
    94→            const Icon = config.icon;
    95→
    96→            return (
    97→              <Card
    98→                key={notification.id}
    99→                className={cn(
   100→                  "cursor-pointer transition-colors hover:bg-accent/50 active:scale-[0.99]",
   101→                  !notification.read && "border-primary/30 bg-primary/5"
   102→                )}
   103→                onClick={() => handleNotificationClick(notification)}
   104→              >
   105→                <CardContent className="flex items-start gap-3 p-4">
   106→

   113→                    <Icon className="h-4 w-4" />
   114→

   115→

   116→

   117→

   123→                        {notification.title}
   124→

   125→                      {!notification.read && (
   126→
   127→                      )}
   128→

   129→                    {notification.body && (
   130→

   131→                        {notification.body}
   132→

   133→                    )}
   134→

   135→                      {formatDistanceToNow(new Date(notification.created_at), {
   136→                        addSuffix: true,
   137→                      })}
   138→

   139→

   140→                </CardContent>
   141→              </Card>
   142→            );
   143→          })}
   144→

   145→      )}
   146→

   147→  );
   148→}
   149→

<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, useParams } from "next/navigation";
     5→import { createClient } from "@/lib/supabase/client";
     6→import { Button } from "@/components/ui/button";
     7→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     8→import { Star, Loader2, CheckCircle2 } from "lucide-react";
     9→import { cn } from "@/lib/utils";
    10→import { MAX_RATING_TEXT_LENGTH } from "@/lib/constants";
    11→
    12→export default function RatePage() {
    13→  const [score, setScore] = useState(0);
    14→  const [hoverScore, setHoverScore] = useState(0);
    15→  const [comment, setComment] = useState("");
    16→  const [loading, setLoading] = useState(true);
    17→  const [submitting, setSubmitting] = useState(false);
    18→  const [success, setSuccess] = useState(false);
    19→  const [error, setError] = useState<string | null>(null);
    20→  const [otherName, setOtherName] = useState<string>("");
    21→  const [alreadyRated, setAlreadyRated] = useState(false);
    22→
    23→  const router = useRouter();
    24→  const params = useParams();
    25→  const matchId = params.matchId as string;
    26→  const supabase = createClient();
    27→
    28→  useEffect(() => {
    29→    async function loadMatch() {
    30→      const { data: { user } } = await supabase.auth.getUser();
    31→      if (!user) { router.push("/auth/login"); return; }
    32→
    33→      // Get match details
    34→      const { data: match } = await supabase
    35→        .from("matches")
    36→        .select("id, worker_id, status, jobs(company_id, title)")
    37→        .eq("id", matchId)
    38→        .single();
    39→
    40→      if (!match || match.status !== "hired") {
    41→        setError("This match is not available for rating.");
    42→        setLoading(false);
    43→        return;
    44→      }
    45→
    46→      // Check if already rated
    47→      const { data: existingRating } = await supabase
    48→        .from("ratings")
    49→        .select("id")
    50→        .eq("match_id", matchId)
    51→        .eq("from_id", user.id)
    52→        .single();
    53→
    54→      if (existingRating) {
    55→        setAlreadyRated(true);
    56→        setLoading(false);
    57→        return;
    58→      }
    59→
    60→      // Get the other person's name
    61→      const isWorker = match.worker_id === user.id;
    62→      const jobs = match.jobs as unknown as { company_id: string; title: string } | null;
    63→
    64→      if (isWorker && jobs?.company_id) {
    65→        const { data: company } = await supabase
    66→          .from("companies")
    67→          .select("name, profile_id")
    68→          .eq("id", jobs.company_id)
    69→          .single();
    70→        setOtherName(company?.name || "the contractor");
    71→      } else if (!isWorker) {
    72→        const { data: profile } = await supabase
    73→          .from("profiles")
    74→          .select("name")
    75→          .eq("id", match.worker_id)
    76→          .single();
    77→        setOtherName(profile?.name || "the worker");
    78→      } else {
    79→        setOtherName("the other party");
    80→      }
    81→
    82→      setLoading(false);
    83→    }
    84→
    85→    loadMatch();
    86→  }, [matchId, supabase, router]);
    87→
    88→  async function handleSubmit(e: React.FormEvent) {
    89→    e.preventDefault();
    90→    if (score === 0) { setError("Please select a rating."); return; }
    91→
    92→    setSubmitting(true);
    93→    setError(null);
    94→
    95→    const { data: { user } } = await supabase.auth.getUser();
    96→    if (!user) return;
    97→
    98→    // Get the other person's ID
    99→    const { data: match } = await supabase
   100→      .from("matches")
   101→      .select("worker_id, jobs(company_id)")
   102→      .eq("id", matchId)
   103→      .single();
   104→
   105→    if (!match) { setError("Match not found."); setSubmitting(false); return; }
   106→
   107→    const isWorker = match.worker_id === user.id;
   108→    let toId: string;
   109→
   110→    if (isWorker) {
   111→      // Worker rating the contractor — need to find the contractor's profile_id
   112→      const jobs = match.jobs as unknown as { company_id: string } | null;
   113→      if (!jobs?.company_id) { setError("Could not find contractor."); setSubmitting(false); return; }
   114→      const { data: company } = await supabase.from("companies").select("profile_id").eq("id", jobs.company_id).single();
   115→      if (!company) { setError("Could not find contractor."); setSubmitting(false); return; }
   116→      toId = company.profile_id;
   117→    } else {
   118→      toId = match.worker_id;
   119→    }
   120→
   121→    const { error: insertError } = await supabase.from("ratings").insert({
   122→      match_id: matchId,
   123→      from_id: user.id,
   124→      to_id: toId,
   125→      score,
   126→      comment: comment.trim() || null,
   127→    });
   128→
   129→    if (insertError) {
   130→      setError(insertError.message);
   131→      setSubmitting(false);
   132→      return;
   133→    }
   134→
   135→    setSuccess(true);
   136→    setSubmitting(false);
   137→
   138→    setTimeout(() => {
   139→      router.push("/dashboard");
   140→      router.refresh();
   141→    }, 2000);
   142→  }
   143→
   144→  if (loading) {
   145→    return (
   146→

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

   149→    );
   150→  }
   151→
   152→  if (alreadyRated) {
   153→    return (
   154→

   155→        <Card className="w-full max-w-md">
   156→          <CardHeader className="text-center">
   157→

   158→              <CheckCircle2 className="h-6 w-6 text-green-600" />
   159→

   160→            <CardTitle className="text-2xl font-bold">Already rated</CardTitle>
   161→            <CardDescription className="mt-2">
   162→              You've already submitted a rating for this job.
   163→            </CardDescription>
   164→          </CardHeader>
   165→          <CardContent>
   166→            <Button asChild className="w-full h-12 text-base" variant="outline">
   167→              Back to dashboard
   168→            </Button>
   169→          </CardContent>
   170→        </Card>
   171→

   172→    );
   173→  }
   174→
   175→  if (success) {
   176→    return (
   177→

   178→        <Card className="w-full max-w-md">
   179→          <CardHeader className="text-center">
   180→

   181→              <CheckCircle2 className="h-6 w-6 text-green-600" />
   182→

   183→            <CardTitle className="text-2xl font-bold">Thanks for rating!</CardTitle>
   184→            <CardDescription className="mt-2">
   185→              Your feedback helps build trust on RateRight. Redirecting...
   186→            </CardDescription>
   187→          </CardHeader>
   188→        </Card>
   189→

   190→    );
   191→  }
   192→
   193→  return (
   194→

   195→      <Card className="w-full max-w-md">
   196→        <CardHeader className="text-center">
   197→          <CardTitle className="text-2xl font-bold">Rate your experience</CardTitle>
   198→          <CardDescription className="mt-2">
   199→            How was working with {otherName}?
   200→          </CardDescription>
   201→        </CardHeader>
   202→        <CardContent>
   203→          <form onSubmit={handleSubmit} className="space-y-6">
   204→            {/* Star Rating */}
   205→

   206→              {[1, 2, 3, 4, 5].map((value) => (
   207→                <button
   208→                  key={value}
   209→                  type="button"
   210→                  onClick={() => setScore(value)}
   211→                  onMouseEnter={() => setHoverScore(value)}
   212→                  onMouseLeave={() => setHoverScore(0)}
   213→                  className="p-1 transition-transform active:scale-90"
   214→                  aria-label={`${value} star${value !== 1 ? "s" : ""}`}
   215→                >
   216→                  <Star
   217→                    className={cn(
   218→                      "h-10 w-10 transition-colors",
   219→                      (hoverScore || score) >= value
   220→                        ? "fill-yellow-400 text-yellow-400"
   221→                        : "text-muted-foreground/30"
   222→                    )}
   223→                  />
   224→                </button>
   225→              ))}
   226→

   227→

   228→              {score === 0 && "Tap a star to rate"}
   229→              {score === 1 && "Poor"}
   230→              {score === 2 && "Below average"}
   231→              {score === 3 && "Average"}
   232→              {score === 4 && "Good"}
   233→              {score === 5 && "Excellent"}
   234→

   235→
   236→            {/* Comment */}
   237→

   238→
   239→                Comment (optional)
   240→
   241→              <textarea
   242→                id="comment"
   243→                value={comment}
   244→                onChange={(e) => setComment(e.target.value)}
   245→                placeholder="How was the experience?"
   246→                rows={3}
   247→                maxLength={MAX_RATING_TEXT_LENGTH}
   248→                className="w-full rounded-lg border bg-background px-3 py-2 text-base resize-none focus:outline-none focus:ring-2 focus:ring-primary/50"
   249→              />
   250→

   251→
   252→            {error && (
   253→

   254→
{error}

   255→

   256→            )}
   257→
   258→            <Button
   259→              type="submit"
   260→              className="w-full h-12 text-base font-semibold"
   261→              disabled={submitting || score === 0}
   262→            >
   263→              {submitting ? "Submitting..." : "Submit Rating"}
   264→            </Button>
   265→          </form>
   266→
   267→

   268→
   269→              Skip for now
   270→
   271→

   272→        </CardContent>
   273→      </Card>
   274→

   275→  );
   276→}
   277→

<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 { redirect } from "next/navigation";
     2→import { createClient } from "@/lib/supabase/server";
     3→import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
     4→import { Button } from "@/components/ui/button";
     5→import { ArrowLeft } from "lucide-react";
     6→import { RatingsList } from "@/components/ratings-list";
     7→import { RatingsSummary } from "@/components/ratings-summary";
     8→import Link from "next/link";
     9→
    10→interface PageProps {
    11→  params: { userId: string };
    12→}
    13→
    14→export default async function UserRatingsPage({ params }: PageProps) {
    15→  const supabase = await createClient();
    16→
    17→  const {
    18→    data: { user },
    19→  } = await supabase.auth.getUser();
    20→
    21→  if (!user) {
    22→    redirect("/auth/login");
    23→  }
    24→
    25→  const userId = params.userId;
    26→
    27→  // Fetch the profile we're viewing ratings for
    28→  const { data: profile } = await supabase
    29→    .from("profiles")
    30→    .select("id, name, type")
    31→    .eq("id", userId)
    32→    .single();
    33→
    34→  if (!profile) {
    35→    redirect("/dashboard");
    36→  }
    37→
    38→  // Fetch ratings with profile information
    39→  const { data: ratings } = await supabase
    40→    .from("ratings")
    41→    .select(`
    42→      *,
    43→      from_profile:profiles!ratings_from_id_fkey(
    44→        id,
    45→        name,
    46→        avatar_url
    47→      )
    48→    `)
    49→    .eq("to_id", userId)
    50→    .order("created_at", { ascending: false });
    51→
    52→  // Calculate average rating
    53→  const averageRating = ratings?.length
    54→    ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    55→    : 0;
    56→
    57→  const pageTitle = `${profile.name || 'User'}'s Reviews`;
    58→
    59→  return (
    60→

    61→
    62→

    63→          <Button asChild variant="ghost" size="icon">
    64→            <Link href="/dashboard">
    65→              <ArrowLeft className="h-5 w-5" />
    66→            </Link>
    67→          </Button>
    68→
{pageTitle}

    69→

    70→
    71→        {/* Ratings Summary */}
    72→        <RatingsSummary
    73→          averageRating={averageRating}
    74→          totalRatings={ratings?.length || 0}
    75→          className="mb-6"
    76→        />
    77→
    78→        {/* All Ratings */}
    79→        <RatingsList
    80→          ratings={ratings || []}
    81→          title="All Reviews"
    82→          emptyMessage="No reviews yet. Reviews appear here after work is completed."
    83→        />
    84→
    85→

    86→  );
    87→}

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

   115→      {/* Swipe indicator backgrounds */}
   116→      {!isHired && swipeX !== 0 && (
   117→        <>
   118→          {/* Right swipe (hire) */}
   119→          {swipeX > 0 && (
   120→

   127→              <UserCheck className="h-8 w-8 text-green-600" />
   128→
   129→                {swipeProgress >= 1 ? "Release to hire" : "Swipe to hire"}
   130→
   131→

   132→          )}
   133→          {/* Left swipe (reject) */}
   134→          {swipeX < 0 && (
   135→

   142→
   143→                {swipeProgress >= 1 ? "Release to pass" : "Swipe to pass"}
   144→
   145→              <AlertCircle className="h-8 w-8 text-red-600" />
   146→

   147→          )}
   148→        </>
   149→      )}
   150→
   151→      <Card
   152→        className={`transition-all touch-pan-y ${justHired ? "ring-2 ring-green-500" : ""}`}
   153→        style={{
   154→          transform: `translateX(${swipeX}px)`,
   155→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   156→        }}
   157→        onTouchStart={handleTouchStart}
   158→        onTouchMove={handleTouchMove}
   159→        onTouchEnd={handleTouchEnd}
   160→      >
   161→        <CardContent className="p-5">
   162→

   163→            {/* Avatar */}
   164→

   165→              {avatarInitial}
   166→

   167→
   168→

   169→

   170→

   171→                  {displayName}
   172→

   173→                {isHired && (
   174→                  <Badge className="bg-green-600 text-xs shrink-0">Hired</Badge>
   175→                )}
   176→

   177→
   178→              {/* AI Summary */}
   179→

   180→                {summary}
   181→

   182→
   183→              {/* Details */}
   184→

   185→                {match.worker_profile?.experience_years && (
   186→
   187→                    <Clock className="h-4 w-4" />
   188→                    {match.worker_profile.experience_years}yr exp
   189→
   190→                )}
   191→                {match.worker?.suburb && (
   192→
   193→                    <MapPin className="h-4 w-4" />
   194→                    {match.worker.suburb}
   195→
   196→                )}
   197→                {match.match_score && (
   198→
   199→                    <Star className="h-4 w-4 fill-amber-400 text-amber-400" />
   200→                    {Math.round(match.match_score)}% match
   201→
   202→                )}
   203→

   204→
   205→              {/* Certs */}
   206→              {match.worker_profile?.certifications && match.worker_profile.certifications.length > 0 && (
   207→

   208→                  {match.worker_profile.certifications.slice(0, 3).map((cert) => (
   209→                    <Badge key={cert} variant="outline" className="text-xs px-2 py-1">
   210→                      {cert}
   211→                    </Badge>
   212→                  ))}
   213→

   214→              )}
   215→

   216→

   217→
   218→          {/* Action */}
   219→          {!isHired && (
   220→            <Button
   221→              onClick={() => onHire(match)}
   222→              className="mt-4 w-full h-12 text-base font-semibold"
   223→              disabled={hiring}
   224→            >
   225→              {hiring ? (
   226→                <>
   227→                  <Loader2 className="mr-2 h-5 w-5 animate-spin" />
   228→                  Processing...
   229→                </>
   230→              ) : (
   231→                <>
   232→                  <UserCheck className="mr-2 h-5 w-5" />
   233→                  Hire — $50 Fee
   234→                </>
   235→              )}
   236→            </Button>
   237→          )}
   238→
   239→          {justHired && (
   240→

   241→              <CheckCircle2 className="h-5 w-5" />
   242→              Hired! Worker has been notified.
   243→

   244→          )}
   245→
   246→          {isHired && !justHired && (
   247→

   248→              <CheckCircle2 className="h-5 w-5 text-green-600" />
   249→              Hired on{" "}
   250→              {match.hired_at
   251→                ? new Date(match.hired_at).toLocaleDateString("en-AU", {
   252→                    day: "numeric",
   253→                    month: "short",
   254→                  })
   255→                : "recently"}
   256→

   257→          )}
   258→        </CardContent>
   259→      </Card>
   260→

   261→  );
   262→}
   263→
   264→const TRADE_LABEL: Record<string, string> = TRADE_CATEGORIES.reduce((acc, cat) => {
   265→  acc[cat.id] = cat.label;
   266→  return acc;
   267→}, {} as Record<string, string>);
   268→
   269→function generateAiSummary(
   270→  worker: MatchWithWorker["worker"],
   271→  profile: MatchWithWorker["worker_profile"]
   272→): string {
   273→  const parts: string[] = [];
   274→  if (profile?.trades?.length) {
   275→    parts.push(profile.trades.map((t) => TRADE_LABEL[t] || t).join(", "));
   276→  }
   277→  if (profile?.experience_years) {
   278→    parts.push(`${profile.experience_years} years exp`);
   279→  }
   280→  if (worker?.suburb) {
   281→    parts.push(worker.suburb);
   282→  }
   283→  if (profile?.availability) {
   284→    parts.push(profile.availability);
   285→  }
   286→  if (profile?.certifications?.length) {
   287→    parts.push(profile.certifications.slice(0, 2).join(", "));
   288→  }
   289→  return parts.join(" \u00B7 ") || "Worker profile";
   290→}
   291→
   292→export default function ContractorMatchesPage() {
   293→  const supabase = createClient();
   294→  const [jobs, setJobs] = useState<Job[]>([]);
   295→  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
   296→  const [matches, setMatches] = useState<MatchWithWorker[]>([]);
   297→  const [loading, setLoading] = useState(true);
   298→  const [loadingMatches, setLoadingMatches] = useState(false);
   299→  const [hiring, setHiring] = useState<string | null>(null);
   300→  const [hireSuccess, setHireSuccess] = useState<string | null>(null);
   301→  const [currentPage, setCurrentPage] = useState(1);
   302→  const [totalPages, setTotalPages] = useState(1);
   303→  const [totalCount, setTotalCount] = useState(0);
   304→  const ITEMS_PER_PAGE = 10;
   305→
   306→  // Payment modal state
   307→  const [companyId, setCompanyId] = useState<string | null>(null);
   308→  const [selectedMatch, setSelectedMatch] = useState<MatchWithWorker | null>(null);
   309→  const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
   310→
   311→  // Load contractor's jobs
   312→  useEffect(() => {
   313→    async function load() {
   314→      const { data: { user } } = await supabase.auth.getUser();
   315→      if (!user) return;
   316→
   317→      const { data: company } = await supabase
   318→        .from("companies")
   319→        .select("id")
   320→        .eq("profile_id", user.id)
   321→        .single();
   322→
   323→      if (!company) {
   324→        setLoading(false);
   325→        return;
   326→      }
   327→
   328→      setCompanyId(company.id);
   329→
   330→      const { data: jobsData } = await supabase
   331→        .from("jobs")
   332→        .select("id, company_id, status, created_at")
   333→        .eq("company_id", company.id)
   334→        .in("status", ["active", "filled"])
   335→        .order("created_at", { ascending: false });
   336→
   337→      if (jobsData && jobsData.length > 0) {
   338→        // Convert PostgreSQL point to JSON format
   339→        const convertedJobs = jobsData.map((job: any) => ({
   340→          ...job,
   341→          location: postgresPointToJson(job.location)
   342→        }));
   343→        setJobs(convertedJobs);
   344→        setSelectedJobId(jobsData[0].id);
   345→      }
   346→      setLoading(false);
   347→    }
   348→    load();
   349→  }, [supabase]);
   350→
   351→  // Load matches for selected job
   352→  useEffect(() => {
   353→    if (!selectedJobId) return;
   354→    async function loadMatches() {
   355→      setLoadingMatches(true);
   356→
   357→      // Get total count first
   358→      const { count } = await supabase
   359→        .from("matches")
   360→        .select("*", { count: "exact", head: true })
   361→        .eq("job_id", selectedJobId);
   362→
   363→      if (count) {
   364→        setTotalCount(count);
   365→        setTotalPages(Math.ceil(count / ITEMS_PER_PAGE));
   366→      }
   367→
   368→      // Calculate pagination range
   369→      const from = (currentPage - 1) * ITEMS_PER_PAGE;
   370→      const to = from + ITEMS_PER_PAGE - 1;
   371→
   372→      // Get matches for this job with pagination
   373→      const { data: matchData } = await supabase
   374→        .from("matches")
   375→        .select("id, job_id, worker_id, status, match_score, ai_summary, contractor_action, worker_action, hired_at, fee_charged, created_at")
   376→        .eq("job_id", selectedJobId)
   377→        .order("match_score", { ascending: false })
   378→        .range(from, to);
   379→
   380→      if (!matchData || matchData.length === 0) {
   381→        setMatches([]);
   382→        setLoadingMatches(false);
   383→        return;
   384→      }
   385→
   386→      // Fetch worker profiles and base profiles
   387→      const workerIds: string[] = [];
   388→      for (const m of matchData) {
   389→        if (m.worker_id) workerIds.push(m.worker_id);
   390→      }
   391→
   392→      if (workerIds.length === 0) {
   393→        setMatches([]);
   394→        setLoadingMatches(false);
   395→        return;
   396→      }
   397→
   398→      const [{ data: profiles }, { data: workerProfiles }] = await Promise.all([
   399→        supabase.from("profiles").select("id, name, display_name, suburb, avatar_url").in("id", workerIds),
   400→        supabase.from("worker_profiles").select("profile_id, trades, experience_years, certifications, availability").in("profile_id", workerIds),
   401→      ]);
   402→
   403→      type ProfileRow = { id: string; name: string | null; display_name: string | null; suburb: string | null; avatar_url: string | null };
   404→      type WPRow = { profile_id: string; trades: string[]; experience_years: number | null; certifications: string[]; availability: string | null };
   405→
   406→      const profileMap = new Map((profiles || []).map((p: ProfileRow) => [p.id, p]));
   407→      const wpMap = new Map((workerProfiles || []).map((w: WPRow) => [w.profile_id, w]));
   408→
   409→      const enriched: MatchWithWorker[] = matchData.map((m: Record<string, unknown>) => ({
   410→        id: m.id as string,
   411→        job_id: m.job_id as string,
   412→        worker_id: m.worker_id as string,
   413→        status: m.status as string,
   414→        match_score: m.match_score as number | null,
   415→        ai_summary: m.ai_summary as string | null,
   416→        contractor_action: m.contractor_action as string,
   417→        worker_action: m.worker_action as string,
   418→        hired_at: m.hired_at as string | null,
   419→        fee_charged: m.fee_charged as boolean,
   420→        created_at: m.created_at as string,
   421→        worker: profileMap.get(m.worker_id as string) || null,
   422→        worker_profile: wpMap.get(m.worker_id as string) || null,
   423→      }));
   424→
   425→      setMatches(enriched);
   426→      setLoadingMatches(false);
   427→    }
   428→    loadMatches();
   429→  }, [selectedJobId, supabase, currentPage]);
   430→
   431→  async function handleHire(match: MatchWithWorker) {
   432→    // Haptic feedback
   433→    if (navigator.vibrate) {
   434→      navigator.vibrate(50);
   435→    }
   436→
   437→    if (!companyId) {
   438→      toast.error("Company not found. Please complete your profile.");
   439→      return;
   440→    }
   441→
   442→    // Open payment modal
   443→    setSelectedMatch(match);
   444→    setIsPaymentModalOpen(true);
   445→  }
   446→
   447→  async function handlePaymentSuccess() {
   448→    if (!selectedMatch) return;
   449→
   450→    const supabase = createClient();
   451→    const hiredAt = new Date().toISOString();
   452→
   453→    // CRITICAL: Persist to database BEFORE updating local state
   454→    const { error } = await supabase
   455→      .from("matches")
   456→      .update({
   457→        status: "hired",
   458→        contractor_action: "accepted",
   459→        hired_at: hiredAt,
   460→        fee_charged: true,
   461→      })
   462→      .eq("id", selectedMatch.id);
   463→
   464→    if (error) {
   465→      logger.error("Failed to update match in database:", error);
   466→      alert("Failed to complete hire. Please try again.");
   467→      return;
   468→    }
   469→
   470→    // Update local state to show hired
   471→    setMatches((prev) =>
   472→      prev.map((m) =>
   473→        m.id === selectedMatch.id
   474→          ? { ...m, status: "hired", contractor_action: "accepted", hired_at: hiredAt, fee_charged: true }
   475→          : m
   476→      )
   477→    );
   478→
   479→    setHireSuccess(selectedMatch.id);
   480→    setIsPaymentModalOpen(false);
   481→    setSelectedMatch(null);
   482→    setTimeout(() => setHireSuccess(null), 3000);
   483→  }
   484→
   485→  async function handleReject(match: MatchWithWorker) {
   486→    // Haptic feedback
   487→    if (navigator.vibrate) {
   488→      navigator.vibrate([30, 20, 30]);
   489→    }
   490→
   491→    // Remove from list (soft delete - just update status)
   492→    await supabase
   493→      .from("matches")
   494→      .update({
   495→        contractor_action: "rejected",
   496→      })
   497→      .eq("id", match.id);
   498→
   499→    // Remove from UI
   500→    setMatches((prev) => prev.filter((m) => m.id !== match.id));
   501→  }
   502→
   503→  if (loading) {
   504→    return (
   505→

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

   508→    );
   509→  }
   510→
   511→  return (
   512→

   513→
   514→        {jobs.length === 0 ? (
   515→

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

   518→

   519→              Post a job to start seeing matched workers.
   520→

   521→            <Button asChild className="mt-8 h-16 px-8 text-lg font-semibold">
   522→              <Link href="/contractor/post-job">Post a Job</Link>
   523→            </Button>
   524→

   525→        ) : (
   526→          <>
   527→            {/* Job selector — horizontal scroll */}
   528→            {jobs.length > 1 && (
   529→

   530→                {jobs.map((job) => (
   531→                  <button
   532→                    key={job.id}
   533→                    onClick={() => {
   534→                      setSelectedJobId(job.id);
   535→                      setCurrentPage(1); // Reset to first page when changing jobs
   536→                    }}
   537→                    className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   538→                      selectedJobId === job.id
   539→                        ? "bg-primary text-primary-foreground"
   540→                        : "bg-muted text-muted-foreground"
   541→                    }`}
   542→                  >
   543→                    {job.title}
   544→                  </button>
   545→                ))}
   546→

   547→            )}
   548→
   549→            {/* Selected job summary */}
   550→            {selectedJobId && (
   551→

   552→                {(() => {
   553→                  const job = jobs.find((j) => j.id === selectedJobId);
   554→                  if (!job) return null;
   555→                  return (
   556→

   557→                      <Badge variant="secondary">{TRADE_LABEL[job.trade] || job.trade}</Badge>
   558→                      {job.suburb && (
   559→
   560→                          <MapPin className="h-3.5 w-3.5" />
   561→                          {job.suburb}
   562→
   563→                      )}
   564→
   565→                        {matches.length} match{matches.length !== 1 ? "es" : ""}
   566→
   567→

   568→                  );
   569→                })()}
   570→

   571→            )}
   572→
   573→            {/* Matches */}
   574→            {loadingMatches ? (
   575→

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

   578→            ) : matches.length === 0 ? (
   579→

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

   582→

   583→                  Workers will show up here as they apply. We'll notify you.
   584→

   585→

   586→            ) : (
   587→              <>
   588→

   589→                  {/* Swipe hint */}
   590→                  {matches.length > 0 && matches.some(m => m.status !== "hired") && (
   591→

   592→

   593→                        👉 Swipe right to hire • Swipe left to pass
   594→

   595→

   596→                  )}
   597→
   598→                  {matches.map((match) => (
   599→                    <SwipeableMatchCard
   600→                      key={match.id}
   601→                      match={match}
   602→                      isHired={match.status === "hired"}
   603→                      justHired={hireSuccess === match.id}
   604→                      hiring={hiring === match.id}
   605→                      onHire={handleHire}
   606→                      onReject={handleReject}
   607→                    />
   608→                  ))}
   609→

   610→
   611→                {/* Pagination Controls */}
   612→                {totalPages > 1 && (
   613→

   614→                    <Button
   615→                      variant="outline"
   616→                      size="sm"
   617→                      onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
   618→                      disabled={currentPage === 1}
   619→                    >
   620→                      Previous
   621→                    </Button>
   622→
   623→

   624→                      {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
   625→                        let pageNum;
   626→                        if (totalPages <= 5) {
   627→                          pageNum = i + 1;
   628→                        } else if (currentPage <= 3) {
   629→                          pageNum = i + 1;
   630→                        } else if (currentPage >= totalPages - 2) {
   631→                          pageNum = totalPages - 4 + i;
   632→                        } else {
   633→                          pageNum = currentPage - 2 + i;
   634→                        }
   635→
   636→                        return (
   637→                          <Button
   638→                            key={pageNum}
   639→                            variant={currentPage === pageNum ? "default" : "outline"}
   640→                            size="sm"
   641→                            onClick={() => setCurrentPage(pageNum)}
   642→                            className="min-w-[36px]"
   643→                          >
   644→                            {pageNum}
   645→                          </Button>
   646→                        );
   647→                      })}
   648→

   649→
   650→                    <Button
   651→                      variant="outline"
   652→                      size="sm"
   653→                      onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
   654→                      disabled={currentPage === totalPages}
   655→                    >
   656→                      Next
   657→                    </Button>
   658→

   659→                )}
   660→              </>
   661→            )}
   662→          </>
   663→        )}
   664→
   665→
   666→      <BottomNav userType="contractor" />
   667→
   668→      {/* Payment Modal */}
   669→      {selectedMatch && companyId && (
   670→        <HirePaymentModal
   671→          isOpen={isPaymentModalOpen}
   672→          onClose={() => {
   673→            setIsPaymentModalOpen(false);
   674→            setSelectedMatch(null);
   675→          }}
   676→          onSuccess={handlePaymentSuccess}
   677→          matchId={selectedMatch.id}
   678→          companyId={companyId}
   679→          amount={PAYMENT_AMOUNT}
   680→          workerName={selectedMatch.worker?.display_name || undefined}
   681→        />
   682→      )}
   683→

   684→  );
   685→}
   686→

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

   254→
   255→        {/* STEP 1: Select Trade */}
   256→        {step === 1 && (
   257→

   258→
What trade do you need?

   259→

   260→              Tap to select. AI will write the job post for you.
   261→

   262→

   263→              {TRADE_CATEGORIES.map((trade) => {
   264→                const Icon = trade.icon;
   265→                return (
   266→                  <button
   267→                    key={trade.id}
   268→                    onClick={() => handleTradeSelect(trade.id)}
   269→                    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}`}
   270→                  >
   271→                    <Icon className="h-10 w-10" />
   272→                    {trade.label}
   273→                  </button>
   274→                );
   275→              })}
   276→

   277→

   278→        )}
   279→
   280→        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
   281→        {step === 2 && (
   282→

   283→            {aiLoading ? (
   284→

   285→

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

   288→
AI writing your job post...

   289→

   290→                  Based on market rates and common requirements
   291→

   292→

   293→            ) : (
   294→

   295→

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

   298→

   299→

   300→                  AI pre-filled these. Edit anything you like.
   301→

   302→
   303→

   304→                  {/* Job Details Section */}
   305→

   306→                    Job Title
   307→                    <Input
   308→                      id="title"
   309→                      value={title}
   310→                      onChange={(e) => setTitle(e.target.value)}
   311→                      className="mt-2 h-14 text-base"
   312→                    />
   313→

   314→
   315→

   316→                    Description
   317→                    <textarea
   318→                      id="description"
   319→                      value={description}
   320→                      onChange={(e) => setDescription(e.target.value)}
   321→                      rows={5}
   322→                      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"
   323→                    />
   324→

   325→
   326→

   327→                    Pay Rate (AUD)
   328→

   329→

   330→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   331→                        <Input
   332→                          type="number"
   333→                          value={rateMin}
   334→                          onChange={(e) => setRateMin(e.target.value)}
   335→                          placeholder="Min"
   336→                          className="h-14 pl-10 text-base"
   337→                        />
   338→

   339→                      to
   340→

   341→                        <DollarSign className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
   342→                        <Input
   343→                          type="number"
   344→                          value={rateMax}
   345→                          onChange={(e) => setRateMax(e.target.value)}
   346→                          placeholder="Max"
   347→                          className="h-14 pl-10 text-base"
   348→                        />
   349→

   350→

   351→

   352→                      <Button
   353→                        type="button"
   354→                        variant={rateType === "hourly" ? "default" : "outline"}
   355→                        onClick={() => setRateType("hourly")}
   356→                        className="h-12 text-base font-semibold"
   357→                      >
   358→                        Per Hour
   359→                      </Button>
   360→                      <Button
   361→                        type="button"
   362→                        variant={rateType === "daily" ? "default" : "outline"}
   363→                        onClick={() => setRateType("daily")}
   364→                        className="h-12 text-base font-semibold"
   365→                      >
   366→                        Per Day
   367→                      </Button>
   368→

   369→

   370→
   371→                  {/* Location Section */}
   372→

   373→

   374→                      <MapPin className="h-5 w-5" />
   375→                      Location
   376→

   377→
   378→

   379→

   380→                        Site Address
   381→                        <Input
   382→                          id="siteAddress"
   383→                          value={siteAddress}
   384→                          onChange={(e) => setSiteAddress(e.target.value)}
   385→                          placeholder="e.g. 123 George St, Sydney"
   386→                          className="mt-2 h-14 text-base"
   387→                        />
   388→

   389→
   390→

   391→                        Suburb
   392→                        <Input
   393→                          id="suburb"
   394→                          value={suburb}
   395→                          onChange={(e) => setSuburb(e.target.value)}
   396→                          placeholder="e.g. Parramatta"
   397→                          className="mt-2 h-14 text-base"
   398→                        />
   399→

   400→

   401→

   402→
   403→                  {/* Date & Workers Section */}
   404→

   405→

   406→                      <Calendar className="h-5 w-5" />
   407→                      Schedule
   408→

   409→
   410→

   411→

   412→                        Start Date
   413→                        <Input
   414→                          id="startDate"
   415→                          type="date"
   416→                          value={startDate}
   417→                          onChange={(e) => setStartDate(e.target.value)}
   418→                          className="mt-2 h-14 text-base"
   419→                        />
   420→

   421→
   422→

   423→                        Duration
   424→                        <Input
   425→                          id="duration"
   426→                          value={duration}
   427→                          onChange={(e) => setDuration(e.target.value)}
   428→                          placeholder="e.g. 2 weeks, ongoing"
   429→                          className="mt-2 h-14 text-base"
   430→                        />
   431→

   432→
   433→

   434→
   435→                          <Users className="h-5 w-5" />
   436→                          Workers Needed
   437→
   438→

   439→                          <Button
   440→                            type="button"
   441→                            variant="outline"
   442→                            onClick={() =>
   443→                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
   444→                            }
   445→                            className="h-14 w-14 text-2xl font-bold"
   446→                          >
   447→                            -
   448→                          </Button>
   449→
   450→                            {workersNeeded}
   451→
   452→                          <Button
   453→                            type="button"
   454→                            variant="outline"
   455→                            onClick={() =>
   456→                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))
   457→                            }
   458→                            className="h-14 w-14 text-2xl font-bold"
   459→                          >
   460→                            +
   461→                          </Button>
   462→

   463→

   464→

   465→

   466→
   467→                  {/* Required Certifications */}
   468→                  {certs.length > 0 && (
   469→

   470→                      Required Certifications
   471→

   472→                        {certs.map((cert) => (
   473→                          <Badge key={cert} variant="secondary" className="text-sm py-1.5 px-3">
   474→                            {cert}
   475→                            <button
   476→                              onClick={() => setCerts(certs.filter((c) => c !== cert))}
   477→                              className="ml-2 text-muted-foreground hover:text-foreground"
   478→                            >
   479→                              ×
   480→                            </button>
   481→                          </Badge>
   482→                        ))}
   483→

   484→

   485→                  )}
   486→

   487→

   488→            )}
   489→

   490→        )}
   491→
   492→        {/* STEP 3: Success */}
   493→        {step === 3 && (
   494→          <Card className="mt-8">
   495→            <CardContent className="flex flex-col items-center py-10 text-center">
   496→

   497→                <Check className="h-10 w-10 text-green-600" />
   498→

   499→
Job Posted!

   500→

   501→                We're matching you with the best workers now.
   502→                You'll get notified when we find matches.
   503→

   504→

   505→                <Button
   506→                  onClick={() => router.push("/contractor/matches")}
   507→                  className="w-full h-12 text-base font-semibold"
   508→                >
   509→                  View Matches
   510→                </Button>
   511→                <Button
   512→                  onClick={() => router.push("/dashboard")}
   513→                  variant="outline"
   514→                  className="w-full h-12 text-base"
   515→                >
   516→                  Back to Dashboard
   517→                </Button>
   518→

   519→            </CardContent>
   520→          </Card>
   521→        )}
   522→
   523→
   524→      {/* Sticky Bottom CTA (Step 2 only) */}
   525→      {step === 2 && !aiLoading && (
   526→

   527→

   528→            <Button
   529→              onClick={handlePost}
   530→              className="w-full h-12 text-base font-semibold"
   531→              disabled={loading || !title || !suburb}
   532→            >
   533→              {loading ? (
   534→                <>
   535→                  <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   536→                  Posting...
   537→                </>
   538→              ) : (
   539→                <>
   540→                  Post Job Now
   541→                  <Check className="ml-2 h-6 w-6" />
   542→                </>
   543→              )}
   544→            </Button>
   545→

   546→              Free to post. You only pay $50 when you hire someone.
   547→

   548→

   549→

   550→      )}
   551→

   552→  );
   553→}
   554→

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