1→"use client";
     2→
     3→import { useState, useEffect, useCallback } from "react";
     4→import { useRouter } from "next/navigation";
     5→import Link from "next/link";
     6→import toast from "react-hot-toast";
     7→import { createClient } from "@/lib/supabase/client";
     8→import { Button } from "@/components/ui/button";
     9→import { Input } from "@/components/ui/input";
    10→import { Label } from "@/components/ui/label";
    11→import { Card, CardContent } from "@/components/ui/card";
    12→import VoiceRecorder from "@/components/VoiceRecorder";
    13→import {
    14→  HardHat,
    15→  ArrowLeft,
    16→  ArrowRight,
    17→  Loader2,
    18→  CheckCircle2,
    19→  Sparkles,
    20→  MapPin,
    21→  Clock,
    22→  Shield,
    23→  Edit3,
    24→  Wrench,
    25→  Calendar,
    26→  Zap,
    27→  Mic,
    28→} from "lucide-react";
    29→
    30→type Step = "trade" | "experience" | "location" | "availability" | "generating" | "review";
    31→
    32→const TRADES = [
    33→  "Labourer",
    34→  "Steel Fixer",
    35→  "Formworker",
    36→  "Carpenter",
    37→  "Electrician",
    38→  "Plumber",
    39→  "Concreter",
    40→  "Bricklayer",
    41→  "Painter",
    42→  "Roofer",
    43→  "Scaffolder",
    44→  "Tiler",
    45→  "Plasterer",
    46→  "Welder",
    47→  "Crane Operator",
    48→  "Excavator Operator",
    49→];
    50→
    51→const EXPERIENCE_OPTIONS = [
    52→  { label: "Less than 1 year", value: 0 },
    53→  { label: "1-3 years", value: 2 },
    54→  { label: "3-5 years", value: 4 },
    55→  { label: "5-10 years", value: 7 },
    56→  { label: "10+ years", value: 12 },
    57→];
    58→
    59→const AVAILABILITY_OPTIONS = [
    60→  { label: "Full-time", value: "full-time" as const, desc: "Mon-Fri, regular hours" },
    61→  { label: "Casual", value: "casual" as const, desc: "As needed, flexible" },
    62→  { label: "Weekends", value: "weekends" as const, desc: "Saturday & Sunday" },
    63→  { label: "Flexible", value: "flexible" as const, desc: "Any time, any days" },
    64→];
    65→
    66→interface ProfileData {
    67→  trade: string;
    68→  experience: number;
    69→  location: string;
    70→  availability: "full-time" | "casual" | "weekends" | "flexible";
    71→}
    72→
    73→interface GeneratedProfile {
    74→  bio: string;
    75→  skills: string[];
    76→  certifications: string[];
    77→}
    78→
    79→function closestExperience(years: number): number {
    80→  const values = EXPERIENCE_OPTIONS.map((o) => o.value);
    81→  return values.reduce((prev, curr) =>
    82→    Math.abs(curr - years) < Math.abs(prev - years) ? curr : prev
    83→  );
    84→}
    85→
    86→export default function ProfileBuildPage() {
    87→  const [step, setStep] = useState<Step>("trade");
    88→  const [loading, setLoading] = useState(true);
    89→  const [generating, setGenerating] = useState(false);
    90→  const [saving, setSaving] = useState(false);
    91→
    92→  // Form data
    93→  const [trade, setTrade] = useState<string>("");
    94→  const [experience, setExperience] = useState<number | null>(null);
    95→  const [location, setLocation] = useState("");
    96→  const [availability, setAvailability] = useState<ProfileData["availability"] | null>(null);
    97→
    98→  // Generated data
    99→  const [generatedBio, setGeneratedBio] = useState("");
   100→  const [generatedSkills, setGeneratedSkills] = useState<string[]>([]);
   101→  const [generatedCerts, setGeneratedCerts] = useState<string[]>([]);
   102→
   103→  // Editable states
   104→  const [editedBio, setEditedBio] = useState("");
   105→  const [editedSkills, setEditedSkills] = useState<string[]>([]);
   106→  const [editedCerts, setEditedCerts] = useState<string[]>([]);
   107→
   108→  // Voice flow
   109→  const [showVoice, setShowVoice] = useState(false);
   110→  const [voiceProcessing, setVoiceProcessing] = useState(false);
   111→  const [voiceStatus, setVoiceStatus] = useState("");
   112→  const [voiceError, setVoiceError] = useState<string | null>(null);
   113→
   114→  const router = useRouter();
   115→  const supabase = createClient();
   116→
   117→  useEffect(() => {
   118→    async function checkAuth() {
   119→      const {
   120→        data: { user },
   121→      } = await supabase.auth.getUser();
   122→      if (!user) {
   123→        router.push("/auth/login");
   124→        return;
   125→      }
   126→      setLoading(false);
   127→    }
   128→    checkAuth();
   129→  }, [supabase.auth, router]);
   130→
   131→  async function generateProfile() {
   132→    if (!trade || experience === null || !location || !availability) return;
   133→
   134→    setGenerating(true);
   135→    setStep("generating");
   136→
   137→    try {
   138→      const response = await fetch("/api/ai/generate-profile", {
   139→        method: "POST",
   140→        headers: { "Content-Type": "application/json" },
   141→        body: JSON.stringify({
   142→          trade,
   143→          experience,
   144→          location,
   145→          availability,
   146→        }),
   147→      });
   148→
   149→      if (!response.ok) {
   150→        throw new Error("Failed to generate profile");
   151→      }
   152→
   153→      const data: GeneratedProfile = await response.json();
   154→
   155→      setGeneratedBio(data.bio);
   156→      setGeneratedSkills(data.skills);
   157→      setGeneratedCerts(data.certifications);
   158→
   159→      // Initialize editable versions
   160→      setEditedBio(data.bio);
   161→      setEditedSkills(data.skills);
   162→      setEditedCerts(data.certifications);
   163→
   164→      setStep("review");
   165→    } catch (error) {
   166→      console.error("Generation error:", error);
   167→      toast.error("Failed to generate profile. Please try again.");
   168→      setStep("availability");
   169→    } finally {
   170→      setGenerating(false);
   171→    }
   172→  }
   173→
   174→  const handleVoiceComplete = useCallback(async (blob: Blob) => {
   175→    setVoiceProcessing(true);
   176→    setVoiceError(null);
   177→    setVoiceStatus("Transcribing your voice...");
   178→
   179→    try {
   180→      // Step 1: Transcribe
   181→      const formData = new FormData();
   182→      formData.append("audio", blob, "recording.webm");
   183→
   184→      const transcribeRes = await fetch("/api/ai/transcribe-voice", {
   185→        method: "POST",
   186→        body: formData,
   187→      });
   188→
   189→      if (!transcribeRes.ok) {
   190→        throw new Error("Failed to transcribe audio");
   191→      }
   192→
   193→      const { text } = await transcribeRes.json();
   194→      if (!text) throw new Error("No speech detected. Please try again.");
   195→
   196→      setVoiceStatus("Understanding your details...");
   197→
   198→      // Step 2: Parse profile fields
   199→      const parseRes = await fetch("/api/ai/parse-profile-voice", {
   200→        method: "POST",
   201→        headers: { "Content-Type": "application/json" },
   202→        body: JSON.stringify({ transcript: text }),
   203→      });
   204→
   205→      if (!parseRes.ok) {
   206→        throw new Error("Failed to parse your details");
   207→      }
   208→
   209→      const parsed = await parseRes.json();
   210→
   211→      // Pre-fill fields
   212→      if (parsed.trade) setTrade(parsed.trade);
   213→      if (parsed.experience_years !== null && parsed.experience_years !== undefined) {
   214→        setExperience(closestExperience(parsed.experience_years));
   215→      }
   216→      if (parsed.location) setLocation(parsed.location);
   217→      if (parsed.availability) setAvailability(parsed.availability);
   218→
   219→      // Check what we got and move to the right step
   220→      setShowVoice(false);
   221→      setVoiceStatus("");
   222→
   223→      const hasTrade = !!parsed.trade;
   224→      const hasExp = parsed.experience_years !== null && parsed.experience_years !== undefined;
   225→      const hasLoc = !!parsed.location;
   226→      const hasAvail = !!parsed.availability;
   227→
   228→      if (hasTrade && hasExp && hasLoc && hasAvail) {
   229→        // All fields filled — go straight to generating
   230→        // Need to call generateProfile after state updates, so use a timeout
   231→        setTimeout(() => {
   232→          setStep("generating");
   233→        }, 0);
   234→      } else if (!hasTrade) {
   235→        setStep("trade");
   236→      } else if (!hasExp) {
   237→        setStep("experience");
   238→      } else if (!hasLoc) {
   239→        setStep("location");
   240→      } else {
   241→        setStep("availability");
   242→      }
   243→    } catch (error) {
   244→      console.error("Voice processing error:", error);
   245→      setVoiceError(error instanceof Error ? error.message : "Something went wrong. Please try again.");
   246→      setVoiceStatus("");
   247→    } finally {
   248→      setVoiceProcessing(false);
   249→    }
   250→  }, []);
   251→
   252→  // When step changes to "generating" from voice flow, trigger profile generation
   253→  useEffect(() => {
   254→    if (step === "generating" && !generating && trade && experience !== null && location && availability) {
   255→      generateProfile();
   256→    }
   257→    // eslint-disable-next-line react-hooks/exhaustive-deps
   258→  }, [step]);
   259→
   260→  async function handleSave() {
   261→    setSaving(true);
   262→
   263→    try {
   264→      const {
   265→        data: { user },
   266→      } = await supabase.auth.getUser();
   267→      if (!user) throw new Error("Not authenticated");
   268→
   269→      // Update or create worker profile
   270→      const { data: existingProfile } = await supabase
   271→        .from("worker_profiles")
   272→        .select("id")
   273→        .eq("profile_id", user.id)
   274→        .single();
   275→
   276→      const profileData = {
   277→        profile_id: user.id,
   278→        trades: [trade],
   279→        experience_years: experience,
   280→        availability,
   281→        certifications: editedCerts,
   282→        bio: editedBio,
   283→        ai_extracted: {
   284→          skills: editedSkills,
   285→          generated_at: new Date().toISOString(),
   286→          original_bio: generatedBio,
   287→          original_skills: generatedSkills,
   288→          original_certs: generatedCerts,
   289→        },
   290→      };
   291→
   292→      if (existingProfile) {
   293→        await supabase
   294→          .from("worker_profiles")
   295→          .update(profileData)
   296→          .eq("profile_id", user.id);
   297→      } else {
   298→        await supabase.from("worker_profiles").insert(profileData);
   299→      }
   300→
   301→      // Update main profile
   302→      await supabase
   303→        .from("profiles")
   304→        .update({
   305→          suburb: location,
   306→          onboarding_completed: true,
   307→        })
   308→        .eq("id", user.id);
   309→
   310→      router.push("/dashboard");
   311→      router.refresh();
   312→    } catch (error) {
   313→      console.error("Save error:", error);
   314→      toast.error("Failed to save profile. Please try again.");
   315→    } finally {
   316→      setSaving(false);
   317→    }
   318→  }
   319→
   320→  const stepOrder: Step[] = ["trade", "experience", "location", "availability", "generating", "review"];
   321→  const currentIndex = stepOrder.indexOf(step);
   322→  const totalSteps = 4; // Only count user-facing steps
   323→
   324→  if (loading) {
   325→    return (
   326→

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

   329→    );
   330→  }
   331→
   332→  return (
   333→

   334→
   335→        {/* Progress bar */}
   336→        {step !== "generating" && step !== "review" && (
   337→

   338→            {Array.from({ length: totalSteps }, (_, i) => (
   339→              <div
   340→                key={i}
   341→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   342→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   343→                }`}
   344→              />
   345→            ))}
   346→

   347→        )}
   348→
   349→        {/* Step: Trade */}
   350→        {step === "trade" && (
   351→

   352→            <StepHeader
   353→              icon={<Wrench className="h-5 w-5" />}
   354→              title="What's your trade?"
   355→              subtitle="Pick your primary trade"
   356→            />
   357→
   358→            {/* Voice option */}
   359→            {!showVoice && (
   360→              <button
   361→                onClick={() => setShowVoice(true)}
   362→                className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 px-5 py-4 text-base font-medium text-primary transition-colors hover:border-primary hover:bg-primary/10 active:scale-95"
   363→                style={{ minHeight: "64px" }}
   364→              >
   365→                <Mic className="h-6 w-6" />
   366→                Or tell us with your voice
   367→              </button>
   368→            )}
   369→
   370→            {showVoice && (
   371→              <Card className="mt-6">
   372→                <CardContent className="flex flex-col items-center gap-4 pt-6">
   373→

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

   376→
   377→                  {voiceProcessing ? (
   378→

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

   381→

   382→                  ) : (
   383→                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />
   384→                  )}
   385→
   386→                  {voiceError && (
   387→
{voiceError}

   388→                  )}
   389→
   390→                  <button
   391→                    onClick={() => {
   392→                      setShowVoice(false);
   393→                      setVoiceError(null);
   394→                    }}
   395→                    className="text-sm text-muted-foreground hover:text-foreground"
   396→                  >
   397→                    Cancel — I'll tap instead
   398→                  </button>
   399→                </CardContent>
   400→              </Card>
   401→            )}
   402→
   403→

   404→              {TRADES.map((t) => (
   405→                <button
   406→                  key={t}
   407→                  onClick={() => {
   408→                    setTrade(t);
   409→                    setStep("experience");
   410→                  }}
   411→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   412→                    trade === t
   413→                      ? "border-primary bg-primary/10 text-primary"
   414→                      : "border-border hover:border-primary/50"
   415→                  }`}
   416→                  style={{ minHeight: "64px" }}
   417→                >
   418→                  {t}
   419→                </button>
   420→              ))}
   421→

   422→

   423→        )}
   424→
   425→        {/* Step: Experience */}
   426→        {step === "experience" && (
   427→

   428→            <StepHeader
   429→              icon={<Clock className="h-5 w-5" />}
   430→              title="Your experience?"
   431→              subtitle="Years in construction"
   432→            />
   433→
   434→

   435→              {EXPERIENCE_OPTIONS.map((opt) => (
   436→                <button
   437→                  key={opt.value}
   438→                  onClick={() => {
   439→                    setExperience(opt.value);
   440→                    setStep("location");
   441→                  }}
   442→                  className={`flex w-full items-center justify-between rounded-xl border-2 px-5 text-left text-base font-medium transition-colors active:scale-95 ${
   443→                    experience === opt.value
   444→                      ? "border-primary bg-primary/10 text-primary"
   445→                      : "border-border hover:border-primary/50"
   446→                  }`}
   447→                  style={{ minHeight: "64px" }}
   448→                >
   449→                  {opt.label}
   450→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   451→                </button>
   452→              ))}
   453→

   454→
   455→            <button
   456→              onClick={() => setStep("trade")}
   457→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   458→            >
   459→              <ArrowLeft className="h-4 w-4" />
   460→              Back
   461→            </button>
   462→

   463→        )}
   464→
   465→        {/* Step: Location */}
   466→        {step === "location" && (
   467→

   468→            <StepHeader
   469→              icon={<MapPin className="h-5 w-5" />}
   470→              title="Where are you based?"
   471→              subtitle="Your suburb or area"
   472→            />
   473→
   474→            <form
   475→              onSubmit={(e) => {
   476→                e.preventDefault();
   477→                if (location.trim()) setStep("availability");
   478→              }}
   479→              className="mt-6 space-y-4"
   480→            >
   481→

   482→                Suburb or Area
   483→                <Input
   484→                  id="location"
   485→                  placeholder="e.g. Parramatta, Blacktown"
   486→                  value={location}
   487→                  onChange={(e) => setLocation(e.target.value)}
   488→                  required
   489→                  autoFocus
   490→                  className="h-14 text-base"
   491→                />
   492→

   493→

   494→                We'll match you to nearby jobs
   495→

   496→
   497→

   498→                <Button
   499→                  type="button"
   500→                  variant="outline"
   501→                  onClick={() => setStep("experience")}
   502→                  className="h-14 w-14 p-0"
   503→                >
   504→                  <ArrowLeft className="h-5 w-5" />
   505→                </Button>
   506→                <Button
   507→                  type="submit"
   508→                  disabled={!location.trim()}
   509→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   510→                >
   511→                  Continue
   512→                  <ArrowRight className="h-5 w-5" />
   513→                </Button>
   514→

   515→            </form>
   516→

   517→        )}
   518→
   519→        {/* Step: Availability */}
   520→        {step === "availability" && (
   521→

   522→            <StepHeader
   523→              icon={<Calendar className="h-5 w-5" />}
   524→              title="Your availability?"
   525→              subtitle="When can you work?"
   526→            />
   527→
   528→

   529→              {AVAILABILITY_OPTIONS.map((opt) => (
   530→                <button
   531→                  key={opt.value}
   532→                  onClick={() => {
   533→                    setAvailability(opt.value);
   534→                    generateProfile();
   535→                  }}
   536→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   537→                    availability === opt.value
   538→                      ? "border-primary bg-primary/10"
   539→                      : "border-border hover:border-primary/50"
   540→                  }`}
   541→                  style={{ minHeight: "64px" }}
   542→                >
   543→

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

   547→
{opt.desc}

   548→

   549→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   550→                </button>
   551→              ))}
   552→

   553→
   554→            <button
   555→              onClick={() => setStep("location")}
   556→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   557→            >
   558→              <ArrowLeft className="h-4 w-4" />
   559→              Back
   560→            </button>
   561→

   562→        )}
   563→
   564→        {/* Step: Generating */}
   565→        {step === "generating" && (
   566→

   567→

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

   570→
Building your profile...

   571→

   572→              AI is generating your bio, skills, and certifications
   573→

   574→

   575→

   576→

   577→

   578→

   579→

   580→        )}
   581→
   582→        {/* Step: Review */}
   583→        {step === "review" && (
   584→

   585→

   586→

   587→                <Sparkles className="h-5 w-5" />
   588→

   589→

   590→
Review & Edit

   591→

   592→                  AI generated this — customise as needed
   593→

   594→

   595→

   596→
   597→            {/* Your Answers Summary */}
   598→            <Card className="mt-6">
   599→              <CardContent className="space-y-3 pt-6">
   600→

   601→

   602→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   603→                    {trade}
   604→

   605→                  <button
   606→                    onClick={() => setStep("trade")}
   607→                    className="text-xs text-primary hover:underline"
   608→                  >
   609→                    Change
   610→                  </button>
   611→

   612→

   613→

   614→                    <Clock className="h-4 w-4 text-muted-foreground" />
   615→
   616→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   617→
   618→

   619→                  <button
   620→                    onClick={() => setStep("experience")}
   621→                    className="text-xs text-primary hover:underline"
   622→                  >
   623→                    Change
   624→                  </button>
   625→

   626→

   627→

   628→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   629→                    {location}
   630→

   631→                  <button
   632→                    onClick={() => setStep("location")}
   633→                    className="text-xs text-primary hover:underline"
   634→                  >
   635→                    Change
   636→                  </button>
   637→

   638→              </CardContent>
   639→            </Card>
   640→
   641→            {/* Bio */}
   642→

   643→

   644→
   645→                  <Sparkles className="h-4 w-4 text-primary" />
   646→                  Your Bio
   647→
   648→                AI-generated
   649→

   650→              <textarea
   651→                value={editedBio}
   652→                onChange={(e) => setEditedBio(e.target.value)}
   653→                className="min-h-[120px] w-full rounded-xl border-2 border-border bg-background px-4 py-3 text-base focus:border-primary focus:outline-none"
   654→                placeholder="Your professional bio..."
   655→              />
   656→

   657→
   658→            {/* Skills */}
   659→

   660→

   661→
   662→                  <Zap className="h-4 w-4 text-primary" />
   663→                  Skills
   664→
   665→                Tap to remove
   666→

   667→

   668→                {editedSkills.map((skill, idx) => (
   669→                  <button
   670→                    key={idx}
   671→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   672→                    className="rounded-full border-2 border-primary bg-primary/10 px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-primary/20 active:scale-95"
   673→                  >
   674→                    {skill} ×
   675→                  </button>
   676→                ))}
   677→

   678→

   679→
   680→            {/* Certifications */}
   681→

   682→

   683→
   684→                  <Shield className="h-4 w-4 text-primary" />
   685→                  Certifications
   686→
   687→                Tap to remove
   688→

   689→

   690→                {editedCerts.map((cert, idx) => (
   691→                  <button
   692→                    key={idx}
   693→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   694→                    className="flex w-full items-center justify-between rounded-xl border-2 border-border bg-background px-4 py-3 text-left text-sm font-medium transition-colors hover:border-destructive hover:bg-destructive/5 active:scale-95"
   695→                  >
   696→                    {cert}
   697→                    Remove
   698→                  </button>
   699→                ))}
   700→

   701→

   702→

   703→        )}
   704→
   705→
   706→      {/* Bottom sticky CTA - only show on review step */}
   707→      {step === "review" && (
   708→

   709→

   710→            <Button
   711→              onClick={handleSave}
   712→              className="h-16 w-full gap-2 text-lg font-semibold"
   713→              disabled={saving}
   714→            >
   715→              {saving ? (
   716→                <>
   717→                  <Loader2 className="h-6 w-6 animate-spin" />
   718→                  Saving profile...
   719→                </>
   720→              ) : (
   721→                <>
   722→                  <CheckCircle2 className="h-6 w-6" />
   723→                  Save My Profile
   724→                </>
   725→              )}
   726→            </Button>
   727→

   728→

   729→      )}
   730→

   731→  );
   732→}
   733→
   734→function StepHeader({
   735→  icon,
   736→  title,
   737→  subtitle,
   738→}: {
   739→  icon: React.ReactNode;
   740→  title: string;
   741→  subtitle: string;
   742→}) {
   743→  return (
   744→

   745→

   746→        {icon}
   747→

   748→

   749→
{title}

   750→
{subtitle}

   751→

   752→

   753→  );
   754→}
   755→

<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 {
    11→  MapPin,
    12→  Calendar,
    13→  DollarSign,
    14→  Loader2,
    15→  Search,
    16→  Briefcase,
    17→  Check,
    18→  X,
    19→} from "lucide-react";
    20→import type { Job } from "@/lib/types";
    21→import { postgresPointToJson } from "@/lib/location-utils";
    22→
    23→const TRADE_FILTERS = [
    24→  "All",
    25→  "Steel Fixer",
    26→  "Formworker",
    27→  "Labourer",
    28→  "Concreter",
    29→  "Carpenter",
    30→  "Scaffolder",
    31→  "Other",
    32→];
    33→
    34→const TRADE_ID_TO_LABEL: Record<string, string> = {
    35→  "steel-fixer": "Steel Fixer",
    36→  "formworker": "Formworker",
    37→  "labourer": "Labourer",
    38→  "concreter": "Concreter",
    39→  "carpenter": "Carpenter",
    40→  "scaffolder": "Scaffolder",
    41→  "other": "Other",
    42→};
    43→
    44→function tradeLabelFromId(id: string): string {
    45→  return TRADE_ID_TO_LABEL[id] || id;
    46→}
    47→
    48→interface SwipeableJobCardProps {
    49→  job: Job;
    50→  hasApplied: boolean;
    51→  onApply: (job: Job) => void;
    52→  onClick: (job: Job) => void;
    53→}
    54→
    55→function SwipeableJobCard({ job, hasApplied, onApply, onClick }: SwipeableJobCardProps) {
    56→  const [swipeX, setSwipeX] = useState(0);
    57→  const [swiping, setSwiping] = useState(false);
    58→  const startX = useRef(0);
    59→  const currentX = useRef(0);
    60→
    61→  const handleTouchStart = (e: React.TouchEvent) => {
    62→    if (hasApplied) return;
    63→    startX.current = e.touches[0].clientX;
    64→    setSwiping(true);
    65→  };
    66→
    67→  const handleTouchMove = (e: React.TouchEvent) => {
    68→    if (!swiping || hasApplied) return;
    69→    currentX.current = e.touches[0].clientX;
    70→    const diff = currentX.current - startX.current;
    71→    // Only allow right swipe (positive diff)
    72→    if (diff > 0) {
    73→      setSwipeX(Math.min(diff, 150));
    74→    }
    75→  };
    76→
    77→  const handleTouchEnd = () => {
    78→    if (!swiping || hasApplied) return;
    79→    setSwiping(false);
    80→
    81→    // If swiped more than 100px, apply
    82→    if (swipeX > 100) {
    83→      onApply(job);
    84→    }
    85→
    86→    // Reset
    87→    setSwipeX(0);
    88→  };
    89→
    90→  const swipeProgress = Math.min(swipeX / 100, 1);
    91→  const backgroundColor = hasApplied
    92→    ? "transparent"
    93→    : `rgba(34, 197, 94, ${swipeProgress * 0.1})`;
    94→
    95→  return (
    96→

    97→      {/* Swipe indicator background */}
    98→      {!hasApplied && swipeX > 0 && (
    99→

   106→          <Check className="h-8 w-8 text-green-600" />
   107→
   108→            {swipeProgress >= 1 ? "Release to apply" : "Swipe to apply"}
   109→
   110→

   111→      )}
   112→
   113→      <Card
   114→        className="cursor-pointer active:scale-[0.98] transition-transform touch-pan-y"
   115→        style={{
   116→          transform: `translateX(${swipeX}px)`,
   117→          transition: swiping ? 'none' : 'transform 0.2s ease-out',
   118→          backgroundColor,
   119→        }}
   120→        onTouchStart={handleTouchStart}
   121→        onTouchMove={handleTouchMove}
   122→        onTouchEnd={handleTouchEnd}
   123→        onClick={() => !swiping && onClick(job)}
   124→      >
   125→        <CardContent className="p-5">
   126→

   127→

   128→

   129→                <Badge variant="secondary" className="text-sm shrink-0 py-1 px-3">
   130→                  {tradeLabelFromId(job.trade)}
   131→                </Badge>
   132→                {hasApplied && (
   133→                  <Badge className="text-sm bg-green-600 shrink-0 py-1 px-3">
   134→                    <Check className="h-3.5 w-3.5 mr-1" />
   135→                    Applied
   136→                  </Badge>
   137→                )}
   138→

   139→
{job.title}

   140→

   141→

   142→
   143→

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

   177→        </CardContent>
   178→      </Card>
   179→

   180→  );
   181→}
   182→
   183→export default function WorkerJobsPage() {
   184→  const supabase = createClient();
   185→  const [jobs, setJobs] = useState<Job[]>([]);
   186→  const [loading, setLoading] = useState(true);
   187→  const [activeTrade, setActiveTrade] = useState("All");
   188→  const [selectedJob, setSelectedJob] = useState<Job | null>(null);
   189→  const [applying, setApplying] = useState(false);
   190→  const [appliedJobs, setAppliedJobs] = useState<Set<string>>(new Set());
   191→  const [userId, setUserId] = useState<string | null>(null);
   192→
   193→  useEffect(() => {
   194→    async function load() {
   195→      const { data: { user } } = await supabase.auth.getUser();
   196→      if (user) setUserId(user.id);
   197→
   198→      // Fetch active jobs
   199→      const { data } = await supabase
   200→        .from("jobs")
   201→        .select("*")
   202→        .eq("status", "active")
   203→        .order("created_at", { ascending: false });
   204→
   205→      if (data) {
   206→        // Convert PostgreSQL point to JSON format
   207→        const convertedJobs = data.map((job: any) => ({
   208→          ...job,
   209→          location: postgresPointToJson(job.location)
   210→        }));
   211→        setJobs(convertedJobs);
   212→      }
   213→
   214→      // Fetch already-applied jobs
   215→      if (user) {
   216→        const { data: matches } = await supabase
   217→          .from("matches")
   218→          .select("job_id")
   219→          .eq("worker_id", user.id);
   220→
   221→        if (matches) {
   222→          setAppliedJobs(new Set(matches.map((m: { job_id: string }) => m.job_id)));
   223→        }
   224→      }
   225→
   226→      setLoading(false);
   227→    }
   228→    load();
   229→  }, [supabase]);
   230→
   231→  const filteredJobs =
   232→    activeTrade === "All"
   233→      ? jobs
   234→      : jobs.filter(
   235→          (j) => tradeLabelFromId(j.trade).toLowerCase() === activeTrade.toLowerCase()
   236→        );
   237→
   238→  async function handleApply(job: Job) {
   239→    if (!userId || appliedJobs.has(job.id)) return;
   240→    setApplying(true);
   241→
   242→    // Haptic feedback (if supported)
   243→    if (navigator.vibrate) {
   244→      navigator.vibrate(50);
   245→    }
   246→
   247→    const { error } = await supabase.from("matches").insert({
   248→      job_id: job.id,
   249→      worker_id: userId,
   250→      status: "applied",
   251→      contractor_action: "pending",
   252→      worker_action: "accepted",
   253→    });
   254→
   255→    if (!error) {
   256→      setAppliedJobs((prev) => new Set(prev).add(job.id));
   257→    }
   258→
   259→    setApplying(false);
   260→    setSelectedJob(null);
   261→  }
   262→
   263→  if (loading) {
   264→    return (
   265→

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

   268→    );
   269→  }
   270→
   271→  return (
   272→

   273→
   274→        {/* Trade filters — horizontal scroll */}
   275→

   276→          {TRADE_FILTERS.map((trade) => (
   277→            <button
   278→              key={trade}
   279→              onClick={() => setActiveTrade(trade)}
   280→              className={`shrink-0 rounded-full px-5 py-3 text-base font-semibold transition-all active:scale-95 min-h-[48px] ${
   281→                activeTrade === trade
   282→                  ? "bg-primary text-primary-foreground"
   283→                  : "bg-muted text-muted-foreground"
   284→              }`}
   285→            >
   286→              {trade}
   287→            </button>
   288→          ))}
   289→

   290→
   291→        {/* Swipe hint */}
   292→        {filteredJobs.length > 0 && !loading && (
   293→

   294→

   295→              👉 Swipe right to quick apply • Tap to view details
   296→

   297→

   298→        )}
   299→
   300→        {/* Job list */}
   301→        {filteredJobs.length === 0 ? (
   302→

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

   305→

   306→              {activeTrade === "All"
   307→                ? "No jobs posted yet. Check back soon."
   308→                : `No ${activeTrade} jobs right now. Try another trade.`}
   309→

   310→

   311→        ) : (
   312→

   313→            {filteredJobs.map((job) => (
   314→              <SwipeableJobCard
   315→                key={job.id}
   316→                job={job}
   317→                hasApplied={appliedJobs.has(job.id)}
   318→                onApply={handleApply}
   319→                onClick={setSelectedJob}
   320→              />
   321→            ))}
   322→

   323→        )}
   324→
   325→
   326→      {/* Job Detail Sheet (bottom sheet style) */}
   327→      {selectedJob && (
   328→

   329→
 setSelectedJob(null)}
   332→          />
   333→

   334→            {/* Handle */}
   335→

   336→

   337→

   338→
   339→

   340→              <Badge variant="secondary" className="mb-3 text-sm py-1.5 px-3">
   341→                {tradeLabelFromId(selectedJob.trade)}
   342→              </Badge>
   343→
{selectedJob.title}

   344→
   345→

   346→                {selectedJob.suburb && (
   347→
   348→                    <MapPin className="h-5 w-5" />
   349→                    {selectedJob.site_address || selectedJob.suburb}
   350→
   351→                )}
   352→                {(selectedJob.rate_min || selectedJob.rate_max) && (
   353→
   354→                    <DollarSign className="h-5 w-5" />
   355→                    {selectedJob.rate_min && selectedJob.rate_max
   356→                      ? `$${selectedJob.rate_min}–$${selectedJob.rate_max}`
   357→                      : selectedJob.rate_min
   358→                      ? `From $${selectedJob.rate_min}`
   359→                      : `Up to $${selectedJob.rate_max}`}
   360→                    {selectedJob.rate_type === "hourly" ? "/hr" : "/day"}
   361→
   362→                )}
   363→

   364→
   365→

   366→                {selectedJob.start_date && (
   367→
   368→                    <Calendar className="h-5 w-5" />
   369→                    Starts{" "}
   370→                    {new Date(selectedJob.start_date).toLocaleDateString("en-AU", {
   371→                      weekday: "short",
   372→                      day: "numeric",
   373→                      month: "short",
   374→                    })}
   375→
   376→                )}
   377→                {selectedJob.duration && (
   378→                  Duration: {selectedJob.duration}
   379→                )}
   380→                {selectedJob.workers_needed > 1 && (
   381→
   382→                    <Briefcase className="h-5 w-5" />
   383→                    {selectedJob.workers_needed} workers needed
   384→
   385→                )}
   386→

   387→
   388→              {selectedJob.description && (
   389→

   390→
About the Job

   391→

   392→                    {selectedJob.description}
   393→

   394→

   395→              )}
   396→
   397→              {selectedJob.certifications_required?.length > 0 && (
   398→

   399→
Required Certs

   400→

   401→                    {selectedJob.certifications_required.map((cert) => (
   402→                      <Badge key={cert} variant="outline" className="text-sm py-1.5 px-3">
   403→                        {cert}
   404→                      </Badge>
   405→                    ))}
   406→

   407→

   408→              )}
   409→

   410→
   411→            {/* Sticky Bottom CTA */}
   412→

   413→

   414→                {appliedJobs.has(selectedJob.id) ? (
   415→                  <Button disabled className="w-full h-16 text-lg font-semibold bg-green-600">
   416→                    <Check className="mr-2 h-6 w-6" />
   417→                    Already Applied
   418→                  </Button>
   419→                ) : (
   420→                  <Button
   421→                    onClick={() => handleApply(selectedJob)}
   422→                    className="w-full h-16 text-lg font-semibold"
   423→                    disabled={applying}
   424→                  >
   425→                    {applying ? (
   426→                      <>
   427→                        <Loader2 className="mr-2 h-6 w-6 animate-spin" />
   428→                        Applying...
   429→                      </>
   430→                    ) : (
   431→                      <>
   432→                        Apply with One Tap
   433→                        <Check className="ml-2 h-6 w-6" />
   434→                      </>
   435→                    )}
   436→                  </Button>
   437→                )}
   438→

   439→                  Always free for workers. No fees ever.
   440→

   441→

   442→

   443→

   444→

   445→      )}
   446→
   447→      <BottomNav userType="worker" />
   448→

   449→  );
   450→}
   451→

<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 { Search, MessageSquare } from "lucide-react";
    10→import { createClient } from "@/lib/supabase/client";
    11→
    12→interface Conversation {
    13→  conversation_id: string;
    14→  other_participant_name: string;
    15→  other_participant_avatar: string | null;
    16→  last_message: string | null;
    17→  last_message_time: string | null;
    18→  unread_count: number;
    19→  other_participant_is_online: boolean;
    20→}
    21→
    22→export default function MessagesPage() {
    23→  const router = useRouter();
    24→  const [searchQuery, setSearchQuery] = useState("");
    25→  const [conversations, setConversations] = useState<Conversation[]>([]);
    26→  const [loading, setLoading] = useState(true);
    27→  const supabase = createClient();
    28→
    29→  useEffect(() => {
    30→    fetchConversations();
    31→
    32→    // Subscribe to new messages for real-time updates
    33→    const channel = supabase
    34→      .channel('messages')
    35→      .on('postgres_changes',
    36→        {
    37→          event: 'INSERT',
    38→          schema: 'public',
    39→          table: 'messages'
    40→        },
    41→        () => {
    42→          fetchConversations();
    43→        }
    44→      )
    45→      .subscribe();
    46→
    47→    return () => {
    48→      supabase.removeChannel(channel);
    49→    };
    50→  }, []);
    51→
    52→  const fetchConversations = async () => {
    53→    try {
    54→      const response = await fetch('/api/messages');
    55→      if (response.ok) {
    56→        const data = await response.json();
    57→        setConversations(data.conversations || []);
    58→      }
    59→    } catch (error) {
    60→      console.error("Error fetching conversations:", error);
    61→    } finally {
    62→      setLoading(false);
    63→    }
    64→  };
    65→
    66→  const filteredConversations = conversations.filter((conversation) =>
    67→    conversation.other_participant_name?.toLowerCase().includes(searchQuery.toLowerCase())
    68→  );
    69→
    70→  const totalUnread = conversations.reduce(
    71→    (sum, conv) => sum + conv.unread_count,
    72→    0
    73→  );
    74→
    75→  if (loading) {
    76→    return (
    77→

    78→

    79→

    80→            <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
    81→            <Input
    82→              type="search"
    83→              placeholder="Search conversations..."
    84→              value={searchQuery}
    85→              onChange={(e) => setSearchQuery(e.target.value)}
    86→              className="h-12 pl-10 text-base"
    87→              disabled
    88→            />
    89→

    90→

    91→

    92→
Loading conversations...

    93→

    94→

    95→    );
    96→  }
    97→
    98→  return (
    99→

   100→      {/* Search Bar */}
   101→

   102→

   103→          <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
   104→          <Input
   105→            type="search"
   106→            placeholder="Search conversations..."
   107→            value={searchQuery}
   108→            onChange={(e) => setSearchQuery(e.target.value)}
   109→            className="h-12 pl-10 text-base"
   110→          />
   111→

   112→

   113→
   114→      {/* Conversations List */}
   115→
   116→        {filteredConversations.length === 0 ? (
   117→

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

   120→

   121→              {searchQuery
   122→                ? "No conversations match your search"
   123→                : "Start a conversation by hiring a worker or applying for a job"}
   124→

   125→            {!searchQuery && (
   126→              <Button asChild>
   127→                <Link href="/dashboard">Browse Jobs</Link>
   128→              </Button>
   129→            )}
   130→

   131→        ) : (
   132→

   133→            {filteredConversations.map((conversation) => (
   134→              <ConversationPreview
   135→                key={conversation.conversation_id}
   136→                id={conversation.conversation_id}
   137→                name={conversation.other_participant_name}
   138→                avatar={conversation.other_participant_avatar || undefined}
   139→                lastMessage={conversation.last_message || "No messages yet"}
   140→                lastMessageTime={conversation.last_message_time ? new Date(conversation.last_message_time) : new Date()}
   141→                unreadCount={conversation.unread_count}
   142→                isOnline={conversation.other_participant_is_online}
   143→                onClick={() => router.push(`/messages/${conversation.conversation_id}`)}
   144→              />
   145→            ))}
   146→

   147→        )}
   148→
   149→

   150→  );
   151→}
   152→

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


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 { Input } from "@/components/ui/input";
     9→import { Label } from "@/components/ui/label";
    10→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
    11→
    12→export default function LoginPage() {
    13→  const [email, setEmail] = useState("");
    14→  const [password, setPassword] = useState("");
    15→  const [error, setError] = useState<string | null>(null);
    16→  const [loading, setLoading] = useState(false);
    17→  const router = useRouter();
    18→  const supabase = createClient();
    19→
    20→  async function handleLogin(e: React.FormEvent) {
    21→    e.preventDefault();
    22→    setLoading(true);
    23→    setError(null);
    24→
    25→    const { data, error } = await supabase.auth.signInWithPassword({
    26→      email,
    27→      password,
    28→    });
    29→
    30→    if (error) {
    31→      setError(error.message);
    32→      setLoading(false);
    33→      return;
    34→    }
    35→
    36→    // Check if email is verified before allowing dashboard access
    37→    if (data.user && !data.user.email_confirmed_at) {
    38→      router.push("/auth/verify-email");
    39→      router.refresh();
    40→      return;
    41→    }
    42→
    43→    router.push("/(authenticated)/dashboard");
    44→    router.refresh();
    45→  }
    46→
    47→  return (
    48→

    49→      <Card className="w-full max-w-sm">
    50→        <CardHeader className="text-center">
    51→          <CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
    52→          <CardDescription>Sign in to your RateRight account</CardDescription>
    53→        </CardHeader>
    54→        <CardContent>
    55→          <form onSubmit={handleLogin} className="space-y-5">
    56→

    57→              Email
    58→              <Input
    59→                id="email"
    60→                type="email"
    61→                placeholder="you@example.com"
    62→                value={email}
    63→                onChange={(e) => setEmail(e.target.value)}
    64→                required
    65→                autoComplete="email"
    66→                className="h-14 text-base"
    67→              />
    68→

    69→

    70→              Password
    71→              <Input
    72→                id="password"
    73→                type="password"
    74→                placeholder="Your password"
    75→                value={password}
    76→                onChange={(e) => setPassword(e.target.value)}
    77→                required
    78→                autoComplete="current-password"
    79→                className="h-14 text-base"
    80→              />
    81→

    82→            {error && (
    83→
{error}

    84→            )}
    85→            <Button type="submit" className="w-full h-16 text-lg font-semibold" disabled={loading}>
    86→              {loading ? "Signing in..." : "Sign in"}
    87→            </Button>
    88→          </form>
    89→

    90→            Don't have an account?{" "}
    91→            <Link href="/auth/signup" className="text-primary underline hover:no-underline">
    92→              Sign up
    93→            </Link>
    94→

    95→        </CardContent>
    96→      </Card>
    97→

    98→  );
    99→}
   100→

<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 } 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 { Mail, CheckCircle2, Clock } from "lucide-react";
     9→
    10→export default function VerifyEmailPage() {
    11→  const [emailSent, setEmailSent] = useState(false);
    12→  const [loading, setLoading] = useState(false);
    13→  const [error, setError] = useState<string | null>(null);
    14→  const router = useRouter();
    15→  const supabase = createClient();
    16→
    17→  useEffect(() => {
    18→    // Check if user is already logged in and email is verified
    19→    checkEmailStatus();
    20→  }, []);
    21→
    22→  async function checkEmailStatus() {
    23→    const { data: { user } } = await supabase.auth.getUser();
    24→
    25→    if (user?.email_confirmed_at) {
    26→      // Email is already verified, redirect to dashboard
    27→      router.push('/(authenticated)/dashboard');
    28→    }
    29→  }
    30→
    31→  async function resendVerificationEmail() {
    32→    setLoading(true);
    33→    setError(null);
    34→
    35→    try {
    36→      const { data: { user } } = await supabase.auth.getUser();
    37→
    38→      if (!user) {
    39→        setError("No user found. Please log in again.");
    40→        setLoading(false);
    41→        return;
    42→      }
    43→
    44→      const { error } = await supabase.auth.resend({
    45→        type: 'signup',
    46→        email: user.email as string,
    47→      });
    48→
    49→      if (error) {
    50→        setError(error.message);
    51→      } else {
    52→        setEmailSent(true);
    53→      }
    54→    } catch (error) {
    55→      setError("Failed to resend verification email.");
    56→    } finally {
    57→      setLoading(false);
    58→    }
    59→  }
    60→
    61→  return (
    62→

    63→      <Card className="w-full max-w-md">
    64→        <CardHeader className="text-center">
    65→

    66→            <Mail className="h-6 w-6 text-blue-600" />
    67→

    68→          <CardTitle className="mt-6 text-3xl font-extrabold text-gray-900">
    69→            Check your email
    70→          </CardTitle>
    71→          <CardDescription className="mt-2 text-sm text-gray-600">
    72→            We've sent a verification email to your address. Please check your inbox and click the link to verify your email.
    73→          </CardDescription>
    74→        </CardHeader>
    75→        <CardContent className="space-y-6">
    76→

    77→

    78→              <Clock className="h-5 w-5 text-blue-400 mt-0.5" />
    79→

    80→
Didn't receive the email?

    81→

    82→
Check your spam folder or click the button below to resend the verification email.

    83→

    84→

    85→

    86→

    87→
    88→          {error && (
    89→

    90→

    91→

    92→
Error

    93→

    94→
{error}

    95→

    96→

    97→

    98→

    99→          )}
   100→
   101→          {emailSent && (
   102→

   103→

   104→                <CheckCircle2 className="h-5 w-5 text-green-400" />
   105→

   106→
Email sent!

   107→

   108→
We've resent the verification email. Please check your inbox.

   109→

   110→

   111→

   112→

   113→          )}
   114→
   115→

   116→            <Button
   117→              onClick={resendVerificationEmail}
   118→              disabled={loading || emailSent}
   119→              className="w-full"
   120→              variant="outline"
   121→            >
   122→              {loading ? "Sending..." : emailSent ? "Email sent" : "Resend verification email"}
   123→            </Button>
   124→
   125→            <Button
   126→              onClick={() => router.push('/auth/login')}
   127→              variant="ghost"
   128→              className="w-full"
   129→            >
   130→              Back to login
   131→            </Button>
   132→

   133→        </CardContent>
   134→      </Card>
   135→

   136→  );
   137→}
   138→

<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 authenticated layout and the remaining component files.


1→import { redirect } from 'next/navigation'
     2→import { createClient } from '@/lib/supabase/server'
     3→import Link from 'next/link'
     4→import { Home, Briefcase, MessageSquare, User } from 'lucide-react'
     5→
     6→export default async function AuthenticatedLayout({
     7→  children,
     8→}: {
     9→  children: React.ReactNode
    10→}) {
    11→  const supabase = await createClient()
    12→  const { data: { user } } = await supabase.auth.getUser()
    13→
    14→  if (!user) {
    15→    redirect('/auth/login')
    16→  }
    17→
    18→  return (
    19→

    20→      {/* Header */}
    21→
    22→

    23→          <Link href="/dashboard" className="text-xl font-bold text-blue-600">
    24→            RateRight
    25→          </Link>
    26→          {user.email}
    27→

    28→
    29→
    30→      {/* Main Content */}
    31→
    32→        {children}
    33→
    34→
    35→      {/* Bottom Navigation */}
    36→
    37→

    38→          <Link href="/dashboard" className="flex flex-col items-center p-2 text-gray-600 hover:text-blue-600">
    39→            <Home className="w-6 h-6" />
    40→            Home
    41→          </Link>
    42→          <Link href="/contractor/post-job" className="flex flex-col items-center p-2 text-gray-600 hover:text-blue-600">
    43→            <Briefcase className="w-6 h-6" />
    44→            Jobs
    45→          </Link>
    46→          <Link href="/messages" className="flex flex-col items-center p-2 text-gray-600 hover:text-blue-600">
    47→            <MessageSquare className="w-6 h-6" />
    48→            Messages
    49→          </Link>
    50→          <Link href="/worker/profile/build" className="flex flex-col items-center p-2 text-gray-600 hover:text-blue-600">
    51→            <User className="w-6 h-6" />
    52→            Profile
    53→          </Link>
    54→

    55→
    56→

    57→  )
    58→}
    59→

<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 Link from "next/link";
     4→import { usePathname } from "next/navigation";
     5→import { Home, Briefcase, 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: isContractor ? "/contractor/matches" : "/worker/jobs",
    25→      label: "Jobs",
    26→      icon: Briefcase,
    27→    },
    28→    {
    29→      href: isContractor ? "/contractor/post-job" : "/worker/jobs",
    30→      label: "Post",
    31→      icon: Plus,
    32→      isCenter: isContractor,
    33→    },
    34→    {
    35→      href: "/messages",
    36→      label: "Messages",
    37→      icon: MessageSquare,
    38→    },
    39→    {
    40→      href: "/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: Briefcase,
    57→    },
    58→    {
    59→      href: "/messages",
    60→      label: "Messages",
    61→      icon: MessageSquare,
    62→    },
    63→    {
    64→      href: "/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>


     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>


1→"use client";
     2→
     3→import { useState, useRef, useCallback } from "react";
     4→import { Mic, Square } from "lucide-react";
     5→
     6→interface VoiceRecorderProps {
     7→  onRecordingComplete: (blob: Blob) => void;
     8→  disabled?: boolean;
     9→}
    10→
    11→export default function VoiceRecorder({ onRecordingComplete, disabled }: VoiceRecorderProps) {
    12→  const [recording, setRecording] = useState(false);
    13→  const [error, setError] = useState<string | null>(null);
    14→  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
    15→  const chunksRef = useRef<Blob[]>([]);
    16→
    17→  const startRecording = useCallback(async () => {
    18→    setError(null);
    19→    try {
    20→      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    21→      const mediaRecorder = new MediaRecorder(stream, {
    22→        mimeType: MediaRecorder.isTypeSupported("audio/webm")
    23→          ? "audio/webm"
    24→          : "audio/mp4",
    25→      });
    26→      mediaRecorderRef.current = mediaRecorder;
    27→      chunksRef.current = [];
    28→
    29→      mediaRecorder.ondataavailable = (e) => {
    30→        if (e.data.size > 0) {
    31→          chunksRef.current.push(e.data);
    32→        }
    33→      };
    34→
    35→      mediaRecorder.onstop = () => {
    36→        const blob = new Blob(chunksRef.current, { type: mediaRecorder.mimeType });
    37→        onRecordingComplete(blob);
    38→        stream.getTracks().forEach((track) => track.stop());
    39→      };
    40→
    41→      mediaRecorder.start();
    42→      setRecording(true);
    43→    } catch {
    44→      setError("Microphone access denied. Please allow mic access and try again.");
    45→    }
    46→  }, [onRecordingComplete]);
    47→
    48→  const stopRecording = useCallback(() => {
    49→    if (mediaRecorderRef.current && mediaRecorderRef.current.state === "recording") {
    50→      mediaRecorderRef.current.stop();
    51→      setRecording(false);
    52→    }
    53→  }, []);
    54→
    55→  return (
    56→

    57→      <button
    58→        type="button"
    59→        onClick={recording ? stopRecording : startRecording}
    60→        disabled={disabled}
    61→        className={`flex h-16 w-16 items-center justify-center rounded-full transition-all active:scale-95 ${
    62→          recording
    63→            ? "animate-pulse bg-red-500 text-white shadow-lg shadow-red-500/40"
    64→            : "bg-primary text-primary-foreground shadow-md hover:shadow-lg"
    65→        } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
    66→        aria-label={recording ? "Stop recording" : "Start recording"}
    67→      >
    68→        {recording ? (
    69→          <Square className="h-7 w-7" />
    70→        ) : (
    71→          <Mic className="h-7 w-7" />
    72→        )}
    73→      </button>
    74→

    75→        {recording ? "Tap to stop" : "Tap to speak"}
    76→

    77→      {error && (
    78→
{error}

    79→      )}
    80→

    81→  );
    82→}
    83→

<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 { Card } from "@/components/ui/card";
     2→import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
     3→import { Badge } from "@/components/ui/badge";
     4→import { cn } from "@/lib/utils";
     5→import { formatDistanceToNow } from "date-fns";
     6→
     7→interface ConversationPreviewProps {
     8→  id: string;
     9→  name: string;
    10→  avatar?: string;
    11→  lastMessage: string;
    12→  lastMessageTime: Date;
    13→  unreadCount?: number;
    14→  isOnline?: boolean;
    15→  onClick?: () => void;
    16→}
    17→
    18→export function ConversationPreview({
    19→  id,
    20→  name,
    21→  avatar,
    22→  lastMessage,
    23→  lastMessageTime,
    24→  unreadCount = 0,
    25→  isOnline = false,
    26→  onClick,
    27→}: ConversationPreviewProps) {
    28→  const initials = name
    29→    .split(" ")
    30→    .map((n) => n[0])
    31→    .join("")
    32→    .toUpperCase()
    33→    .slice(0, 2);
    34→
    35→  return (
    36→    <Card
    37→      className={cn(
    38→        "border-0 border-b rounded-none last:border-b-0",
    39→        "active:bg-accent transition-colors cursor-pointer"
    40→      )}
    41→      onClick={onClick}
    42→    >
    43→

    44→        {/* Avatar with online indicator */}
    45→

    46→          <Avatar className="h-14 w-14">
    47→            <AvatarImage src={avatar} alt={name} />
    48→            <AvatarFallback className="bg-primary/10 text-primary font-semibold">
    49→              {initials}
    50→            </AvatarFallback>
    51→          </Avatar>
    52→          {isOnline && (
    53→

    54→          )}
    55→

    56→
    57→        {/* Content */}
    58→

    59→

    60→
{name}

    61→
    62→              {formatDistanceToNow(lastMessageTime, { addSuffix: true })}
    63→
    64→

    65→

    66→            {lastMessage}
    67→

    68→

    69→
    70→        {/* Unread badge */}
    71→        {unreadCount > 0 && (
    72→          <Badge
    73→            variant="default"
    74→            className="shrink-0 h-6 min-w-6 px-2 rounded-full text-xs font-semibold"
    75→          >
    76→            {unreadCount > 99 ? "99+" : unreadCount}
    77→          </Badge>
    78→        )}
    79→

    80→    </Card>
    81→  );
    82→}

<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 { cn } from "@/lib/utils";
     2→
     3→interface MessageBubbleProps {
     4→  content: string;
     5→  isOwn?: boolean;
     6→  timestamp: Date;
     7→  status?: "sent" | "delivered" | "read";
     8→}
     9→
    10→export function MessageBubble({
    11→  content,
    12→  isOwn = false,
    13→  timestamp,
    14→  status = "delivered",
    15→}: MessageBubbleProps) {
    16→  const formatTime = (date: Date) => {
    17→    return date.toLocaleTimeString("en-AU", {
    18→      hour: "numeric",
    19→      minute: "2-digit",
    20→      hour12: true,
    21→    });
    22→  };
    23→
    24→  return (
    25→

    26→

    35→
{content}

    36→

    37→          {formatTime(timestamp)}
    38→          {isOwn && (
    39→
    40→              {status === "read" ? "✓✓" : "✓"}
    41→
    42→          )}
    43→

    44→

    45→

    46→  );
    47→}

<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, useCallback } from 'react';
     4→import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
     5→import { StripeProvider } from './StripeProvider';
     6→import { PaymentForm } from './PaymentForm';
     7→import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
     8→import { createClient } from '@/lib/supabase/client';
     9→
    10→interface HirePaymentModalProps {
    11→  isOpen: boolean;
    12→  onClose: () => void;
    13→  onSuccess: () => void;
    14→  matchId: string;
    15→  companyId: string;
    16→  amount?: number;
    17→  workerName?: string;
    18→}
    19→
    20→type PaymentState =
    21→  | { type: 'initial' }
    22→  | { type: 'loading' }
    23→  | { type: 'ready'; clientSecret: string; publishableKey: string }
    24→  | { type: 'processing' }
    25→  | { type: 'success' }
    26→  | { type: 'error'; message: string };
    27→
    28→export function HirePaymentModal({
    29→  isOpen,
    30→  onClose,
    31→  onSuccess,
    32→  matchId,
    33→  companyId,
    34→  amount = 50.0,
    35→  workerName,
    36→}: HirePaymentModalProps) {
    37→  const [paymentState, setPaymentState] = useState<PaymentState>({ type: 'initial' });
    38→  const supabase = createClient();
    39→
    40→  const handleOpen = useCallback(async () => {
    41→    if (paymentState.type !== 'initial') return;
    42→
    43→    setPaymentState({ type: 'loading' });
    44→
    45→    try {
    46→      const response = await fetch('/api/payments/create', {
    47→        method: 'POST',
    48→        headers: { 'Content-Type': 'application/json' },
    49→        body: JSON.stringify({
    50→          match_id: matchId,
    51→          company_id: companyId,
    52→          amount,
    53→        }),
    54→      });
    55→
    56→      const data = await response.json();
    57→
    58→      if (!response.ok) {
    59→        throw new Error(data.error || 'Failed to initialize payment');
    60→      }
    61→
    62→      if (!data.clientSecret || !data.publishableKey) {
    63→        throw new Error('Invalid payment configuration');
    64→      }
    65→
    66→      setPaymentState({
    67→        type: 'ready',
    68→        clientSecret: data.clientSecret,
    69→        publishableKey: data.publishableKey,
    70→      });
    71→    } catch (error) {
    72→      console.error('Payment initialization error:', error);
    73→      setPaymentState({
    74→        type: 'error',
    75→        message: error instanceof Error ? error.message : 'Failed to initialize payment',
    76→      });
    77→    }
    78→  }, [matchId, companyId, amount, paymentState.type]);
    79→
    80→  // Initialize payment when modal opens
    81→  if (isOpen && paymentState.type === 'initial') {
    82→    handleOpen();
    83→  }
    84→
    85→  const handlePaymentSuccess = () => {
    86→    setPaymentState({ type: 'success' });
    87→    // Call the onSuccess callback after a brief delay to show success state
    88→    setTimeout(() => {
    89→      onSuccess();
    90→      // Reset state for next time
    91→      setPaymentState({ type: 'initial' });
    92→    }, 2000);
    93→  };
    94→
    95→  const handleCancel = () => {
    96→    onClose();
    97→    // Reset state after a delay
    98→    setTimeout(() => setPaymentState({ type: 'initial' }), 300);
    99→  };
   100→
   101→  const renderContent = () => {
   102→    switch (paymentState.type) {
   103→      case 'initial':
   104→      case 'loading':
   105→        return (
   106→

   107→            <Loader2 className="h-8 w-8 animate-spin text-primary" />
   108→
Initializing payment...

   109→

   110→        );
   111→
   112→      case 'ready':
   113→        return (
   114→          <StripeProvider
   115→            publishableKey={paymentState.publishableKey}
   116→            clientSecret={paymentState.clientSecret}
   117→          >
   118→            <PaymentForm
   119→              onSuccess={handlePaymentSuccess}
   120→              onCancel={handleCancel}
   121→              amount={amount}
   122→            />
   123→          </StripeProvider>
   124→        );
   125→
   126→      case 'processing':
   127→        return (
   128→

   129→            <Loader2 className="h-8 w-8 animate-spin text-primary" />
   130→
Processing payment...

   131→

   132→        );
   133→
   134→      case 'success':
   135→        return (
   136→

   137→

   138→              <CheckCircle2 className="h-12 w-12 text-green-600" />
   139→

   140→
Payment Successful!

   141→

   142→              The worker has been notified of your hire.
   143→

   144→

   145→        );
   146→
   147→      case 'error':
   148→        return (
   149→

   150→

   151→              <AlertCircle className="h-12 w-12 text-destructive" />
   152→

   153→
Payment Failed

   154→

   155→              {paymentState.message}
   156→

   157→            <button
   158→              onClick={() => setPaymentState({ type: 'initial' })}
   159→              className="mt-4 text-primary hover:underline"
   160→            >
   161→              Try again
   162→            </button>
   163→

   164→        );
   165→
   166→      default:
   167→        return null;
   168→    }
   169→  };
   170→
   171→  return (
   172→    <Dialog open={isOpen} onOpenChange={(open) => !open && handleCancel()}>
   173→      <DialogContent className="sm:max-w-md">
   174→        <DialogHeader>
   175→          <DialogTitle>
   176→            {paymentState.type === 'success'
   177→              ? 'Hire Confirmed'
   178→              : `Hire${workerName ? ` ${workerName}` : ''}`}
   179→          </DialogTitle>
   180→          {paymentState.type !== 'success' && paymentState.type !== 'error' && (
   181→            <DialogDescription>
   182→              Complete payment to confirm your hire.
   183→            </DialogDescription>
   184→          )}
   185→        </DialogHeader>
   186→        {renderContent()}
   187→      </DialogContent>
   188→    </Dialog>
   189→  );
   190→}
   191→

<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 {
     5→  PaymentElement,
     6→  useStripe,
     7→  useElements,
     8→} from '@stripe/react-stripe-js';
     9→import { Button } from '@/components/ui/button';
    10→import { Loader2, CreditCard } from 'lucide-react';
    11→
    12→interface PaymentFormProps {
    13→  onSuccess: () => void;
    14→  onCancel: () => void;
    15→  amount: number;
    16→}
    17→
    18→export function PaymentForm({ onSuccess, onCancel, amount }: PaymentFormProps) {
    19→  const stripe = useStripe();
    20→  const elements = useElements();
    21→  const [isLoading, setIsLoading] = useState(false);
    22→  const [errorMessage, setErrorMessage] = useState<string | null>(null);
    23→
    24→  const handleSubmit = async (e: React.FormEvent) => {
    25→    e.preventDefault();
    26→
    27→    if (!stripe || !elements) {
    28→      return;
    29→    }
    30→
    31→    setIsLoading(true);
    32→    setErrorMessage(null);
    33→
    34→    try {
    35→      const { error, paymentIntent } = await stripe.confirmPayment({
    36→        elements,
    37→        confirmParams: {
    38→          return_url: `${window.location.origin}/contractor/payment-success`,
    39→        },
    40→        redirect: 'if_required',
    41→      });
    42→
    43→      if (error) {
    44→        setErrorMessage(error.message || 'Payment failed. Please try again.');
    45→      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
    46→        onSuccess();
    47→      } else if (paymentIntent && paymentIntent.status === 'requires_action') {
    48→        // 3D Secure or other authentication required - Stripe handles this automatically
    49→        // The redirect above will handle it
    50→      } else {
    51→        setErrorMessage('Payment processing. Please wait...');
    52→      }
    53→    } catch (error) {
    54→      console.error('Payment error:', error);
    55→      setErrorMessage('An unexpected error occurred. Please try again.');
    56→    } finally {
    57→      setIsLoading(false);
    58→    }
    59→  };
    60→
    61→  return (
    62→    <form onSubmit={handleSubmit} className="space-y-4">
    63→

    64→

    65→

    66→
Hire Fee

    67→
${amount.toFixed(2)} AUD

    68→

    69→          <CreditCard className="h-8 w-8 text-muted-foreground" />
    70→

    71→

    72→          One-time fee to hire this worker. No additional charges.
    73→

    74→

    75→
    76→

    77→        <PaymentElement
    78→          options={{
    79→            layout: {
    80→              type: 'tabs',
    81→              defaultCollapsed: false,
    82→            },
    83→          }}
    84→        />
    85→

    86→
    87→      {errorMessage && (
    88→

    89→          {errorMessage}
    90→

    91→      )}
    92→
    93→

    94→        <Button
    95→          type="button"
    96→          variant="outline"
    97→          onClick={onCancel}
    98→          disabled={isLoading}
    99→          className="flex-1"
   100→        >
   101→          Cancel
   102→        </Button>
   103→        <Button
   104→          type="submit"
   105→          disabled={!stripe || isLoading}
   106→          className="flex-1"
   107→        >
   108→          {isLoading ? (
   109→            <>
   110→              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
   111→              Processing...
   112→            </>
   113→          ) : (
   114→            `Pay $${amount.toFixed(2)}`
   115→          )}
   116→        </Button>
   117→

   118→
   119→

   120→        Secured by Stripe. We never store your card details.
   121→

   122→    </form>
   123→  );
   124→}
   125→

<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 { ReactNode } from 'react';
     4→import { loadStripe } from '@stripe/stripe-js';
     5→import { Elements } from '@stripe/react-stripe-js';
     6→
     7→// Load Stripe outside of component to avoid recreating on every render
     8→let stripePromise: ReturnType<typeof loadStripe> | null = null;
     9→
    10→export function getStripe(publishableKey: string) {
    11→  if (!stripePromise && publishableKey) {
    12→    stripePromise = loadStripe(publishableKey);
    13→  }
    14→  return stripePromise;
    15→}
    16→
    17→interface StripeProviderProps {
    18→  children: ReactNode;
    19→  publishableKey: string;
    20→  clientSecret: string;
    21→}
    22→
    23→export function StripeProvider({ children, publishableKey, clientSecret }: StripeProviderProps) {
    24→  const stripe = getStripe(publishableKey);
    25→
    26→  if (!stripe) {
    27→    return null;
    28→  }
    29→
    30→  const options = {
    31→    clientSecret,
    32→    appearance: {
    33→      theme: 'stripe' as const,
    34→      variables: {
    35→        colorPrimary: '#0f172a',
    36→        colorBackground: '#ffffff',
    37→        colorText: '#0f172a',
    38→        colorDanger: '#ef4444',
    39→        borderRadius: '8px',
    40→      },
    41→    },
    42→  };
    43→
    44→  return (
    45→    <Elements stripe={stripe} options={options}>
    46→      {children}
    47→    </Elements>
    48→  );
    49→}
    50→

<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 { AlertCircle, RefreshCw, Home, ArrowLeft } from "lucide-react";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     6→
     7→interface ErrorDisplayProps {
     8→  title?: string;
     9→  message?: string;
    10→  details?: string;
    11→  errorId?: string;
    12→  onRetry?: () => void;
    13→  onGoHome?: () => void;
    14→  onGoBack?: () => void;
    15→  showDetails?: boolean;
    16→  className?: string;
    17→}
    18→
    19→export function ErrorDisplay({
    20→  title = "Something went wrong",
    21→  message = "We apologize for the inconvenience. An unexpected error has occurred.",
    22→  details,
    23→  errorId,
    24→  onRetry,
    25→  onGoHome,
    26→  onGoBack,
    27→  showDetails = process.env.NODE_ENV === "development",
    28→  className = "",
    29→}: ErrorDisplayProps) {
    30→  return (
    31→    <div className={`min-h-[400px] flex items-center justify-center p-4 ${className}`}>
    32→      <Card className="w-full max-w-md">
    33→        <CardHeader className="text-center">
    34→

    35→            <AlertCircle className="h-6 w-6 text-destructive" />
    36→

    37→          <CardTitle className="text-xl">{title}</CardTitle>
    38→          <CardDescription className="mt-2">{message}</CardDescription>
    39→        </CardHeader>
    40→        <CardContent className="space-y-4">
    41→          {showDetails && (details || errorId) && (
    42→

    43→              {details && (
    44→                <>
    45→
Error details:

    46→
{details}

    47→                </>
    48→              )}
    49→              {errorId && (
    50→
Error ID: {errorId}

    51→              )}
    52→

    53→          )}
    54→
    55→

    56→            {onRetry && (
    57→              <Button onClick={onRetry} className="w-full gap-2">
    58→                <RefreshCw className="h-4 w-4" />
    59→                Try again
    60→              </Button>
    61→            )}
    62→
    63→            {onGoBack && (
    64→              <Button variant="outline" onClick={onGoBack} className="w-full gap-2">
    65→                <ArrowLeft className="h-4 w-4" />
    66→                Go back
    67→              </Button>
    68→            )}
    69→
    70→            {onGoHome && (
    71→              <Button variant="outline" onClick={onGoHome} className="w-full gap-2">
    72→                <Home className="h-4 w-4" />
    73→                Go to homepage
    74→              </Button>
    75→            )}
    76→

    77→
    78→

    79→            If the problem persists, please contact support.
    80→

    81→        </CardContent>
    82→      </Card>
    83→

    84→  );
    85→}

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