Fix the following UI/UX bugs in /home/ccuser/the-50-dollar-app. MAKE ALL EDITS - do not just report, actually edit the files.

1. **Fix "View All Reviews" never showing** in TWO files:
   - `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/page.tsx`: The page fetches ratings with `.limit(3)` then checks `totalRatings > 3` which is always false since `totalRatings = ratings?.length || 0` (max 3). Fix by adding a separate count query using supabase `.select('id', { count: 'exact', head: true })` to get the real total count, and use that for the `> 3` comparison.
   - `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/profile/page.tsx`: Same issue - fix the same way.

2. **Fix handleReject missing error handling** in `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx`:
   - Find the `handleReject` function (around line 485-501)
   - Wrap the supabase update in try/catch
   - Only remove match from UI on success
   - Show toast error on failure

3. **Fix hiring state never set** in `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx`:
   - The `hiring` state (`const [hiring, setHiring] = useState<string | null>(null)`) is never set
   - In the `handleHire` function, add `setHiring(match.id)` at the start and clear it after the payment modal opens

4. **Add bottom nav to notifications** in `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/notifications/page.tsx`:
   - Add the BottomNav component to the page (check how other pages like dashboard use it)

5. **Fix `<a>` tags to `<Link>`** in `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/rate/[matchId]/page.tsx`:
   - Replace `<a href="/dashboard">` with Next.js `<Link href="/dashboard">`
   - Add `import Link from "next/link"` if not already imported

6. **Fix post-job validation** in `/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/post-job/page.tsx`:
   - Add validation that rateMin <= rateMax before submit
   - Add validation that rateMin > 0 and rateMax > 0
   - Add description to the required fields check on submit
   - Add maxLength={5000} to the description textarea

Read each file first before editing.


I'll start by reading all the files that need to be edited.


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/edit">
   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, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { Badge } from "@/components/ui/badge";
     7→import {
     8→  Building2,
     9→  MapPin,
    10→  Globe,
    11→  Users,
    12→  Edit3,
    13→  Star,
    14→  ArrowLeft,
    15→} from "lucide-react";
    16→import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
    17→import { AvatarUpload } from "@/components/avatar-upload";
    18→import { RatingsSummary } from "@/components/ratings-summary";
    19→import { RatingsList } from "@/components/ratings-list";
    20→
    21→export default async function CompanyProfilePage() {
    22→  const supabase = await createClient();
    23→
    24→  const {
    25→    data: { user },
    26→  } = await supabase.auth.getUser();
    27→
    28→  if (!user) {
    29→    redirect("/auth/login");
    30→  }
    31→
    32→  // Fetch company profile
    33→  const { data: company } = await supabase
    34→    .from("companies")
    35→    .select("*")
    36→    .eq("profile_id", user.id)
    37→    .single();
    38→
    39→  if (!company) {
    40→    redirect("/contractor/signup");
    41→  }
    42→
    43→  // Fetch ratings with profile information
    44→  const { data: ratings } = await supabase
    45→    .from("ratings")
    46→    .select(`
    47→      *,
    48→      from_profile:profiles!ratings_from_id_fkey(
    49→        id,
    50→        name,
    51→        avatar_url
    52→      )
    53→    `)
    54→    .eq("to_id", user.id)
    55→    .order("created_at", { ascending: false })
    56→    .limit(3); // Show latest 3 ratings
    57→
    58→  const avgRating = ratings && ratings.length > 0
    59→    ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    60→    : 0;
    61→
    62→  const totalRatings = ratings?.length || 0;
    63→
    64→  return (
    65→

    66→
    67→

    68→          <Button asChild variant="ghost" size="icon">
    69→            <Link href="/dashboard">
    70→              <ArrowLeft className="h-5 w-5" />
    71→            </Link>
    72→          </Button>
    73→
Company Profile

    74→

    75→
    76→        {/* Company header */}
    77→

    78→          <AvatarUpload
    79→            currentUrl={company.logo_url}
    80→            userName={company.name}
    81→          />
    82→
    83→
{company.name}

    84→
    85→

    86→            {company.address && (
    87→
    88→                <MapPin className="h-3.5 w-3.5" />
    89→                {company.address}
    90→
    91→            )}
    92→            {company.industry && (
    93→
    94→                <Building2 className="h-3.5 w-3.5" />
    95→                {company.industry}
    96→
    97→            )}
    98→            {avgRating > 0 && (
    99→
   100→                ⭐ {avgRating.toFixed(1)} ({totalRatings})
   101→
   102→            )}
   103→

   104→
   105→          {company.size && (
   106→            <Badge variant="outline" className="mt-2">
   107→              <Users className="mr-1 h-3 w-3" />
   108→              {company.size}
   109→            </Badge>
   110→          )}
   111→

   112→
   113→        {/* Company info */}
   114→

   115→          {company.abn && (
   116→            <Card>
   117→              <CardContent className="pt-5">
   118→

   119→                  <Building2 className="h-4 w-4" />
   120→                  ABN
   121→

   122→
{company.abn}

   123→              </CardContent>
   124→            </Card>
   125→          )}
   126→
   127→          {company.website && (
   128→            <Card>
   129→              <CardContent className="pt-5">
   130→

   131→                  <Globe className="h-4 w-4" />
   132→                  Website
   133→

   134→
   140→                  {company.website}
   141→
   142→              </CardContent>
   143→            </Card>
   144→          )}
   145→
   146→          {company.ai_research && (
   147→            <Card>
   148→              <CardContent className="pt-5">
   149→

   150→                  <Building2 className="h-4 w-4" />
   151→                  About
   152→

   153→

   154→                  {(company.ai_research as { description?: string })?.description ||
   155→                   (company.ai_research as { summary?: string })?.summary ||
   156→                   "Company information available."}
   157→

   158→              </CardContent>
   159→            </Card>
   160→          )}
   161→

   162→
   163→        {/* Ratings */}
   164→        {(ratings && ratings.length > 0) && (
   165→

   166→            <RatingsSummary
   167→              averageRating={avgRating}
   168→              totalRatings={totalRatings}
   169→            />
   170→
   171→

   172→              <RatingsList
   173→                ratings={ratings}
   174→                title="Recent Reviews"
   175→              />
   176→
   177→              {totalRatings > 3 && (
   178→                <Button asChild variant="outline" className="w-full mt-4">
   179→                  <Link href={`/ratings/${user.id}`}>
   180→                    <Star className="h-4 w-4 mr-2" />
   181→                    View All Reviews ({totalRatings})
   182→                  </Link>
   183→                </Button>
   184→              )}
   185→

   186→

   187→        )}
   188→
   189→        {/* Edit button */}
   190→

   191→          <Button asChild variant="outline" className="w-full h-12 text-base gap-2">
   192→            <Link href="/contractor/signup">
   193→              <Edit3 className="h-5 w-5" />
   194→              Edit Profile
   195→            </Link>
   196→          </Button>
   197→

   198→
   199→
   200→      <BottomNavWrapper userType="contractor" />
   201→

   202→  );
   203→}

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


Now I need to check how BottomNav is used in other pages to understand the pattern for the notifications page.


No matches found


/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx:11:import { BottomNav } from "@/components/bottom-nav";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/profile/page.tsx:16:import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/jobs/page.tsx:10:import { BottomNav } from "@/components/bottom-nav";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/page.tsx:20:import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/page.tsx:10:import { BottomNav } from "@/components/bottom-nav";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/dashboard/page.tsx:9:import { BottomNavWrapper } from "@/components/bottom-nav-wrapper";


Let me check the BottomNavWrapper to understand the pattern, since notifications is a shared page (not worker or contractor specific).


     1→"use client";
     2→
     3→import { BottomNav } from "@/components/bottom-nav";
     4→
     5→interface BottomNavWrapperProps {
     6→  userType: "contractor" | "worker";
     7→}
     8→
     9→export function BottomNavWrapper({ userType }: BottomNavWrapperProps) {
    10→  return <BottomNav userType={userType} />;
    11→}
    12→

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


Let me check how the dashboard page uses the BottomNavWrapper, since it too is a shared page.


     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

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


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 {
--
255-            </>
256-          )}
257-        </div>
258-      </main>
259-
260:      <BottomNavWrapper userType={profile?.type || "worker"} />
261-    </div>
262-  );
263-}


The notifications page is a client component, so it needs to know the user type. Let me check how the BottomNav component works to see if there's a way to handle this dynamically.


1→"use client";
     2→
     3→import Link from "next/link";
     4→import { usePathname } from "next/navigation";
     5→import { Home, HardHat, Building2, Plus, MessageSquare, User } from "lucide-react";
     6→import { cn } from "@/lib/utils";
     7→
     8→interface BottomNavProps {
     9→  userType: "contractor" | "worker";
    10→}
    11→
    12→export function BottomNav({ userType }: BottomNavProps) {
    13→  const pathname = usePathname();
    14→
    15→  const isContractor = userType === "contractor";
    16→
    17→  const links = [
    18→    {
    19→      href: "/dashboard",
    20→      label: "Home",
    21→      icon: Home,
    22→    },
    23→    {
    24→      href: "/contractor/matches",
    25→      label: "Matches",
    26→      icon: Building2,
    27→    },
    28→    {
    29→      href: "/contractor/post-job",
    30→      label: "Post",
    31→      icon: Plus,
    32→      isCenter: true,
    33→    },
    34→    {
    35→      href: "/messages",
    36→      label: "Messages",
    37→      icon: MessageSquare,
    38→    },
    39→    {
    40→      href: isContractor ? "/contractor/profile" : "/worker/profile",
    41→      label: "Profile",
    42→      icon: User,
    43→    },
    44→  ];
    45→
    46→  // Workers don't get the center "Post" button — replace with different layout
    47→  const workerLinks = [
    48→    {
    49→      href: "/dashboard",
    50→      label: "Home",
    51→      icon: Home,
    52→    },
    53→    {
    54→      href: "/worker/jobs",
    55→      label: "Jobs",
    56→      icon: HardHat,
    57→    },
    58→    {
    59→      href: "/messages",
    60→      label: "Messages",
    61→      icon: MessageSquare,
    62→    },
    63→    {
    64→      href: "/worker/profile",
    65→      label: "Profile",
    66→      icon: User,
    67→    },
    68→  ];
    69→
    70→  const navLinks = isContractor ? links : workerLinks;
    71→
    72→  return (
    73→
    74→

    75→        {navLinks.map((link) => {
    76→          const isActive =
    77→            pathname === link.href ||
    78→            (link.href !== "/dashboard" && pathname.startsWith(link.href));
    79→          const Icon = link.icon;
    80→
    81→          if ("isCenter" in link && link.isCenter) {
    82→            return (
    83→              <Link
    84→                key={link.href}
    85→                href={link.href}
    86→                className="flex -mt-4 items-center justify-center"
    87→              >
    88→

    89→                  <Icon className="h-7 w-7 text-primary-foreground" />
    90→

    91→              </Link>
    92→            );
    93→          }
    94→
    95→          return (
    96→            <Link
    97→              key={link.href + link.label}
    98→              href={link.href}
    99→              className={cn(
   100→                "flex min-w-[4rem] flex-col items-center gap-0.5 py-2 text-xs transition-colors active:scale-95",
   101→                isActive
   102→                  ? "text-primary font-medium"
   103→                  : "text-muted-foreground"
   104→              )}
   105→            >
   106→              <Icon className="h-5 w-5" />
   107→              {link.label}
   108→            </Link>
   109→          );
   110→        })}
   111→

   112→
   113→  );
   114→}
   115→

<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 I have all the context I need. The notifications page is a client component, so it needs to determine the user type dynamically. Let me check if there's an existing hook or pattern for getting user type in client components.


/home/ccuser/the-50-dollar-app/src/components/bottom-nav-wrapper.tsx:6:  userType: "contractor" | "worker";
/home/ccuser/the-50-dollar-app/src/components/bottom-nav-wrapper.tsx:9:export function BottomNavWrapper({ userType }: BottomNavWrapperProps) {
/home/ccuser/the-50-dollar-app/src/components/bottom-nav-wrapper.tsx:10:  return <BottomNav userType={userType} />;
/home/ccuser/the-50-dollar-app/src/components/bottom-nav.tsx:9:  userType: "contractor" | "worker";
/home/ccuser/the-50-dollar-app/src/components/bottom-nav.tsx:12:export function BottomNav({ userType }: BottomNavProps) {
/home/ccuser/the-50-dollar-app/src/components/bottom-nav.tsx:15:  const isContractor = userType === "contractor";
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx:666:      <BottomNav userType="contractor" />
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/profile/page.tsx:200:      <BottomNavWrapper userType="contractor" />
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/dashboard/page.tsx:260:      <BottomNavWrapper userType={profile?.type || "worker"} />
/home/ccuser/the-50-dollar-app/src/app/(authenticated)/messages/page.tsx:29:  const [userType, setUserType] = useState<"contractor" | "worker">("worker");

[Showing results with pagination = limit: 10, offset: 0]


Let me see how messages page handles this.


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

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


    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 || []);

<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 I have all the patterns I need. Let me make all the edits. I'll do them in parallel where possible.

**Fix 1a: Worker profile - "View All Reviews" count query**


Edit

  // Fetch ratings with profile information
  const { data: ratings } = await supabase
    .from("ratings")
    .select(`
      *,
      from_profile:profiles!ratings_from_id_fkey(
        id,
        name,
        avatar_url
      )
    `)
    .eq("to_id", user.id)
    .order("created_at", { ascending: false })
    .limit(3); // Show latest 3 ratings

  const avgRating = ratings && ratings.length > 0
    ? (ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length).toFixed(1)
    : null;

  const totalRatings = ratings?.length || 0;

  // Get the real total count of ratings
  const { count: totalRatings } = await supabase
    .from("ratings")
    .select("id", { count: "exact", head: true })
    .eq("to_id", user.id);

  // Fetch latest 3 ratings with profile information
  const { data: ratings } = await supabase
    .from("ratings")
    .select(`
      *,
      from_profile:profiles!ratings_from_id_fkey(
        id,
        name,
        avatar_url
      )
    `)
    .eq("to_id", user.id)
    .order("created_at", { ascending: false })
    .limit(3);

  const avgRating = ratings && ratings.length > 0
    ? (ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length).toFixed(1)
    : null;

  const totalRatingsCount = totalRatings || 0;


The file /home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/profile/page.tsx has been updated successfully.


Now update the references from `totalRatings` to `totalRatingsCount` in the worker profile page.


            <RatingsSummary
              averageRating={parseFloat(avgRating || "0")}
              totalRatings={totalRatings}
            />

            <RatingsSummary
              averageRating={parseFloat(avgRating || "0")}
              totalRatings={totalRatingsCount}
            />


              {totalRatings > 3 && (
                <Button asChild variant="outline" className="w-full mt-4">
                  <Link href={`/ratings/${user.id}`}>
                    <Star className="h-4 w-4 mr-2" />
                    View All Reviews ({totalRatings})
                  </Link>
                </Button>
              )}

              {totalRatingsCount > 3 && (
                <Button asChild variant="outline" className="w-full mt-4">
                  <Link href={`/ratings/${user.id}`}>
                    <Star className="h-4 w-4 mr-2" />
                    View All Reviews ({totalRatingsCount})
                  </Link>
                </Button>
              )}


**Fix 1b: Contractor profile - "View All Reviews" count query**


  // Fetch ratings with profile information
  const { data: ratings } = await supabase
    .from("ratings")
    .select(`
      *,
      from_profile:profiles!ratings_from_id_fkey(
        id,
        name,
        avatar_url
      )
    `)
    .eq("to_id", user.id)
    .order("created_at", { ascending: false })
    .limit(3); // Show latest 3 ratings

  const avgRating = ratings && ratings.length > 0
    ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    : 0;

  const totalRatings = ratings?.length || 0;

  // Get the real total count of ratings
  const { count: totalRatings } = await supabase
    .from("ratings")
    .select("id", { count: "exact", head: true })
    .eq("to_id", user.id);

  // Fetch latest 3 ratings with profile information
  const { data: ratings } = await supabase
    .from("ratings")
    .select(`
      *,
      from_profile:profiles!ratings_from_id_fkey(
        id,
        name,
        avatar_url
      )
    `)
    .eq("to_id", user.id)
    .order("created_at", { ascending: false })
    .limit(3);

  const avgRating = ratings && ratings.length > 0
    ? ratings.reduce((sum, r) => sum + r.score, 0) / ratings.length
    : 0;

  const totalRatingsCount = totalRatings || 0;


The file /home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/profile/page.tsx has been updated successfully.


            <RatingsSummary
              averageRating={avgRating}
              totalRatings={totalRatings}
            />

            <RatingsSummary
              averageRating={avgRating}
              totalRatings={totalRatingsCount}
            />


              {totalRatings > 3 && (
                <Button asChild variant="outline" className="w-full mt-4">
                  <Link href={`/ratings/${user.id}`}>
                    <Star className="h-4 w-4 mr-2" />
                    View All Reviews ({totalRatings})
                  </Link>
                </Button>
              )}

              {totalRatingsCount > 3 && (
                <Button asChild variant="outline" className="w-full mt-4">
                  <Link href={`/ratings/${user.id}`}>
                    <Star className="h-4 w-4 mr-2" />
                    View All Reviews ({totalRatingsCount})
                  </Link>
                </Button>
              )}


Also need to fix the header display of total ratings count in both files.


44:  const { count: totalRatings } = await supabase
68:  const totalRatingsCount = totalRatings || 0;
106:                ⭐ {avgRating.toFixed(1)} ({totalRatings})
174:              totalRatings={totalRatingsCount}
183:              {totalRatingsCount > 3 && (
187:                    View All Reviews ({totalRatingsCount})


63:  const { count: totalRatings } = await supabase
87:  const totalRatingsCount = totalRatings || 0;
198:              totalRatings={totalRatingsCount}
207:              {totalRatingsCount > 3 && (
211:                    View All Reviews ({totalRatingsCount})


The contractor profile still has `totalRatings` on line 106 (the header display). Let me fix that.


**Fix 2: handleReject missing error handling in contractor matches**


  async function handleReject(match: MatchWithWorker) {
    // Haptic feedback
    if (navigator.vibrate) {
      navigator.vibrate([30, 20, 30]);
    }

    // Remove from list (soft delete - just update status)
    await supabase
      .from("matches")
      .update({
        contractor_action: "rejected",
      })
      .eq("id", match.id);

    // Remove from UI
    setMatches((prev) => prev.filter((m) => m.id !== match.id));
  }

  async function handleReject(match: MatchWithWorker) {
    // Haptic feedback
    if (navigator.vibrate) {
      navigator.vibrate([30, 20, 30]);
    }

    try {
      // Remove from list (soft delete - just update status)
      const { error } = await supabase
        .from("matches")
        .update({
          contractor_action: "rejected",
        })
        .eq("id", match.id);

      if (error) {
        toast.error("Failed to reject match. Please try again.");
        return;
      }

      // Remove from UI only on success
      setMatches((prev) => prev.filter((m) => m.id !== match.id));
    } catch {
      toast.error("Something went wrong. Please try again.");
    }
  }


The file /home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx has been updated successfully.


**Fix 3: hiring state never set in handleHire**