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

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

   229→
What trade do you need?

   230→

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

   233→

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

   248→

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

   254→            {aiLoading ? (
   255→

   256→

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

   259→
AI writing your job post...

   260→

   261→                  Based on market rates and common requirements
   262→

   263→

   264→            ) : (
   265→

   266→

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

   269→

   270→

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

   273→
   274→

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

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

   285→
   286→

   287→                    Description
   288→                    <textarea
   289→                      id="description"
   290→                      value={description}
   291→                      onChange={(e) => setDescription(e.target.value)}
   292→                      rows={5}
   293→                      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"
   294→                    />
   295→

   296→
   297→

   298→                    Pay Rate (AUD)
   299→

   300→

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

   310→                      to
   311→

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

   321→

   322→

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

   340→

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

   344→

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

   348→
   349→

   350→

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

   360→
   361→

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

   371→

   372→

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

   376→

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

   380→
   381→

   382→

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

   392→
   393→

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

   403→
   404→

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

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

   434→

   435→

   436→

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

   441→                      Required Certifications
   442→

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

   455→

   456→                  )}
   457→

   458→

   459→            )}
   460→

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

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

   470→
Job Posted!

   471→

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

   475→

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

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

   498→

   499→            {error && (
   500→
{error}

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

   520→              Free to post. You only pay $50 when you hire someone.
   521→

   522→

   523→

   524→      )}
   525→

   526→  );
   527→}
   528→

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

   104→      {/* Swipe indicator backgrounds */}
   105→      {!isHired && swipeX !== 0 && (
   106→        <>
   107→          {/* Right swipe (hire) */}
   108→          {swipeX > 0 && (
   109→

   116→              <UserCheck className="h-8 w-8 text-green-600" />
   117→
   118→                {swipeProgress >= 1 ? "Release to hire" : "Swipe to hire"}
   119→
   120→

   121→          )}
   122→          {/* Left swipe (reject) */}
   123→          {swipeX < 0 && (
   124→

   131→
   132→                {swipeProgress >= 1 ? "Release to pass" : "Swipe to pass"}
   133→
   134→              <AlertCircle className="h-8 w-8 text-red-600" />
   135→

   136→          )}
   137→        </>
   138→      )}
   139→
   140→      <Card
   141→        className={`transition-all touch-pan-y ${justHired ? "ring-2 ring-green-500" : ""}`}
   142→        style={{
   143→          transform: `translateX(${swipeX}px)`,
   144→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   145→        }}
   146→        onTouchStart={handleTouchStart}
   147→        onTouchMove={handleTouchMove}
   148→        onTouchEnd={handleTouchEnd}
   149→      >
   150→        <CardContent className="p-5">
   151→

   152→            {/* Avatar */}
   153→

   154→              {match.worker?.name?.[0]?.toUpperCase() || "?"}
   155→

   156→
   157→

   158→

   159→

   160→                  {match.worker?.name || "Worker"}
   161→

   162→                {isHired && (
   163→                  <Badge className="bg-green-600 text-xs shrink-0">Hired</Badge>
   164→                )}
   165→

   166→
   167→              {/* AI Summary */}
   168→

   169→                {summary}
   170→

   171→
   172→              {/* Details */}
   173→

   174→                {match.worker_profile?.experience_years && (
   175→
   176→                    <Clock className="h-4 w-4" />
   177→                    {match.worker_profile.experience_years}yr exp
   178→
   179→                )}
   180→                {match.worker?.suburb && (
   181→
   182→                    <MapPin className="h-4 w-4" />
   183→                    {match.worker.suburb}
   184→
   185→                )}
   186→                {match.match_score && (
   187→
   188→                    <Star className="h-4 w-4 fill-amber-400 text-amber-400" />
   189→                    {Math.round(match.match_score)}% match
   190→
   191→                )}
   192→

   193→
   194→              {/* Certs */}
   195→              {match.worker_profile?.certifications && match.worker_profile.certifications.length > 0 && (
   196→

   197→                  {match.worker_profile.certifications.slice(0, 3).map((cert) => (
   198→                    <Badge key={cert} variant="outline" className="text-xs px-2 py-1">
   199→                      {cert}
   200→                    </Badge>
   201→                  ))}
   202→

   203→              )}
   204→

   205→

   206→
   207→          {/* Action */}
   208→          {!isHired && (
   209→            <Button
   210→              onClick={() => onHire(match)}
   211→              className="mt-4 w-full h-14 text-base font-semibold"
   212→              disabled={hiring}
   213→            >
   214→              {hiring ? (
   215→                <>
   216→                  <Loader2 className="mr-2 h-5 w-5 animate-spin" />
   217→                  Processing...
   218→                </>
   219→              ) : (
   220→                <>
   221→                  <UserCheck className="mr-2 h-5 w-5" />
   222→                  Hire — $50 Fee
   223→                </>
   224→              )}
   225→            </Button>
   226→          )}
   227→
   228→          {justHired && (
   229→

   230→              <CheckCircle2 className="h-5 w-5" />
   231→              Hired! Worker has been notified.
   232→

   233→          )}
   234→
   235→          {isHired && !justHired && (
   236→

   237→              <CheckCircle2 className="h-5 w-5 text-green-600" />
   238→              Hired on{" "}
   239→              {match.hired_at
   240→                ? new Date(match.hired_at).toLocaleDateString("en-AU", {
   241→                    day: "numeric",
   242→                    month: "short",
   243→                  })
   244→                : "recently"}
   245→

   246→          )}
   247→        </CardContent>
   248→      </Card>
   249→

   250→  );
   251→}
   252→
   253→const TRADE_LABEL: Record<string, string> = {
   254→  "steel-fixer": "Steel Fixer",
   255→  "formworker": "Formworker",
   256→  "labourer": "Labourer",
   257→  "concreter": "Concreter",
   258→  "carpenter": "Carpenter",
   259→  "scaffolder": "Scaffolder",
   260→  "other": "Other",
   261→};
   262→
   263→function generateAiSummary(
   264→  worker: MatchWithWorker["worker"],
   265→  profile: MatchWithWorker["worker_profile"]
   266→): string {
   267→  const parts: string[] = [];
   268→  if (profile?.trades?.length) {
   269→    parts.push(profile.trades.map((t) => TRADE_LABEL[t] || t).join(", "));
   270→  }
   271→  if (profile?.experience_years) {
   272→    parts.push(`${profile.experience_years} years exp`);
   273→  }
   274→  if (worker?.suburb) {
   275→    parts.push(worker.suburb);
   276→  }
   277→  if (profile?.availability) {
   278→    parts.push(profile.availability);
   279→  }
   280→  if (profile?.certifications?.length) {
   281→    parts.push(profile.certifications.slice(0, 2).join(", "));
   282→  }
   283→  return parts.join(" \u00B7 ") || "Worker profile";
   284→}
   285→
   286→export default function ContractorMatchesPage() {
   287→  const supabase = createClient();
   288→  const [jobs, setJobs] = useState<Job[]>([]);
   289→  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
   290→  const [matches, setMatches] = useState<MatchWithWorker[]>([]);
   291→  const [loading, setLoading] = useState(true);
   292→  const [loadingMatches, setLoadingMatches] = useState(false);
   293→  const [hiring, setHiring] = useState<string | null>(null);
   294→  const [hireSuccess, setHireSuccess] = useState<string | null>(null);
   295→  const [error, setError] = useState("");
   296→
   297→  // Payment modal state
   298→  const [companyId, setCompanyId] = useState<string | null>(null);
   299→  const [selectedMatch, setSelectedMatch] = useState<MatchWithWorker | null>(null);
   300→  const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
   301→
   302→  // Load contractor's jobs
   303→  useEffect(() => {
   304→    async function load() {
   305→      const { data: { user } } = await supabase.auth.getUser();
   306→      if (!user) return;
   307→
   308→      const { data: company } = await supabase
   309→        .from("companies")
   310→        .select("id")
   311→        .eq("profile_id", user.id)
   312→        .single();
   313→
   314→      if (!company) {
   315→        setLoading(false);
   316→        return;
   317→      }
   318→
   319→      setCompanyId(company.id);
   320→
   321→      const { data: jobsData } = await supabase
   322→        .from("jobs")
   323→        .select("*")
   324→        .eq("company_id", company.id)
   325→        .in("status", ["active", "filled"])
   326→        .order("created_at", { ascending: false });
   327→
   328→      if (jobsData && jobsData.length > 0) {
   329→        // Convert PostgreSQL point to JSON format
   330→        const convertedJobs = jobsData.map((job: any) => ({
   331→          ...job,
   332→          location: postgresPointToJson(job.location)
   333→        }));
   334→        setJobs(convertedJobs);
   335→        setSelectedJobId(jobsData[0].id);
   336→      }
   337→      setLoading(false);
   338→    }
   339→    load();
   340→  }, [supabase]);
   341→
   342→  // Load matches for selected job
   343→  useEffect(() => {
   344→    if (!selectedJobId) return;
   345→    async function loadMatches() {
   346→      setLoadingMatches(true);
   347→
   348→      // Get matches for this job
   349→      const { data: matchData } = await supabase
   350→        .from("matches")
   351→        .select("*")
   352→        .eq("job_id", selectedJobId)
   353→        .order("match_score", { ascending: false });
   354→
   355→      if (!matchData || matchData.length === 0) {
   356→        setMatches([]);
   357→        setLoadingMatches(false);
   358→        return;
   359→      }
   360→
   361→      // Fetch worker profiles and base profiles
   362→      const workerIds = matchData.map((m: { worker_id: string }) => m.worker_id);
   363→
   364→      const [{ data: profiles }, { data: workerProfiles }] = await Promise.all([
   365→        supabase.from("profiles").select("id, name, suburb, avatar_url").in("id", workerIds),
   366→        supabase.from("worker_profiles").select("profile_id, trades, experience_years, certifications, availability").in("profile_id", workerIds),
   367→      ]);
   368→
   369→      type ProfileRow = { id: string; name: string | null; suburb: string | null; avatar_url: string | null };
   370→      type WPRow = { profile_id: string; trades: string[]; experience_years: number | null; certifications: string[]; availability: string | null };
   371→
   372→      const profileMap = new Map((profiles || []).map((p: ProfileRow) => [p.id, p]));
   373→      const wpMap = new Map((workerProfiles || []).map((w: WPRow) => [w.profile_id, w]));
   374→
   375→      const enriched: MatchWithWorker[] = matchData.map((m: Record<string, unknown>) => ({
   376→        id: m.id as string,
   377→        job_id: m.job_id as string,
   378→        worker_id: m.worker_id as string,
   379→        status: m.status as string,
   380→        match_score: m.match_score as number | null,
   381→        ai_summary: m.ai_summary as string | null,
   382→        contractor_action: m.contractor_action as string,
   383→        worker_action: m.worker_action as string,
   384→        hired_at: m.hired_at as string | null,
   385→        fee_charged: m.fee_charged as boolean,
   386→        created_at: m.created_at as string,
   387→        worker: profileMap.get(m.worker_id as string) || null,
   388→        worker_profile: wpMap.get(m.worker_id as string) || null,
   389→      }));
   390→
   391→      setMatches(enriched);
   392→      setLoadingMatches(false);
   393→    }
   394→    loadMatches();
   395→  }, [selectedJobId, supabase]);
   396→
   397→  async function handleHire(match: MatchWithWorker) {
   398→    // Haptic feedback
   399→    if (navigator.vibrate) {
   400→      navigator.vibrate(50);
   401→    }
   402→
   403→    if (!companyId) {
   404→      setError("Company not found. Please complete your profile.");
   405→      return;
   406→    }
   407→
   408→    // Open payment modal
   409→    setSelectedMatch(match);
   410→    setIsPaymentModalOpen(true);
   411→  }
   412→
   413→  async function handlePaymentSuccess() {
   414→    if (!selectedMatch) return;
   415→
   416→    // Update local state to show hired
   417→    setMatches((prev) =>
   418→      prev.map((m) =>
   419→        m.id === selectedMatch.id
   420→          ? { ...m, status: "hired", contractor_action: "accepted", hired_at: new Date().toISOString(), fee_charged: true }
   421→          : m
   422→      )
   423→    );
   424→
   425→    setHireSuccess(selectedMatch.id);
   426→    setIsPaymentModalOpen(false);
   427→    setSelectedMatch(null);
   428→    setTimeout(() => setHireSuccess(null), 3000);
   429→  }
   430→
   431→  async function handleReject(match: MatchWithWorker) {
   432→    // Haptic feedback
   433→    if (navigator.vibrate) {
   434→      navigator.vibrate([30, 20, 30]);
   435→    }
   436→
   437→    // Remove from list (soft delete - just update status)
   438→    await supabase
   439→      .from("matches")
   440→      .update({
   441→        contractor_action: "rejected",
   442→      })
   443→      .eq("id", match.id);
   444→
   445→    // Remove from UI
   446→    setMatches((prev) => prev.filter((m) => m.id !== match.id));
   447→  }
   448→
   449→  if (loading) {
   450→    return (
   451→

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

   454→    );
   455→  }
   456→
   457→  return (
   458→

   459→
   460→        {jobs.length === 0 ? (
   461→

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

   464→

   465→              Post a job to start seeing matched workers.
   466→

   467→            <Button asChild className="mt-8 h-16 px-8 text-lg font-semibold">
   468→              <Link href="/contractor/post-job">Post a Job</Link>
   469→            </Button>
   470→

   471→        ) : (
   472→          <>
   473→            {/* Job selector — horizontal scroll */}
   474→            {jobs.length > 1 && (
   475→

   476→                {jobs.map((job) => (
   477→                  <button
   478→                    key={job.id}
   479→                    onClick={() => setSelectedJobId(job.id)}
   480→                    className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   481→                      selectedJobId === job.id
   482→                        ? "bg-primary text-primary-foreground"
   483→                        : "bg-muted text-muted-foreground"
   484→                    }`}
   485→                  >
   486→                    {job.title}
   487→                  </button>
   488→                ))}
   489→

   490→            )}
   491→
   492→            {/* Selected job summary */}
   493→            {selectedJobId && (
   494→

   495→                {(() => {
   496→                  const job = jobs.find((j) => j.id === selectedJobId);
   497→                  if (!job) return null;
   498→                  return (
   499→

   500→                      <Badge variant="secondary">{TRADE_LABEL[job.trade] || job.trade}</Badge>
   501→                      {job.suburb && (
   502→
   503→                          <MapPin className="h-3.5 w-3.5" />
   504→                          {job.suburb}
   505→
   506→                      )}
   507→
   508→                        {matches.length} match{matches.length !== 1 ? "es" : ""}
   509→
   510→

   511→                  );
   512→                })()}
   513→

   514→            )}
   515→
   516→            {/* Matches */}
   517→            {loadingMatches ? (
   518→

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

   521→            ) : matches.length === 0 ? (
   522→

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

   525→

   526→                  Workers will show up here as they apply. We'll notify you.
   527→

   528→

   529→            ) : (
   530→

   531→                {error && (
   532→

   533→                    <AlertCircle className="h-4 w-4 shrink-0" />
   534→                    {error}
   535→

   536→                )}
   537→
   538→                {/* Swipe hint */}
   539→                {matches.length > 0 && matches.some(m => m.status !== "hired") && (
   540→

   541→

   542→                      👉 Swipe right to hire • Swipe left to pass
   543→

   544→

   545→                )}
   546→
   547→                {matches.map((match) => (
   548→                  <SwipeableMatchCard
   549→                    key={match.id}
   550→                    match={match}
   551→                    isHired={match.status === "hired"}
   552→                    justHired={hireSuccess === match.id}
   553→                    hiring={hiring === match.id}
   554→                    onHire={handleHire}
   555→                    onReject={handleReject}
   556→                  />
   557→                ))}
   558→

   559→            )}
   560→          </>
   561→        )}
   562→
   563→
   564→      <BottomNav userType="contractor" />
   565→
   566→      {/* Payment Modal */}
   567→      {selectedMatch && companyId && (
   568→        <HirePaymentModal
   569→          isOpen={isPaymentModalOpen}
   570→          onClose={() => {
   571→            setIsPaymentModalOpen(false);
   572→            setSelectedMatch(null);
   573→          }}
   574→          onSuccess={handlePaymentSuccess}
   575→          matchId={selectedMatch.id}
   576→          companyId={companyId}
   577→          amount={50.0}
   578→          workerName={selectedMatch.worker?.name || undefined}
   579→        />
   580→      )}
   581→

   582→  );
   583→}
   584→

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


1→'use client';
     2→
     3→import { useEffect, useState } from 'react';
     4→import { useSearchParams, useRouter } from 'next/navigation';
     5→import Link from 'next/link';
     6→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     7→import { Button } from '@/components/ui/button';
     8→import { Card, CardContent } from '@/components/ui/card';
     9→import { createClient } from '@/lib/supabase/client';
    10→
    11→export default function PaymentSuccessPage() {
    12→  const searchParams = useSearchParams();
    13→  const router = useRouter();
    14→  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
    15→  const [message, setMessage] = useState('Confirming your payment...');
    16→  const supabase = createClient();
    17→
    18→  useEffect(() => {
    19→    const paymentIntent = searchParams.get('payment_intent');
    20→    const paymentIntentClientSecret = searchParams.get('payment_intent_client_secret');
    21→
    22→    if (!paymentIntent || !paymentIntentClientSecret) {
    23→      setStatus('error');
    24→      setMessage('Invalid payment confirmation');
    25→      return;
    26→    }
    27→
    28→    // Check payment status
    29→    async function checkPayment() {
    30→      try {
    31→        // Wait a moment for webhook to process
    32→        await new Promise(resolve => setTimeout(resolve, 2000));
    33→
    34→        // Check if payment was recorded
    35→        const { data: payment } = await supabase
    36→          .from('payments')
    37→          .select('status, match_id')
    38→          .eq('stripe_payment_id', paymentIntent)
    39→          .single();
    40→
    41→        if (payment?.status === 'charged') {
    42→          setStatus('success');
    43→          setMessage('Your payment was successful and the worker has been notified!');
    44→        } else {
    45→          // Payment might still be processing via webhook
    46→          setStatus('success');
    47→          setMessage('Payment received! Processing your hire...');
    48→        }
    49→      } catch (error) {
    50→        console.error('Error checking payment:', error);
    51→        setStatus('success'); // Assume success since Stripe redirected here
    52→        setMessage('Payment received! Processing your hire...');
    53→      }
    54→    }
    55→
    56→    checkPayment();
    57→  }, [searchParams, supabase]);
    58→
    59→  return (
    60→

    61→      <Card className="w-full max-w-md">
    62→        <CardContent className="pt-8 pb-6 text-center">
    63→          {status === 'loading' && (
    64→            <>
    65→              <Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />
    66→
Confirming Payment

    67→
{message}

    68→            </>
    69→          )}
    70→
    71→          {status === 'success' && (
    72→            <>
    73→

    74→                <CheckCircle2 className="h-10 w-10 text-green-600" />
    75→

    76→
Payment Successful!

    77→
{message}

    78→

    79→                <Button asChild className="w-full h-12 text-base">
    80→                  <Link href="/contractor/matches">Back to Matches</Link>
    81→                </Button>
    82→                <Button asChild variant="outline" className="w-full h-12 text-base">
    83→                  <Link href="/dashboard">Go to Dashboard</Link>
    84→                </Button>
    85→

    86→            </>
    87→          )}
    88→
    89→          {status === 'error' && (
    90→            <>
    91→

    92→                <AlertCircle className="h-10 w-10 text-destructive" />
    93→

    94→
Payment Error

    95→
{message}

    96→

    97→                <Button asChild variant="outline" className="w-full h-12 text-base">
    98→                  <Link href="/contractor/matches">Back to Matches</Link>
    99→                </Button>
   100→

   101→            </>
   102→          )}
   103→        </CardContent>
   104→      </Card>
   105→

   106→  );
   107→}
   108→

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


Now let me read the remaining pages and check for the rate, notifications, and messages pages:


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

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

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

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

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

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

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

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

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

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

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

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

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

   226→

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

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

   237→
   238→                Comment (optional)
   239→
   240→              <textarea
   241→                id="comment"
   242→                value={comment}
   243→                onChange={(e) => setComment(e.target.value)}
   244→                placeholder="How was the experience?"
   245→                rows={3}
   246→                maxLength={500}
   247→                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"
   248→              />
   249→

   250→
   251→            {error && (
   252→

   253→
{error}

   254→

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

   267→
   268→              Skip for now
   269→
   270→

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

   274→  );
   275→}
   276→

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

    90→

    91→

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

   102→

   103→

   104→
Loading conversations...

   105→

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

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

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

   115→

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

   125→

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

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

   133→

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

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

   144→        ) : (
   145→

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

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

   165→  );
   166→}
   167→

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


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

   170→
   171→

   172→

   173→              <Button
   174→                variant="ghost"
   175→                size="icon"
   176→                onClick={() => router.push("/messages")}
   177→                className="h-10 w-10"
   178→              >
   179→                <ArrowLeft className="h-5 w-5" />
   180→              </Button>
   181→

   182→

   183→

   184→

   185→

   186→

   187→

   188→

   189→

   190→
   191→

   192→
Loading messages...

   193→

   194→

   195→    );
   196→  }
   197→
   198→  return (
   199→

   200→      {/* Header */}
   201→
   202→

   203→

   204→            <Button
   205→              variant="ghost"
   206→              size="icon"
   207→              onClick={() => router.push("/messages")}
   208→              className="h-10 w-10"
   209→            >
   210→              <ArrowLeft className="h-5 w-5" />
   211→            </Button>
   212→

   213→              <Avatar className="h-10 w-10">
   214→                <AvatarImage src={otherParticipant?.avatar || ""} alt={otherParticipant?.name || ""} />
   215→                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
   216→                  {initials}
   217→                </AvatarFallback>
   218→              </Avatar>
   219→

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

   221→

   222→                  {otherParticipant?.isOnline ? "Online" : "Offline"}
   223→

   224→

   225→

   226→

   227→

   228→            <Button variant="ghost" size="icon" className="h-10 w-10">
   229→              <Phone className="h-5 w-5" />
   230→            </Button>
   231→            <Button variant="ghost" size="icon" className="h-10 w-10">
   232→              <MoreVertical className="h-5 w-5" />
   233→            </Button>
   234→

   235→

   236→
   237→
   238→      {/* Messages */}
   239→
   240→

   241→          {messages.length === 0 ? (
   242→

   243→
No messages yet. Say hello!

   244→

   245→          ) : (
   246→

   247→              {messages.map((msg) => (
   248→                <MessageBubble
   249→                  key={msg.id}
   250→                  content={msg.content}
   251→                  isOwn={msg.isOwn}
   252→                  timestamp={msg.timestamp}
   253→                  status={msg.status}
   254→                />
   255→              ))}
   256→

   257→

   258→          )}
   259→

   260→
   261→
   262→      {/* Message Input */}
   263→
   264→

   265→          <form onSubmit={handleSendMessage} className="flex gap-3">
   266→            <Input
   267→              type="text"
   268→              placeholder="Type a message..."
   269→              value={message}
   270→              onChange={(e) => setMessage(e.target.value)}
   271→              className="flex-1 h-12 text-base"
   272→              disabled={sending}
   273→            />
   274→            <Button
   275→              type="submit"
   276→              size="icon"
   277→              className="h-12 w-12 shrink-0"
   278→              disabled={!message.trim() || sending}
   279→            >
   280→              <Send className="h-5 w-5" />
   281→            </Button>
   282→          </form>
   283→

   284→
   285→

   286→  );
   287→}
   288→

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


Perfect! Now let me check the backup file mentioned and verify the middleware.ts file as well:


     1→import { createServerClient } from "@supabase/ssr";
     2→import { NextResponse, type NextRequest } from "next/server";
     3→
     4→export async function updateSession(request: NextRequest) {
     5→  let supabaseResponse = NextResponse.next({
     6→    request,
     7→  });
     8→
     9→  const supabase = createServerClient(
    10→    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    11→    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    12→    {
    13→      cookies: {
    14→        getAll() {
    15→          return request.cookies.getAll();
    16→        },
    17→        setAll(cookiesToSet) {
    18→          cookiesToSet.forEach(({ name, value }) =>
    19→            request.cookies.set(name, value)
    20→          );
    21→          supabaseResponse = NextResponse.next({
    22→            request,
    23→          });
    24→          cookiesToSet.forEach(({ name, value, options }) =>
    25→            supabaseResponse.cookies.set(name, value, options)
    26→          );
    27→        },
    28→      },
    29→    }
    30→  );
    31→
    32→  // Use getUser() — the only safe server-side auth check (validates JWT with Supabase).
    33→  // Do NOT use getSession() here — it reads unverified cookie data.
    34→  const {
    35→    data: { user },
    36→  } = await supabase.auth.getUser();
    37→
    38→  const pathname = request.nextUrl.pathname;
    39→
    40→  // Route classification
    41→  const protectedPaths = ["/dashboard", "/contractor", "/worker", "/messages", "/profile"];
    42→  const isProtected = protectedPaths.some((p) => pathname.startsWith(p));
    43→  const isSignup = pathname.includes("/signup");
    44→  const isVerifyPage = pathname === "/auth/verify-email";
    45→  const isAuthPage = pathname.startsWith("/auth/login") || pathname.startsWith("/auth/signup");
    46→
    47→  // ── Not logged in ──
    48→  if (!user) {
    49→    // Redirect unauthenticated users away from protected routes (but allow signup pages)
    50→    if (isProtected && !isSignup) {
    51→      const url = request.nextUrl.clone();
    52→      url.pathname = "/auth/login";
    53→      return NextResponse.redirect(url);
    54→    }
    55→    return supabaseResponse;
    56→  }
    57→
    58→  // ── Logged in ──
    59→  const emailVerified = !!user.email_confirmed_at;
    60→
    61→  if (!emailVerified) {
    62→    // Unverified user trying to access protected routes → send to verify page
    63→    // (but let them stay on signup flows to finish onboarding after verification)
    64→    if (isProtected && !isSignup && !isVerifyPage) {
    65→      const url = request.nextUrl.clone();
    66→      url.pathname = "/auth/verify-email";
    67→      return NextResponse.redirect(url);
    68→    }
    69→  } else {
    70→    // Verified user on auth pages → send to dashboard
    71→    if (isAuthPage || isVerifyPage) {
    72→      const url = request.nextUrl.clone();
    73→      url.pathname = "/dashboard";
    74→      return NextResponse.redirect(url);
    75→    }
    76→  }
    77→
    78→  return supabaseResponse;
    79→}
    80→

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