Thoroughly explore the RateRight codebase to document the WORKER user flow. I need to understand every screen, component, API call, and data collection point for workers.

Search for:
1. Worker signup flow - look in src/app/(auth)/, src/app/signup/, or similar auth directories
2. Worker profile creation - look for worker profile forms, worker onboarding
3. Job browsing screens - look for job listing, job search, job browse pages
4. Job application flow - how workers apply to jobs
5. Getting hired - what happens when a worker is selected
6. Job completion flow - how jobs are marked complete
7. Contact details reveal - when/how worker contact info is shown to contractors
8. Any worker dashboard or home screen

For each screen/component found, note:
- The file path and route
- What data is collected or displayed
- What Supabase/API calls are made
- Any guards or conditions (auth checks, payment checks, etc.)

Search broadly across: src/app/, src/components/, src/lib/, app/, components/, lib/
Also check for any API routes in src/app/api/ or app/api/

Be very thorough - check all directories and naming conventions.


I'll thoroughly explore the RateRight codebase to document the worker user flow. Let me start by getting a broad understanding of the project structure.


Bash

find /home/ccuser/the-50-dollar-app -type f -name "*.tsx" -o -name "*.ts" -o -name "*.json" | grep -E "(app|src)" | head -50


Let me get the directory structure more clearly:


ls -la /home/ccuser/the-50-dollar-app/ | grep -E "^d"


drwxrwxr-x  12 ccuser ccuser    4096 Feb  9 16:21 .
drwxr-x---  21 ccuser ccuser    4096 Feb  9 18:47 ..
drwxrwxr-x   8 ccuser ccuser    4096 Feb  9 18:47 .git
drwxr-xr-x   9 root   root      4096 Feb  9 16:21 .next
drwxrwxr-x   9 ccuser ccuser    4096 Feb  9 14:46 .next-build
drwxr-xr-x  10 root   root      4096 Feb  9 06:55 .next-old
drwxrwxr-x   2 ccuser ccuser    4096 Feb  8 21:33 docs
drwxr-xr-x 348 ccuser ccuser   16384 Feb  9 04:44 node_modules
drwxrwxr-x   2 ccuser ccuser    4096 Feb  9 04:01 public
drwxrwxr-x   2 ccuser ccuser    4096 Feb  9 03:29 scripts
drwxrwxr-x   7 ccuser ccuser    4096 Feb  9 07:00 src
drwxrwxr-x   4 ccuser ccuser    4096 Feb  8 11:18 supabase


find /home/ccuser/the-50-dollar-app/src -type d | head -50


/home/ccuser/the-50-dollar-app/src
/home/ccuser/the-50-dollar-app/src/app
/home/ccuser/the-50-dollar-app/src/app/auth
/home/ccuser/the-50-dollar-app/src/app/auth/forgot-password
/home/ccuser/the-50-dollar-app/src/app/auth/callback
/home/ccuser/the-50-dollar-app/src/app/auth/reset-password
/home/ccuser/the-50-dollar-app/src/app/auth/login
/home/ccuser/the-50-dollar-app/src/app/auth/signup
/home/ccuser/the-50-dollar-app/src/app/auth/verify-email
/home/ccuser/the-50-dollar-app/src/app/faq
/home/ccuser/the-50-dollar-app/src/app/legal
/home/ccuser/the-50-dollar-app/src/app/legal/terms-of-service
/home/ccuser/the-50-dollar-app/src/app/legal/privacy-policy
/home/ccuser/the-50-dollar-app/src/app/api
/home/ccuser/the-50-dollar-app/src/app/api/auth
/home/ccuser/the-50-dollar-app/src/app/api/auth/csrf
/home/ccuser/the-50-dollar-app/src/app/api/auth/verify-email
/home/ccuser/the-50-dollar-app/src/app/api/payments
/home/ccuser/the-50-dollar-app/src/app/api/payments/verify
/home/ccuser/the-50-dollar-app/src/app/api/payments/create
/home/ccuser/the-50-dollar-app/src/app/api/ai
/home/ccuser/the-50-dollar-app/src/app/api/ai/transcribe-voice
/home/ccuser/the-50-dollar-app/src/app/api/ai/parse-profile-voice
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-profile
/home/ccuser/the-50-dollar-app/src/app/api/ai/voice-complete-rootfix
/home/ccuser/the-50-dollar-app/src/app/api/ai/voice-complete
/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-job
/home/ccuser/the-50-dollar-app/src/app/api/ratings-rootfix
/home/ccuser/the-50-dollar-app/src/app/api/webhooks
/home/ccuser/the-50-dollar-app/src/app/api/webhooks/stripe
/home/ccuser/the-50-dollar-app/src/app/api/company-logo
/home/ccuser/the-50-dollar-app/src/app/api/match
/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches
/home/ccuser/the-50-dollar-app/src/app/api/geocode
/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup
/home/ccuser/the-50-dollar-app/src/app/api/ratings
/home/ccuser/the-50-dollar-app/src/app/api/messages
/home/ccuser/the-50-dollar-app/src/app/api/messages/start
/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]
/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/send
/home/ccuser/the-50-dollar-app/src/app/offline
/home/ccuser/the-50-dollar-app/src/app/(authenticated)
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/rate
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/rate/[matchId]
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/dashboard
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/components
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/components-rootfix
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/utils


Good! Now let me search for all the pages and components related to workers:


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/dashboard/loading.tsx
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/dashboard/page.tsx


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/rate/[matchId]/page.tsx


Now let me look at the API routes and authentication flow:


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


Perfect! Now let me read the key files for the worker flow. Let me start with the main pages and understand the architecture:


1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import Image from "next/image";
     7→import { Loader2, HardHat } from "lucide-react";
     8→import { useWorkerAuth } from "./hooks/useWorkerAuth";
     9→import { useWorkerProfile } from "./hooks/useWorkerProfile";
    10→import { useVoiceProcessing } from "./hooks/useVoiceProcessing";
    11→import { AuthStep } from "./components/AuthStep";
    12→import { MethodStep } from "./components/MethodStep";
    13→import { VoiceStep } from "./components/VoiceStep";
    14→import { TradeStep } from "./components/TradeStep";
    15→import { ExperienceStep } from "./components/ExperienceStep";
    16→import { LocationStep } from "./components/LocationStep";
    17→import { AvailabilityStep } from "./components/AvailabilityStep";
    18→import { CertificationsStep } from "./components/CertificationsStep";
    19→import { ReviewStep } from "./components/ReviewStep";
    20→import { ProgressBar } from "./components/ProgressBar";
    21→import type { Step } from "./types";
    22→
    23→export default function WorkerSignupPage() {
    24→  const [step, setStep] = useState<Step>("auth");
    25→
    26→  const {
    27→    isAuthenticated,
    28→    loading: authLoading,
    29→    email,
    30→    setEmail,
    31→    password,
    32→    setPassword,
    33→    authError,
    34→    authLoading: isAuthLoading,
    35→    handleAuth,
    36→  } = useWorkerAuth();
    37→
    38→  const {
    39→    selectedTrades,
    40→    setSelectedTrades,
    41→    experience,
    42→    setExperience,
    43→    suburb,
    44→    setSuburb,
    45→    availability,
    46→    setAvailability,
    47→    certifications,
    48→    setCertifications,
    49→    saving,
    50→    toggleTrade,
    51→    toggleCert,
    52→    saveWorkerProfile,
    53→    workerData,
    54→  } = useWorkerProfile();
    55→
    56→  const {
    57→    voiceState,
    58→    handleVoiceComplete,
    59→  } = useVoiceProcessing(setSelectedTrades, setExperience, setSuburb, setAvailability, setStep);
    60→
    61→  useEffect(() => {
    62→    if (isAuthenticated && step === "auth") {
    63→      setStep("method");
    64→    }
    65→  }, [isAuthenticated, step]);
    66→
    67→  const stepOrder: Step[] = ["auth", "method", "voice", "trade", "experience", "location", "availability", "certs", "review"];
    68→  const progressSteps = ["auth", "method", "trade", "experience", "location", "availability", "certs", "review"]; // Exclude voice from progress bar
    69→  const activeSteps = isAuthenticated ? stepOrder.slice(1) : stepOrder;
    70→  const activeProgressSteps = isAuthenticated ? progressSteps.slice(1) : progressSteps;
    71→  const currentIndex = activeProgressSteps.indexOf(step === "voice" ? "method" : step); // Map voice to method for progress
    72→  const totalSteps = activeProgressSteps.length;
    73→
    74→  if (authLoading) {
    75→    return (
    76→

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

    79→    );
    80→  }
    81→
    82→  return (
    83→

    84→      {/* Header */}
    85→
    86→

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

    91→
    92→
    93→
    94→        {/* Progress bar */}
    95→        <ProgressBar currentStep={currentIndex} totalSteps={totalSteps} />
    96→
    97→        {/* Step: Auth */}
    98→        {step === "auth" && (
    99→          <AuthStep
   100→            email={email}
   101→            setEmail={setEmail}
   102→            password={password}
   103→            setPassword={setPassword}
   104→            authError={authError}
   105→            authLoading={isAuthLoading}
   106→            onSubmit={handleAuth}
   107→          />
   108→        )}
   109→
   110→        {/* Step: Choose method */}
   111→        {step === "method" && (
   112→          <MethodStep
   113→            onTypeSelect={() => setStep("trade")}
   114→            onVoiceSelect={() => setStep("voice")}
   115→          />
   116→        )}
   117→
   118→        {/* Step: Voice recording */}
   119→        {step === "voice" && (
   120→          <VoiceStep
   121→            voiceProcessing={voiceState.processing}
   122→            voiceStatus={voiceState.status}
   123→            voiceError={voiceState.error}
   124→            onRecordingComplete={handleVoiceComplete}
   125→            onBack={() => setStep("method")}
   126→          />
   127→        )}
   128→
   129→        {/* Step: Select trade */}
   130→        {step === "trade" && (
   131→          <TradeStep
   132→            selectedTrades={selectedTrades}
   133→            onTradeToggle={toggleTrade}
   134→            onContinue={() => setStep("experience")}
   135→            onBack={() => setStep(isAuthenticated ? "method" : "auth")}
   136→          />
   137→        )}
   138→
   139→        {/* Step: Experience */}
   140→        {step === "experience" && (
   141→          <ExperienceStep
   142→            experience={experience}
   143→            onExperienceSelect={(exp) => {
   144→              setExperience(exp);
   145→              setStep("location");
   146→            }}
   147→            onBack={() => setStep("trade")}
   148→          />
   149→        )}
   150→
   151→        {/* Step: Location */}
   152→        {step === "location" && (
   153→          <LocationStep
   154→            suburb={suburb}
   155→            setSuburb={setSuburb}
   156→            onContinue={() => setStep("availability")}
   157→            onBack={() => setStep("experience")}
   158→          />
   159→        )}
   160→
   161→        {/* Step: Availability */}
   162→        {step === "availability" && (
   163→          <AvailabilityStep
   164→            availability={availability}
   165→            onAvailabilitySelect={(avail) => {
   166→              setAvailability(avail);
   167→              setStep("certs");
   168→            }}
   169→            onBack={() => setStep("location")}
   170→          />
   171→        )}
   172→
   173→        {/* Step: Certifications */}
   174→        {step === "certs" && (
   175→          <CertificationsStep
   176→            certifications={certifications}
   177→            onCertToggle={toggleCert}
   178→            onContinue={() => setStep("review")}
   179→            onBack={() => setStep("availability")}
   180→          />
   181→        )}
   182→
   183→        {/* Step: Review */}
   184→        {step === "review" && workerData && (
   185→          <ReviewStep
   186→            workerData={workerData}
   187→            onEdit={setStep}
   188→            onConfirm={saveWorkerProfile}
   189→            saving={saving}
   190→            onBack={() => setStep("certs")}
   191→          />
   192→        )}
   193→
   194→

   195→  );
   196→}

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


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

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

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

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

   119→

   120→

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

   131→
{job.title}

   132→

   133→

   134→
   135→

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

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

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

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

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

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

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

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

   311→

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

   314→

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

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

   322→

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

   327→

   328→        ) : (
   329→          <>
   330→

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

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

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

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

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

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

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

   403→            {/* Handle */}
   404→

   405→

   406→

   407→
   408→

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

   413→
   414→

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

   433→
   434→

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

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

   459→
About the Job

   460→

   461→                    {selectedJob.description}
   462→

   463→

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

   468→
Required Certs

   469→

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

   476→

   477→              )}
   478→

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

   482→

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

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

   510→

   511→

   512→

   513→

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

   518→  );
   519→}
   520→

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


1→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 } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import {
     8→  HardHat,
     9→  MapPin,
    10→  Clock,
    11→  Shield,
    12→  Zap,
    13→  Edit3,
    14→  Wrench,
    15→  Calendar,
    16→  Sparkles,
    17→  Camera,
    18→  Star,
    19→} from "lucide-react";
    20→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    21→import { AvatarUpload } from "@/components/avatar-upload";
    22→import { RatingsSummary } from "@/components/ratings-summary";
    23→import { RatingsList } from "@/components/ratings-list";
    24→
    25→export default async function WorkerProfilePage() {
    26→  const supabase = await createClient();
    27→
    28→  const {
    29→    data: { user },
    30→  } = await supabase.auth.getUser();
    31→
    32→  if (!user) {
    33→    redirect("/auth/login");
    34→  }
    35→
    36→  const { data: profile } = await supabase
    37→    .from("profiles")
    38→    .select("id, name, avatar_url, suburb, onboarding_completed")
    39→    .eq("id", user.id)
    40→    .single();
    41→
    42→  // If onboarding not completed, send to build
    43→  if (!profile?.onboarding_completed) {
    44→    redirect("/worker/profile/build");
    45→  }
    46→
    47→  const { data: workerProfile } = await supabase
    48→    .from("worker_profiles")
    49→    .select("profile_id, trades, experience_years, certifications, bio, ai_extracted, availability")
    50→    .eq("profile_id", user.id)
    51→    .single();
    52→
    53→  if (!workerProfile) {
    54→    redirect("/worker/profile/build");
    55→  }
    56→
    57→  const trades = workerProfile.trades || [];
    58→  const skills = (workerProfile.ai_extracted as { skills?: string[] })?.skills || [];
    59→  const certs = workerProfile.certifications || [];
    60→  const expLabel = getExperienceLabel(workerProfile.experience_years);
    61→
    62→  // Fetch ratings with profile information
    63→  const { data: ratings } = await supabase
    64→    .from("ratings")
    65→    .select(`
    66→      *,
    67→      from_profile:profiles!ratings_from_id_fkey(
    68→        id,
    69→        name,
    70→        avatar_url
    71→      )
    72→    `)
    73→    .eq("to_id", user.id)
    74→    .order("created_at", { ascending: false })
    75→    .limit(3); // Show latest 3 ratings
    76→
    77→  const avgRating = ratings && ratings.length > 0
    78→    ? (ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length).toFixed(1)
    79→    : null;
    80→
    81→  const totalRatings = ratings?.length || 0;
    82→
    83→  return (
    84→

    85→
    86→        {/* Profile header */}
    87→

    88→          <AvatarUpload
    89→            currentUrl={profile.avatar_url}
    90→            userName={profile.name || user.email || ""}
    91→          />
    92→
    93→
{profile.name || "Worker"}

    94→
    95→

    96→            {trades.map((t: string) => (
    97→              <Badge key={t} variant="secondary" className="text-sm">
    98→                <Wrench className="mr-1 h-3 w-3" />
    99→                {t}
   100→              </Badge>
   101→            ))}
   102→

   103→
   104→

   105→            {profile.suburb && (
   106→
   107→                <MapPin className="h-3.5 w-3.5" />
   108→                {profile.suburb}
   109→
   110→            )}
   111→            {expLabel && (
   112→
   113→                <Clock className="h-3.5 w-3.5" />
   114→                {expLabel}
   115→
   116→            )}
   117→            {avgRating && (
   118→
   119→                ⭐ {avgRating} ({ratings!.length})
   120→
   121→            )}
   122→

   123→
   124→          {workerProfile.availability && (
   125→            <Badge variant="outline" className="mt-2">
   126→              <Calendar className="mr-1 h-3 w-3" />
   127→              {workerProfile.availability}
   128→            </Badge>
   129→          )}
   130→

   131→
   132→        {/* Bio */}
   133→        {workerProfile.bio && (
   134→          <Card className="mt-6">
   135→            <CardContent className="pt-5">
   136→

   137→                <Sparkles className="h-4 w-4" />
   138→                About me
   139→

   140→
{workerProfile.bio}

   141→            </CardContent>
   142→          </Card>
   143→        )}
   144→
   145→        {/* Skills */}
   146→        {skills.length > 0 && (
   147→          <Card className="mt-3">
   148→            <CardContent className="pt-5">
   149→

   150→                <Zap className="h-4 w-4" />
   151→                Skills
   152→

   153→

   154→                {skills.map((skill: string) => (
   155→                  <Badge key={skill} className="text-sm">
   156→                    {skill}
   157→                  </Badge>
   158→                ))}
   159→

   160→            </CardContent>
   161→          </Card>
   162→        )}
   163→
   164→        {/* Certifications */}
   165→        {certs.length > 0 && (
   166→          <Card className="mt-3">
   167→            <CardContent className="pt-5">
   168→

   169→                <Shield className="h-4 w-4" />
   170→                Certifications
   171→

   172→

   173→                {certs.map((cert: string) => (
   174→

   178→                    <Shield className="h-4 w-4 text-primary" />
   179→                    {cert}
   180→

   181→                ))}
   182→

   183→            </CardContent>
   184→          </Card>
   185→        )}
   186→
   187→        {/* Ratings */}
   188→        {(ratings && ratings.length > 0) && (
   189→

   190→            <RatingsSummary
   191→              averageRating={parseFloat(avgRating || "0")}
   192→              totalRatings={totalRatings}
   193→            />
   194→
   195→

   196→              <RatingsList
   197→                ratings={ratings}
   198→                title="Recent Reviews"
   199→              />
   200→
   201→              {totalRatings > 3 && (
   202→                <Button asChild variant="outline" className="w-full mt-4">
   203→                  <Link href={`/ratings/${user.id}`}>
   204→                    <Star className="h-4 w-4 mr-2" />
   205→                    View All Reviews ({totalRatings})
   206→                  </Link>
   207→                </Button>
   208→              )}
   209→

   210→

   211→        )}
   212→
   213→        {/* Edit button */}
   214→

   215→          <Button asChild variant="outline" className="w-full h-12 text-base gap-2">
   216→            <Link href="/worker/profile/build">
   217→              <Edit3 className="h-5 w-5" />
   218→              Edit Profile
   219→            </Link>
   220→          </Button>
   221→

   222→
   223→
   224→      <BottomNavWrapper userType="worker" />
   225→

   226→  );
   227→}
   228→
   229→function getExperienceLabel(years: number | null): string | null {
   230→  if (years === null || years === undefined) return null;
   231→  if (years === 0) return "< 1 year";
   232→  if (years <= 3) return `${years} years`;
   233→  if (years <= 5) return "3-5 years";
   234→  if (years <= 10) return "5-10 years";
   235→  return "10+ years";
   236→}
   237→

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


Now let me read the hooks and components used in worker signup:


find /home/ccuser/the-50-dollar-app/src/app/\(authenticated\)/worker/signup -name "hooks" -o -name "utils" -type d


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/utils
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/signup/hooks


total 28
drwxrwxr-x  2 ccuser ccuser 4096 Feb  9 07:00 .
drwxrwxr-x 10 ccuser ccuser 4096 Feb  9 07:00 ..
-rw-r--r--  1 ccuser ccuser 4328 Feb  9 07:00 useVoiceProcessing.ts
-rw-r--r--  1 ccuser ccuser 1898 Feb  9 07:00 useWorkerAuth.ts
-rw-r--r--  1 ccuser ccuser 4244 Feb  9 07:00 useWorkerProfile.ts


     1→"use client";
     2→
     3→import { useState, useEffect } from "react";
     4→import { useRouter } from "next/navigation";
     5→import toast from "react-hot-toast";
     6→import { createClient } from "@/lib/supabase/client";
     7→
     8→export function useWorkerAuth() {
     9→  const [isAuthenticated, setIsAuthenticated] = useState(false);
    10→  const [loading, setLoading] = useState(true);
    11→  const [email, setEmail] = useState("");
    12→  const [password, setPassword] = useState("");
    13→  const [authError, setAuthError] = useState<string | null>(null);
    14→  const [authLoading, setAuthLoading] = useState(false);
    15→
    16→  const router = useRouter();
    17→  const supabase = createClient();
    18→
    19→  useEffect(() => {
    20→    async function checkAuth() {
    21→      const {
    22→        data: { user },
    23→      } = await supabase.auth.getUser();
    24→      if (user) {
    25→        setIsAuthenticated(true);
    26→      }
    27→      setLoading(false);
    28→    }
    29→    checkAuth();
    30→  }, [supabase.auth]);
    31→
    32→  async function handleAuth(e: React.FormEvent) {
    33→    e.preventDefault();
    34→    setAuthLoading(true);
    35→    setAuthError(null);
    36→
    37→    const { error } = await supabase.auth.signUp({
    38→      email,
    39→      password,
    40→      options: {
    41→        data: { type: "worker" },
    42→        emailRedirectTo: `${window.location.origin}/auth/callback`,
    43→      },
    44→    });
    45→
    46→    if (error) {
    47→      if (error.message.includes("already registered")) {
    48→        const { error: loginError } = await supabase.auth.signInWithPassword({
    49→          email,
    50→          password,
    51→        });
    52→        if (loginError) {
    53→          setAuthError(loginError.message);
    54→          setAuthLoading(false);
    55→          return;
    56→        }
    57→      } else {
    58→        setAuthError(error.message);
    59→        setAuthLoading(false);
    60→        return;
    61→      }
    62→    }
    63→
    64→    setIsAuthenticated(true);
    65→    setAuthLoading(false);
    66→  }
    67→
    68→  return {
    69→    isAuthenticated,
    70→    loading,
    71→    email,
    72→    setEmail,
    73→    password,
    74→    setPassword,
    75→    authError,
    76→    authLoading,
    77→    handleAuth,
    78→  };
    79→}

<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 toast from "react-hot-toast";
     6→import { createClient } from "@/lib/supabase/client";
     7→import { jsonToPostgresPoint } from "@/lib/location-utils";
     8→import { csrfFetch } from "@/lib/csrf-client";
     9→import { generateBio } from "../utils/bio";
    10→import type { WorkerData } from "../types";
    11→
    12→export function useWorkerProfile() {
    13→  const [selectedTrades, setSelectedTrades] = useState<string[]>([]);
    14→  const [experience, setExperience] = useState<number | null>(null);
    15→  const [suburb, setSuburb] = useState("");
    16→  const [availability, setAvailability] = useState<WorkerData["availability"] | null>(null);
    17→  const [certifications, setCertifications] = useState<string[]>([]);
    18→  const [saving, setSaving] = useState(false);
    19→
    20→  const router = useRouter();
    21→  const supabase = createClient();
    22→
    23→  function toggleTrade(trade: string) {
    24→    setSelectedTrades((prev) =>
    25→      prev.includes(trade) ? prev.filter((t) => t !== trade) : [...prev, trade]
    26→    );
    27→  }
    28→
    29→  function toggleCert(cert: string) {
    30→    setCertifications((prev) =>
    31→      prev.includes(cert) ? prev.filter((c) => c !== cert) : [...prev, cert]
    32→    );
    33→  }
    34→
    35→  async function saveWorkerProfile() {
    36→    if (!availability || experience === null) return;
    37→    setSaving(true);
    38→
    39→    try {
    40→      const {
    41→        data: { user },
    42→      } = await supabase.auth.getUser();
    43→      if (!user) {
    44→        toast.error("Not logged in. Please sign in again.");
    45→        setSaving(false);
    46→        return;
    47→      }
    48→
    49→      const workerData: WorkerData = {
    50→        trades: selectedTrades,
    51→        experience,
    52→        suburb,
    53→        availability,
    54→        certifications,
    55→      };
    56→
    57→      const bio = generateBio(workerData);
    58→
    59→      // Geocode the suburb to get lat/lng coordinates
    60→      let location: string | null = null;
    61→      if (suburb) {
    62→        try {
    63→          const geocodeRes = await csrfFetch("/api/geocode", {
    64→            method: "POST",
    65→            headers: { "Content-Type": "application/json" },
    66→            body: JSON.stringify({ suburb }),
    67→          });
    68→
    69→          if (geocodeRes.ok) {
    70→            const geocodeData = await geocodeRes.json();
    71→            if (geocodeData.location) {
    72→              location = jsonToPostgresPoint(geocodeData.location);
    73→            }
    74→          }
    75→        } catch (geoError) {
    76→          // Continue without location - suburb text will still be saved
    77→        }
    78→      }
    79→
    80→      const profileResult = await supabase.from("profiles").update({
    81→        name: user.email?.split("@")[0] ?? "",
    82→        suburb,
    83→        location,
    84→        onboarding_completed: true,
    85→      }).eq("id", user.id);
    86→
    87→      if (profileResult.error) {
    88→        console.error("Profile update failed:", profileResult.error);
    89→        toast.error(`Profile update failed: ${profileResult.error.message}`);
    90→        setSaving(false);
    91→        return;
    92→      }
    93→
    94→      const workerResult = await supabase.from("worker_profiles").insert({
    95→        profile_id: user.id,
    96→        trades: selectedTrades,
    97→        experience_years: experience,
    98→        availability,
    99→        certifications,
   100→        bio,
   101→        ai_extracted: {
   102→          source: "typed_answers",
   103→          created_at: new Date().toISOString(),
   104→        },
   105→      }).select();
   106→
   107→      if (workerResult.error) {
   108→        console.error("Worker profile insert failed:", workerResult.error);
   109→        toast.error(`Worker profile creation failed: ${workerResult.error.message}`);
   110→        setSaving(false);
   111→        return;
   112→      }
   113→
   114→      router.push("/dashboard");
   115→      router.refresh();
   116→    } catch (error: any) {
   117→      console.error("Signup error:", error);
   118→      toast.error(`Something went wrong: ${error.message || "Unknown error"}`);
   119→      setSaving(false);
   120→    }
   121→  }
   122→
   123→  const workerData: WorkerData | null =
   124→    selectedTrades.length > 0 && experience !== null && suburb && availability
   125→      ? { trades: selectedTrades, experience, suburb, availability, certifications }
   126→      : null;
   127→
   128→  return {
   129→    selectedTrades,
   130→    setSelectedTrades,
   131→    experience,
   132→    setExperience,
   133→    suburb,
   134→    setSuburb,
   135→    availability,
   136→    setAvailability,
   137→    certifications,
   138→    setCertifications,
   139→    saving,
   140→    toggleTrade,
   141→    toggleCert,
   142→    saveWorkerProfile,
   143→    workerData,
   144→  };
   145→}

<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 toast from "react-hot-toast";
     5→import { csrfFetch, addCSRFTokenToFormData } from "@/lib/csrf-client";
     6→import { TRADES } from "@/lib/constants";
     7→import type { Step, WorkerData } from "../types";
     8→import { EXPERIENCE_OPTIONS } from "../types";
     9→
    10→interface VoiceProcessingState {
    11→  processing: boolean;
    12→  status: string;
    13→  error: string | null;
    14→  transcript: string | null;
    15→}
    16→
    17→export function useVoiceProcessing(
    18→  setSelectedTrades: (trades: string[]) => void,
    19→  setExperience: (exp: number) => void,
    20→  setSuburb: (suburb: string) => void,
    21→  setAvailability: (availability: WorkerData["availability"]) => void,
    22→  setStep: (step: Step) => void
    23→) {
    24→  const [voiceState, setVoiceState] = useState<VoiceProcessingState>({
    25→    processing: false,
    26→    status: "",
    27→    error: null,
    28→    transcript: null,
    29→  });
    30→
    31→  async function handleVoiceComplete(blob: Blob) {
    32→    setVoiceState({
    33→      processing: true,
    34→      status: "Transcribing your voice...",
    35→      error: null,
    36→      transcript: null,
    37→    });
    38→
    39→    try {
    40→      // Step 1: Transcribe (FormData with CSRF token)
    41→      const formData = new FormData();
    42→      formData.append("audio", blob, "recording.webm");
    43→      addCSRFTokenToFormData(formData);
    44→
    45→      const transcribeRes = await fetch("/api/ai/transcribe-voice", {
    46→        method: "POST",
    47→        body: formData,
    48→      });
    49→
    50→      if (!transcribeRes.ok) {
    51→        if (transcribeRes.status === 429) {
    52→          throw new Error("Too many requests. Please wait a few minutes.");
    53→        }
    54→        const errData = await transcribeRes.json().catch(() => ({}));
    55→        throw new Error(errData.error || "Failed to transcribe audio");
    56→      }
    57→
    58→      const { text } = await transcribeRes.json();
    59→      if (!text) throw new Error("No speech detected. Please try again.");
    60→
    61→      // Store the raw transcript for the bio generator
    62→      setVoiceState(prev => ({ ...prev, transcript: text, status: "Understanding your details..." }));
    63→
    64→      // Step 2: Parse profile fields (JSON with CSRF header)
    65→      const parseRes = await csrfFetch("/api/ai/parse-profile-voice", {
    66→        method: "POST",
    67→        headers: { "Content-Type": "application/json" },
    68→        body: JSON.stringify({ transcript: text }),
    69→      });
    70→
    71→      if (!parseRes.ok) {
    72→        const errData = await parseRes.json().catch(() => ({}));
    73→        throw new Error(errData.error || "Failed to parse your details");
    74→      }
    75→
    76→      const parsed = await parseRes.json();
    77→
    78→      // Pre-fill fields — handle multiple trades
    79→      if (parsed.trades && parsed.trades.length > 0) {
    80→        setSelectedTrades(parsed.trades);
    81→      } else if (parsed.trade) {
    82→        setSelectedTrades([parsed.trade]);
    83→      }
    84→      if (parsed.experience_years !== null && parsed.experience_years !== undefined) {
    85→        // Find closest experience option
    86→        const closest = EXPERIENCE_OPTIONS.reduce((prev, curr) =>
    87→          Math.abs(curr.value - parsed.experience_years) < Math.abs(prev.value - parsed.experience_years) ? curr : prev
    88→        );
    89→        setExperience(closest.value);
    90→      }
    91→      if (parsed.location) setSuburb(parsed.location);
    92→      if (parsed.availability) setAvailability(parsed.availability);
    93→
    94→      // Check what we got and move to the right step
    95→      setVoiceState(prev => ({ ...prev, status: "" }));
    96→
    97→      const hasTrade = parsed.trades && parsed.trades.length > 0;
    98→      const hasExp = parsed.experience_years !== null && parsed.experience_years !== undefined;
    99→      const hasLoc = !!parsed.location;
   100→      const hasAvail = !!parsed.availability;
   101→
   102→      if (hasTrade && hasExp && hasLoc && hasAvail) {
   103→        // All fields filled — go straight to review
   104→        setStep("review");
   105→      } else if (!hasTrade) {
   106→        setStep("trade");
   107→      } else if (!hasExp) {
   108→        setStep("experience");
   109→      } else if (!hasLoc) {
   110→        setStep("location");
   111→      } else {
   112→        setStep("availability");
   113→      }
   114→    } catch (error) {
   115→      console.error("Voice processing error:", error);
   116→      setVoiceState(prev => ({
   117→        ...prev,
   118→        error: error instanceof Error ? error.message : "Something went wrong. Please try again.",
   119→        status: "",
   120→      }));
   121→    } finally {
   122→      setVoiceState(prev => ({ ...prev, processing: false }));
   123→    }
   124→  }
   125→
   126→  return {
   127→    voiceState,
   128→    handleVoiceComplete,
   129→  };
   130→}

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


Now let me examine the worker profile build page and some key components:


1→import { Wrench, ArrowLeft, ArrowRight } from "lucide-react";
     2→import { Button } from "@/components/ui/button";
     3→import { StepHeader } from "./StepHeader";
     4→import { TRADES } from "@/lib/constants";
     5→
     6→interface TradeStepProps {
     7→  selectedTrades: string[];
     8→  onTradeToggle: (trade: string) => void;
     9→  onContinue: () => void;
    10→  onBack: () => void;
    11→}
    12→
    13→export function TradeStep({
    14→  selectedTrades,
    15→  onTradeToggle,
    16→  onContinue,
    17→  onBack
    18→}: TradeStepProps) {
    19→  return (
    20→

    21→      <StepHeader
    22→        icon={<Wrench className="h-5 w-5" />}
    23→        title="What's your trade?"
    24→        subtitle="Select one or more"
    25→      />
    26→
    27→

    28→        {TRADES.map((trade) => (
    29→          <button
    30→            key={trade}
    31→            onClick={() => onTradeToggle(trade)}
    32→            className={`rounded-xl border-2 px-4 py-3 text-left text-sm font-medium transition-colors ${
    33→              selectedTrades.includes(trade)
    34→                ? "border-primary bg-primary/10 text-primary"
    35→                : "border-border hover:border-primary/50"
    36→            }`}
    37→          >
    38→            {trade}
    39→          </button>
    40→        ))}
    41→

    42→
    43→

    44→        <Button
    45→          variant="outline"
    46→          onClick={onBack}
    47→          className="h-12"
    48→        >
    49→          <ArrowLeft className="h-5 w-5" />
    50→        </Button>
    51→        <Button
    52→          onClick={onContinue}
    53→          disabled={selectedTrades.length === 0}
    54→          className="h-12 flex-1 gap-2 text-base font-semibold"
    55→        >
    56→          Continue
    57→          <ArrowRight className="h-5 w-5" />
    58→        </Button>
    59→

    60→

    61→  );
    62→}

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