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

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

   350→    );
   351→  }
   352→
   353→  return (
   354→

   355→
   356→        {/* Progress bar */}
   357→        {step !== "generating" && step !== "review" && (
   358→

   359→            {Array.from({ length: totalSteps }, (_, i) => (
   360→              <div
   361→                key={i}
   362→                className={`h-1.5 flex-1 rounded-full transition-colors ${
   363→                  i <= currentIndex ? "bg-primary" : "bg-muted"
   364→                }`}
   365→              />
   366→            ))}
   367→

   368→        )}
   369→
   370→        {/* Step: Trade */}
   371→        {step === "trade" && (
   372→

   373→            <StepHeader
   374→              icon={<Wrench className="h-5 w-5" />}
   375→              title="What's your trade?"
   376→              subtitle="Pick your primary trade"
   377→            />
   378→
   379→            {/* Voice option */}
   380→            {!showVoice && (
   381→              <button
   382→                onClick={() => setShowVoice(true)}
   383→                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"
   384→                style={{ minHeight: "64px" }}
   385→              >
   386→                <Mic className="h-6 w-6" />
   387→                Or tell us with your voice
   388→              </button>
   389→            )}
   390→
   391→            {showVoice && (
   392→              <Card className="mt-6">
   393→                <CardContent className="flex flex-col items-center gap-4 pt-6">
   394→

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

   397→
   398→                  {voiceProcessing ? (
   399→

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

   402→

   403→                  ) : (
   404→                    <VoiceRecorder onRecordingComplete={handleVoiceComplete} />
   405→                  )}
   406→
   407→                  {voiceError && (
   408→
{voiceError}

   409→                  )}
   410→
   411→                  <button
   412→                    onClick={() => {
   413→                      setShowVoice(false);
   414→                      setVoiceError(null);
   415→                    }}
   416→                    className="text-sm text-muted-foreground hover:text-foreground"
   417→                  >
   418→                    Cancel — I'll tap instead
   419→                  </button>
   420→                </CardContent>
   421→              </Card>
   422→            )}
   423→
   424→

   425→              {TRADES.map((t) => (
   426→                <button
   427→                  key={t}
   428→                  onClick={() => {
   429→                    setTrade(t);
   430→                    setStep("experience");
   431→                  }}
   432→                  className={`rounded-xl border-2 px-4 py-4 text-left text-base font-medium transition-colors active:scale-95 ${
   433→                    trade === t
   434→                      ? "border-primary bg-primary/10 text-primary"
   435→                      : "border-border hover:border-primary/50"
   436→                  }`}
   437→                  style={{ minHeight: "64px" }}
   438→                >
   439→                  {t}
   440→                </button>
   441→              ))}
   442→

   443→

   444→        )}
   445→
   446→        {/* Step: Experience */}
   447→        {step === "experience" && (
   448→

   449→            <StepHeader
   450→              icon={<Clock className="h-5 w-5" />}
   451→              title="Your experience?"
   452→              subtitle="Years in construction"
   453→            />
   454→
   455→

   456→              {EXPERIENCE_OPTIONS.map((opt) => (
   457→                <button
   458→                  key={opt.value}
   459→                  onClick={() => {
   460→                    setExperience(opt.value);
   461→                    setStep("location");
   462→                  }}
   463→                  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 ${
   464→                    experience === opt.value
   465→                      ? "border-primary bg-primary/10 text-primary"
   466→                      : "border-border hover:border-primary/50"
   467→                  }`}
   468→                  style={{ minHeight: "64px" }}
   469→                >
   470→                  {opt.label}
   471→                  <ArrowRight className="h-5 w-5 text-muted-foreground" />
   472→                </button>
   473→              ))}
   474→

   475→
   476→            <button
   477→              onClick={() => setStep("trade")}
   478→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   479→            >
   480→              <ArrowLeft className="h-4 w-4" />
   481→              Back
   482→            </button>
   483→

   484→        )}
   485→
   486→        {/* Step: Location */}
   487→        {step === "location" && (
   488→

   489→            <StepHeader
   490→              icon={<MapPin className="h-5 w-5" />}
   491→              title="Where are you based?"
   492→              subtitle="Your suburb or area"
   493→            />
   494→
   495→            <form
   496→              onSubmit={(e) => {
   497→                e.preventDefault();
   498→                if (location.trim()) setStep("availability");
   499→              }}
   500→              className="mt-6 space-y-4"
   501→            >
   502→

   503→                Suburb or Area
   504→                <Input
   505→                  id="location"
   506→                  placeholder="e.g. Parramatta, Blacktown"
   507→                  value={location}
   508→                  onChange={(e) => setLocation(e.target.value)}
   509→                  required
   510→                  autoFocus
   511→                  className="h-14 text-base"
   512→                />
   513→

   514→

   515→                We'll match you to nearby jobs
   516→

   517→
   518→

   519→                <Button
   520→                  type="button"
   521→                  variant="outline"
   522→                  onClick={() => setStep("experience")}
   523→                  className="h-14 w-14 p-0"
   524→                >
   525→                  <ArrowLeft className="h-5 w-5" />
   526→                </Button>
   527→                <Button
   528→                  type="submit"
   529→                  disabled={!location.trim()}
   530→                  className="h-14 flex-1 gap-2 text-base font-semibold"
   531→                >
   532→                  Continue
   533→                  <ArrowRight className="h-5 w-5" />
   534→                </Button>
   535→

   536→            </form>
   537→

   538→        )}
   539→
   540→        {/* Step: Availability */}
   541→        {step === "availability" && (
   542→

   543→            <StepHeader
   544→              icon={<Calendar className="h-5 w-5" />}
   545→              title="Your availability?"
   546→              subtitle="When can you work?"
   547→            />
   548→
   549→

   550→              {AVAILABILITY_OPTIONS.map((opt) => (
   551→                <button
   552→                  key={opt.value}
   553→                  onClick={() => {
   554→                    setAvailability(opt.value);
   555→                    generateProfile();
   556→                  }}
   557→                  className={`flex w-full items-start justify-between rounded-xl border-2 px-5 py-4 text-left transition-colors active:scale-95 ${
   558→                    availability === opt.value
   559→                      ? "border-primary bg-primary/10"
   560→                      : "border-border hover:border-primary/50"
   561→                  }`}
   562→                  style={{ minHeight: "64px" }}
   563→                >
   564→

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

   568→
{opt.desc}

   569→

   570→                  <Zap className="h-5 w-5 shrink-0 text-primary" />
   571→                </button>
   572→              ))}
   573→

   574→
   575→            <button
   576→              onClick={() => setStep("location")}
   577→              className="mt-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
   578→            >
   579→              <ArrowLeft className="h-4 w-4" />
   580→              Back
   581→            </button>
   582→

   583→        )}
   584→
   585→        {/* Step: Generating */}
   586→        {step === "generating" && (
   587→

   588→

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

   591→
Building your profile...

   592→

   593→              AI is generating your bio, skills, and certifications
   594→

   595→

   596→

   597→

   598→

   599→

   600→

   601→        )}
   602→
   603→        {/* Step: Review */}
   604→        {step === "review" && (
   605→

   606→

   607→

   608→                <Sparkles className="h-5 w-5" />
   609→

   610→

   611→
Review & Edit

   612→

   613→                  AI generated this — customise as needed
   614→

   615→

   616→

   617→
   618→            {/* Your Answers Summary */}
   619→            <Card className="mt-6">
   620→              <CardContent className="space-y-3 pt-6">
   621→

   622→

   623→                    <Wrench className="h-4 w-4 text-muted-foreground" />
   624→                    {trade}
   625→

   626→                  <button
   627→                    onClick={() => setStep("trade")}
   628→                    className="text-xs text-primary hover:underline"
   629→                  >
   630→                    Change
   631→                  </button>
   632→

   633→

   634→

   635→                    <Clock className="h-4 w-4 text-muted-foreground" />
   636→
   637→                      {EXPERIENCE_OPTIONS.find((o) => o.value === experience)?.label}
   638→
   639→

   640→                  <button
   641→                    onClick={() => setStep("experience")}
   642→                    className="text-xs text-primary hover:underline"
   643→                  >
   644→                    Change
   645→                  </button>
   646→

   647→

   648→

   649→                    <MapPin className="h-4 w-4 text-muted-foreground" />
   650→                    {location}
   651→

   652→                  <button
   653→                    onClick={() => setStep("location")}
   654→                    className="text-xs text-primary hover:underline"
   655→                  >
   656→                    Change
   657→                  </button>
   658→

   659→              </CardContent>
   660→            </Card>
   661→
   662→            {/* Bio */}
   663→

   664→

   665→
   666→                  <Sparkles className="h-4 w-4 text-primary" />
   667→                  Your Bio
   668→
   669→                AI-generated
   670→

   671→              <textarea
   672→                value={editedBio}
   673→                onChange={(e) => setEditedBio(e.target.value)}
   674→                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"
   675→                placeholder="Your professional bio..."
   676→              />
   677→

   678→
   679→            {/* Skills */}
   680→

   681→

   682→
   683→                  <Zap className="h-4 w-4 text-primary" />
   684→                  Skills
   685→
   686→                Tap to remove
   687→

   688→

   689→                {editedSkills.map((skill, idx) => (
   690→                  <button
   691→                    key={idx}
   692→                    onClick={() => setEditedSkills(editedSkills.filter((_, i) => i !== idx))}
   693→                    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"
   694→                  >
   695→                    {skill} ×
   696→                  </button>
   697→                ))}
   698→

   699→

   700→
   701→            {/* Certifications */}
   702→

   703→

   704→
   705→                  <Shield className="h-4 w-4 text-primary" />
   706→                  Certifications
   707→
   708→                Tap to remove
   709→

   710→

   711→                {editedCerts.map((cert, idx) => (
   712→                  <button
   713→                    key={idx}
   714→                    onClick={() => setEditedCerts(editedCerts.filter((_, i) => i !== idx))}
   715→                    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"
   716→                  >
   717→                    {cert}
   718→                    Remove
   719→                  </button>
   720→                ))}
   721→

   722→

   723→

   724→        )}
   725→
   726→
   727→      {/* Bottom sticky CTA - only show on review step */}
   728→      {step === "review" && (
   729→

   730→

   731→            <Button
   732→              onClick={handleSave}
   733→              className="h-16 w-full gap-2 text-lg font-semibold"
   734→              disabled={saving}
   735→            >
   736→              {saving ? (
   737→                <>
   738→                  <Loader2 className="h-6 w-6 animate-spin" />
   739→                  Saving profile...
   740→                </>
   741→              ) : (
   742→                <>
   743→                  <CheckCircle2 className="h-6 w-6" />
   744→                  Save My Profile
   745→                </>
   746→              )}
   747→            </Button>
   748→

   749→

   750→      )}
   751→

   752→  );
   753→}
   754→
   755→function StepHeader({
   756→  icon,
   757→  title,
   758→  subtitle,
   759→}: {
   760→  icon: React.ReactNode;
   761→  title: string;
   762→  subtitle: string;
   763→}) {
   764→  return (
   765→

   766→

   767→        {icon}
   768→

   769→

   770→
{title}

   771→
{subtitle}

   772→

   773→

   774→  );
   775→}
   776→

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


Let me continue reading more pages:


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

    90→

    91→

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

   102→

   103→

   104→
Loading conversations...

   105→

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

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

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

   115→

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

   125→

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

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

   133→

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

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

   144→        ) : (
   145→

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

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

   165→  );
   166→}
   167→

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


<tool_use_error>File does not exist.</tool_use_error>


<tool_use_error>Sibling tool call errored</tool_use_error>


Let me check the actual page structure:


Bash

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


Let me use a different approach:


"use client";

import { useState, useRef, useEffect, use } from "react";
import { useRouter } from "next/navigation";
import { MessageBubble } from "@/components/MessageBubble";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ArrowLeft, Send, Phone, MoreVertical } from "lucide-react";
import { createClient } from "@/lib/supabase/client";

interface Message {
  id: string;
  content: string;
  isOwn: boolean;
  timestamp: Date;
  status?: "sent" | "delivered" | "read";
}

interface OtherParticipant {
  id: string;
  name: string;
  avatar: string | null;
  type: string;
  isOnline: boolean;
}

interface ConversationData {
  id: string;
  otherParticipant: OtherParticipant;
  messages: Message[];
}

export default function ConversationPage({ params }: { params: Promise<{ conversationId: string }> }) {
  const router = useRouter();
  const resolvedParams = use(params);
  const conversationId = resolvedParams.conversationId;

  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState<Message[]>([]);
  const [otherParticipant, setOtherParticipant] = useState<OtherParticipant | null>(null);
  const [loading, setLoading] = useState(true);
  const [sending, setSending] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const supabase = createClient();

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    fetchConversation();

    // Subscribe to new messages in this conversation
    const channel = supabase
      .channel(`conversation:${conversationId}`)
      .on('postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `conversation_id=eq.${conversationId}`
        },
        (payload) => {
          const newMessage = payload.new as any;
          // Only add if it's not from us (we already added it optimistically)
          supabase.auth.getUser().then(({ data: { user } }) => {
            if (newMessage.sender_id !== user?.id) {
              setMessages(prev => [...prev, {
                id: newMessage.id,
                content: newMessage.content,
                isOwn: false,
                timestamp: new Date(newMessage.created_at),
                status: newMessage.status
              }]);
            }
          });
        }
      )
      .subscribe();

    return () => {
      supabase.removeChannel(channel);
    };
  }, [conversationId]);

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const fetchConversation = async () => {
    try {
      const response = await fetch(`/api/messages/${conversationId}`);
      if (response.ok) {
        const data = await response.json();
        const conversation: ConversationData = data.conversation;
        setOtherParticipant(conversation.otherParticipant);
        setMessages(conversation.messages.map(msg => ({
          ...msg,
          timestamp: new Date(msg.timestamp)
        })));
      } else if (response.status === 404) {
        router.push('/messages');
      }
    } catch (error) {
      console.error("Error fetching conversation:", error);
    } finally {
      setLoading(false);
    }
  };

  const handleSendMessage = async (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim() && !sending) {
      setSending(true);
      const messageContent = message.trim();

      // Optimistic update
      const tempId = Date.now().toString();
      const newMessage: Message = {
        id: tempId,
        content: messageContent,
        isOwn: true,
        timestamp: new Date(),
        status: "sent"
      };
      setMessages(prev => [...prev, newMessage]);
      setMessage("");

      try {
        const response = await fetch(`/api/messages/${conversationId}/send`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ content: messageContent })
        });

        if (response.ok) {
          const data = await response.json();
          // Update the temp message with the real one
          setMessages(prev => prev.map(msg =>
            msg.id === tempId ? { ...data.message, timestamp: new Date(data.message.timestamp) } : msg
          ));
        } else {
          // Remove the optimistic message on error
          setMessages(prev => prev.filter(msg => msg.id !== tempId));
          setMessage(messageContent); // Restore the message
        }
      } catch (error) {
        console.error("Error sending message:", error);
        setMessages(prev => prev.filter(msg => msg.id !== tempId));
        setMessage(messageContent);
      } finally {
        setSending(false);
      }
    }
  };

  const initials = otherParticipant?.name
    ? otherParticipant.name
        .split(" ")
        .map((n) => n[0])
        .join("")
        .toUpperCase()
        .slice(0, 2)
    : "??";

  if (loading) {
    return (

              <Button
                variant="ghost"
                size="icon"
                onClick={() => router.push("/messages")}
                className="h-10 w-10"
              >
                <ArrowLeft className="h-5 w-5" />
              </Button>

Loading messages...

    );
  }

  return (

      {/* Header */}

            <Button
              variant="ghost"
              size="icon"
              onClick={() => router.push("/messages")}
              className="h-10 w-10"
            >
              <ArrowLeft className="h-5 w-5" />
            </Button>

              <Avatar className="h-10 w-10">
                <AvatarImage src={otherParticipant?.avatar || ""} alt={otherParticipant?.name || ""} />
                <AvatarFallback className="bg-primary/10 text-primary font-semibold">
                  {initials}
                </AvatarFallback>
              </Avatar>

{otherParticipant?.name || "Unknown"}

                  {otherParticipant?.isOnline ? "Online" : "Offline"}

            <Button variant="ghost" size="icon" className="h-10 w-10">
              <Phone className="h-5 w-5" />
            </Button>
            <Button variant="ghost" size="icon" className="h-10 w-10">
              <MoreVertical className="h-5 w-5" />
            </Button>

      {/* Messages */}

          {messages.length === 0 ? (

No messages yet. Say hello!

          ) : (

              {messages.map((msg) => (
                <MessageBubble
                  key={msg.id}
                  content={msg.content}
                  isOwn={msg.isOwn}
                  timestamp={msg.timestamp}
                  status={msg.status}
                />
              ))}

          )}

      {/* Message Input */}

          <form onSubmit={handleSendMessage} className="flex gap-3">
            <Input
              type="text"
              placeholder="Type a message..."
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              className="flex-1 h-12 text-base"
              disabled={sending}
            />
            <Button
              type="submit"
              size="icon"
              className="h-12 w-12 shrink-0"
              disabled={!message.trim() || sending}
            >
              <Send className="h-5 w-5" />
            </Button>
          </form>

  );
}


cat "/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/post-job/page.tsx" 2>/dev/null | head -100


"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import {
  ArrowLeft,
  ArrowRight,
  Loader2,
  Sparkles,
  Check,
  MapPin,
  Calendar,
  Users,
  DollarSign,
  HardHat,
  Wrench,
  Hammer,
  Blocks,
  Fence,
  CircleDot,
  Shovel,
} from "lucide-react";
import { jsonToPostgresPoint } from "@/lib/location-utils";

const TRADE_CATEGORIES = [
  { id: "steel-fixer", label: "Steel Fixer", icon: Wrench, color: "bg-blue-500/10 border-blue-500/30 text-blue-700" },
  { id: "formworker", label: "Formworker", icon: Blocks, color: "bg-amber-500/10 border-amber-500/30 text-amber-700" },
  { id: "labourer", label: "Labourer", icon: Shovel, color: "bg-green-500/10 border-green-500/30 text-green-700" },
  { id: "concreter", label: "Concreter", icon: CircleDot, color: "bg-gray-500/10 border-gray-500/30 text-gray-700" },
  { id: "carpenter", label: "Carpenter", icon: Hammer, color: "bg-orange-500/10 border-orange-500/30 text-orange-700" },
  { id: "scaffolder", label: "Scaffolder", icon: Fence, color: "bg-purple-500/10 border-purple-500/30 text-purple-700" },
  { id: "other", label: "Other", icon: HardHat, color: "bg-muted border-border text-foreground" },
];

const AI_SUGGESTIONS: Record<string, { title: string; description: string; certs: string[]; rateMin: number; rateMax: number; rateType: "hourly" | "daily" }> = {
  "steel-fixer": {
    title: "Steel Fixer Needed",
    description: "Experienced steel fixer required for commercial construction project. Must be able to read drawings, tie rebar, and work with mesh. PPE required on site at all times. Early starts — 6:30am.",
    certs: ["White Card", "Working at Heights"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "formworker": {
    title: "Formworker Required",
    description: "Skilled formworker needed for concrete pours on a residential build. Experience with timber and steel formwork systems. Must be comfortable with early starts and physical work.",
    certs: ["White Card"],
    rateMin: 42,
    rateMax: 58,
    rateType: "hourly",
  },
  "labourer": {
    title: "General Labourer Needed",
    description: "Reliable labourer needed for site clean-up, material handling, and general duties. No specific trade experience needed but must be fit and punctual. PPE and boots required.",
    certs: ["White Card"],
    rateMin: 32,
    rateMax: 42,
    rateType: "hourly",
  },
  "concreter": {
    title: "Concreter Required",
    description: "Experienced concreter needed for slab pours and finishing work. Must know how to screed, float, and finish concrete to a high standard. Early morning pours expected.",
    certs: ["White Card"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "carpenter": {
    title: "Carpenter Needed",
    description: "Qualified carpenter required for framing and fit-out work. Must have own basic tools. Experience with residential or commercial builds preferred.",
    certs: ["White Card", "Trade Certificate"],
    rateMin: 45,
    rateMax: 65,
    rateType: "hourly",
  },
  "scaffolder": {
    title: "Scaffolder Required",
    description: "Licensed scaffolder needed for erection and dismantling of scaffolding systems. Must hold current scaffolding licence. Safety-first attitude essential.",
    certs: ["White Card", "Scaffolding Licence", "Working at Heights"],
    rateMin: 48,
    rateMax: 68,
    rateType: "hourly",
  },
  "other": {
    title: "Construction Worker Needed",
    description: "Worker needed for construction project. Please specify the role and requirements in the description.",
    certs: ["White Card"],
    rateMin: 35,
    rateMax: 55,
    rateType: "hourly",
  },
};


cat "/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/matches/page.tsx" 2>/dev/null | head -100


"use client";

import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { BottomNav } from "@/components/bottom-nav";
import { HirePaymentModal } from "@/components/payment/HirePaymentModal";
import {
  ArrowLeft,
  Loader2,
  Star,
  MapPin,
  Clock,
  CheckCircle2,
  UserCheck,
  HardHat,
  AlertCircle,
} from "lucide-react";
import type { Job } from "@/lib/types";
import { postgresPointToJson } from "@/lib/location-utils";

interface MatchWithWorker {
  id: string;
  job_id: string;
  worker_id: string;
  status: string;
  match_score: number | null;
  ai_summary: string | null;
  contractor_action: string;
  worker_action: string;
  hired_at: string | null;
  fee_charged: boolean;
  created_at: string;
  worker: {
    id: string;
    name: string | null;
    suburb: string | null;
    avatar_url: string | null;
  } | null;
  worker_profile: {
    trades: string[];
    experience_years: number | null;
    certifications: string[];
    availability: string | null;
  } | null;
}

interface SwipeableMatchCardProps {
  match: MatchWithWorker;
  isHired: boolean;
  justHired: boolean;
  hiring: boolean;
  onHire: (match: MatchWithWorker) => void;
  onReject: (match: MatchWithWorker) => void;
}

function SwipeableMatchCard({ match, isHired, justHired, hiring, onHire, onReject }: SwipeableMatchCardProps) {
  const [swipeX, setSwipeX] = useState(0);
  const [swiping, setSwiping] = useState(false);
  const startX = useRef(0);
  const currentX = useRef(0);

  const summary = match.ai_summary || generateAiSummary(match.worker, match.worker_profile);

  const handleTouchStart = (e: React.TouchEvent) => {
    if (isHired) return;
    startX.current = e.touches[0].clientX;
    setSwiping(true);
  };

  const handleTouchMove = (e: React.TouchEvent) => {
    if (!swiping || isHired) return;
    currentX.current = e.touches[0].clientX;
    const diff = currentX.current - startX.current;
    // Allow both directions
    setSwipeX(Math.max(-150, Math.min(diff, 150)));
  };

  const handleTouchEnd = () => {
    if (!swiping || isHired) return;
    setSwiping(false);

    // If swiped right > 100px, hire
    if (swipeX > 100) {
      onHire(match);
    }
    // If swiped left > 100px, reject
    else if (swipeX < -100) {
      onReject(match);
    }

    // Reset
    setSwipeX(0);
  };

  const swipeProgress = Math.abs(swipeX) / 100;
  const swipeDirection = swipeX > 0 ? "right" : "left";


cat "/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/signup/page.tsx" 2>/dev/null | head -100


"use client";

import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { createClient } from "@/lib/supabase/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import {
  Building2,
  Search,
  Sparkles,
  CheckCircle2,
  ArrowLeft,
  ArrowRight,
  Loader2,
  Edit3,
  Globe,
  MapPin,
  Users,
  FileText,
} from "lucide-react";
import { guessCompanyDomain } from "@/lib/company-utils";
import { validateABN, formatABN } from "@/lib/abn-utils";

type Step = "auth" | "company" | "researching" | "review";

interface CompanyData {
  name: string;
  abn: string;
  address: string;
  industry: string;
  size: string;
  website: string;
  logoUrl?: string | null;
  logoSource?: 'clearbit' | 'google' | 'none';
}

async function lookupABN(abn: string): Promise<CompanyData> {
  // Clean the ABN - remove spaces
  const cleanAbn = abn.replace(/\s/g, '');

  // Call the ABN lookup API
  const response = await fetch(`/api/abn-lookup?abn=${cleanAbn}`);

  if (!response.ok) {
    const errData = await response.json().catch(() => null);
    throw new Error(errData?.error || 'Failed to lookup ABN');
  }

  const result = await response.json();

  if (!result.success || !result.data) {
    throw new Error(result.error || 'Unable to lookup ABN');
  }

  const data = result.data;

  // Map the ABR data to our CompanyData format
  const companyData: CompanyData = {
    name: data.name || 'Unknown Company',
    abn: formatABN(data.abn || cleanAbn),
    address: [data.postcode, data.state].filter(Boolean).join(' ') || 'Address not available',
    industry: 'Construction',
    size: 'Unknown',
    website: '',
  };

  // Try to find company logo
  await fetchCompanyLogo(companyData);

  return companyData;
}

async function fetchCompanyLogo(companyData: CompanyData): Promise<void> {
  try {
    // If we have an explicit website, try that first
    if (companyData.website) {
      const domain = extractDomainFromUrl(companyData.website);
      if (domain) {
        const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
        if (logoResponse.ok) {
          const logoData = await logoResponse.json();
          companyData.logoUrl = logoData.logoUrl;
          companyData.logoSource = logoData.source;
          return;
        }
      }
    }

    // Otherwise, try to guess domains from company name
    const possibleDomains = guessCompanyDomain(companyData.name);

    // Try each domain until we find a logo
    for (const domain of possibleDomains) {
      const logoResponse = await fetch(`/api/company-logo?domain=${domain}`);
      if (logoResponse.ok) {


Now let me check for error handling and state handling files:


grep -r "TODO\|FIXME\|STUB\|XXX\|HACK" /home/ccuser/the-50-dollar-app/src --include="*.tsx" --include="*.ts" 2>/dev/null | head -30


/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts: * ABN Lookup API Route — GET /api/abn-lookup?abn=XXXXXXXXXXX
/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts:  // TODO: Implement ASIC Connect API integration
/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts:          isOnline: true // TODO: Implement online status
/home/ccuser/the-50-dollar-app/src/lib/abn-utils.ts: * Format an 11-digit ABN into the standard display format: XX XXX XXX XXX


import { redirect } from "next/navigation";

export default function WorkerProfilePage() {
  redirect("/worker/profile/build");
}


Let me check all error handling and loading pages:


import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';

export default function DashboardLoading() {
  return (

        {/* Page header skeleton */}

          <Skeleton className="h-8 w-48" />
          <Skeleton className="h-5 w-64" />

        {/* Stats cards skeleton */}

          {[...Array(2)].map((_, i) => (
            <Card key={i}>
              <CardContent className="flex flex-col items-center p-4">
                <Skeleton className="h-8 w-12" />
                <Skeleton className="h-3 w-20 mt-2" />
              </CardContent>
            </Card>
          ))}

        {/* CTA card skeleton */}
        <Card>
          <CardHeader className="pb-3">
            <Skeleton className="h-6 w-32" />
            <Skeleton className="h-4 w-48 mt-1" />
          </CardHeader>
          <CardContent>
            <Skeleton className="h-16 w-full" />
          </CardContent>
        </Card>

        {/* Secondary card skeleton */}
        <Card>
          <CardHeader className="pb-3">
            <Skeleton className="h-6 w-40" />
            <Skeleton className="h-4 w-56 mt-1" />
          </CardHeader>
          <CardContent>
            <Skeleton className="h-14 w-full" />
          </CardContent>
        </Card>

  );
}


'use client';

import { useEffect } from 'react';
import { ErrorDisplay } from '@/components/error-display';

export default function AuthenticatedError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error('Authenticated route error boundary caught:', error);
  }, [error]);

  // Check if it's an authentication error
  const isAuthError = error.message?.includes('authentication') ||
                      error.message?.includes('unauthorized') ||
                      error.message?.includes('login');

  return (
    <div className="min-h-screen bg-background flex items-center justify-center p-4">
      <ErrorDisplay
        error={error}
        onRetry={isAuthError ? undefined : reset}
        showContactSupport={true}
        variant={isAuthError ? 'authentication' : 'default'}
      />
    </div>
  );
}


'use client';

import { useEffect } from 'react';
import { ErrorDisplay } from '@/components/error-display';

export default function AuthError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error('Auth error:', error);
  }, [error]);

  return (
    <div className="min-h-screen bg-background flex items-center justify-center p-4">
      <ErrorDisplay
        title="Authentication error"
        message="We ran into a problem. Please try signing in again."
        error={error}
        onRetry={reset}
        onGoHome={() => window.location.href = '/auth/login'}
      />
    </div>
  );
}


Now let me check the layout files to understand responsive design:


import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "react-hot-toast";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "RateRight — Hire Construction Workers Fast",
  description:
    "The simplest way to hire construction workers in Australia. AI does the work, you just tap. $50 flat fee per hire.",
  keywords: [
    "construction hiring",
    "hire labourers",
    "construction workers Australia",
    "hire tradies",
    "construction jobs Sydney",
  ],
  manifest: "/manifest.json",
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: "RateRight",
  },
  other: {
    "mobile-web-app-capable": "yes",
    "apple-mobile-web-app-capable": "yes",
    "application-name": "RateRight",
    "msapplication-starturl": "/",
    "msapplication-TileColor": "#2563eb",
    "theme-color": "#2563eb",
  },
};

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  maximumScale: 1,
  themeColor: "#2563eb",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en-AU">
      <head>
        <link rel="manifest" href="/manifest.json" />
        <link rel="icon" href="/favicon.ico" sizes="any" />
        <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
        <link rel="apple-touch-icon" href="/icon-144x144.png" />
        <meta name="apple-mobile-web-app-status-bar-style" content="default" />
        <meta name="apple-mobile-web-app-title" content="RateRight" />
        <meta name="mobile-web-app-capable" content="yes" />
        <meta name="apple-mobile-web-app-capable" content="yes" />
        <meta name="application-name" content="RateRight" />
        <meta name="msapplication-starturl" content="/" />
        <meta name="msapplication-TileColor" content="#2563eb" />
        <meta name="theme-color" content="#2563eb" />
      </head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
      >
        {children}
        <Toaster position="top-right" />
        <script
          dangerouslySetInnerHTML={{
            __html: `
              if ('serviceWorker' in navigator) {
                window.addEventListener('load', () => {
                  navigator.serviceWorker.register('/sw.js').then(
                    (registration) => {
                      console.log('SW registered:', registration);
                    },
                    (err) => {
                      console.log('SW registration failed:', err);
                    }
                  );
                });
              }
            `,
          }}
        />
      </body>
    </html>
  );
}


grep -n "className" "/home/ccuser/the-50-dollar-app/src/app/(authenticated)/worker/jobs/page.tsx" | head -20


96:

100:          className="absolute inset-0 rounded-xl flex items-center justify-start pl-6 pointer-events-none"
106:          <Check className="h-8 w-8 text-green-600" />
107:
114:        className="cursor-pointer active:scale-[0.98] transition-transform touch-pan-y"
125:        <CardContent className="p-5">
126:

127:

128:

129:                <Badge variant="secondary" className="text-sm shrink-0 py-1 px-3">
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" />
139:
{job.title}

143:

145:
146:                <MapPin className="h-4 w-4" />
151:
152:                <DollarSign className="h-4 w-4" />
162:
163:                <Calendar className="h-4 w-4" />


Let me check for form validation:


grep -n "minLength\|required\|validate\|pattern\|error" "/home/ccuser/the-50-dollar-app/src/app/auth/signup/page.tsx" | head -15


18:  const [error, setError] = useState<string | null>(null);
37:    const { error } = await supabase.auth.signUp({
46:    if (error) {
47:      setError(error.message);
76:                  required
88:                  required
90:                  minLength={6}
142:            {error && (
144:
{error}


505 /home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/post-job/page.tsx


cat "/home/ccuser/the-50-dollar-app/src/app/(authenticated)/contractor/post-job/page.tsx" | tail -300 | head -200


What trade do you need?

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

              {TRADE_CATEGORIES.map((trade) => {
                const Icon = trade.icon;
                return (
                  <button
                    key={trade.id}
                    onClick={() => handleTradeSelect(trade.id)}
                    className={`flex flex-col items-center justify-center gap-3 rounded-2xl border-2 p-6 min-h-[120px] text-center transition-all active:scale-95 ${trade.color}`}
                  >
                    <Icon className="h-10 w-10" />
                    {trade.label}
                  </button>
                );
              })}

        )}

        {/* STEP 2: AI-filled Job Details + Location/Date/Workers (COMBINED) */}
        {step === 2 && (

            {aiLoading ? (

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

AI writing your job post...

                  Based on market rates and common requirements

            ) : (

                  <Sparkles className="h-5 w-5 text-primary" />

Job Details

                  AI pre-filled these. Edit anything you like.

                  {/* Job Details Section */}

                    Job Title
                    <Input
                      id="title"
                      value={title}
                      onChange={(e) => setTitle(e.target.value)}
                      className="mt-2 h-14 text-base"
                    />

                    Description
                    <textarea
                      id="description"
                      value={description}
                      onChange={(e) => setDescription(e.target.value)}
                      rows={5}
                      className="mt-2 flex w-full rounded-md border border-input bg-background px-4 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
                    />

                    Pay Rate (AUD)

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

                      to

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

                      <Button
                        type="button"
                        variant={rateType === "hourly" ? "default" : "outline"}
                        onClick={() => setRateType("hourly")}
                        className="h-12 text-base font-semibold"
                      >
                        Per Hour
                      </Button>
                      <Button
                        type="button"
                        variant={rateType === "daily" ? "default" : "outline"}
                        onClick={() => setRateType("daily")}
                        className="h-12 text-base font-semibold"
                      >
                        Per Day
                      </Button>

                  {/* Location Section */}

                      <MapPin className="h-5 w-5" />
                      Location

                        Site Address
                        <Input
                          id="siteAddress"
                          value={siteAddress}
                          onChange={(e) => setSiteAddress(e.target.value)}
                          placeholder="e.g. 123 George St, Sydney"
                          className="mt-2 h-14 text-base"
                        />

                        Suburb
                        <Input
                          id="suburb"
                          value={suburb}
                          onChange={(e) => setSuburb(e.target.value)}
                          placeholder="e.g. Parramatta"
                          className="mt-2 h-14 text-base"
                        />

                  {/* Date & Workers Section */}

                      <Calendar className="h-5 w-5" />
                      Schedule

                        Start Date
                        <Input
                          id="startDate"
                          type="date"
                          value={startDate}
                          onChange={(e) => setStartDate(e.target.value)}
                          className="mt-2 h-14 text-base"
                        />

                        Duration
                        <Input
                          id="duration"
                          value={duration}
                          onChange={(e) => setDuration(e.target.value)}
                          placeholder="e.g. 2 weeks, ongoing"
                          className="mt-2 h-14 text-base"
                        />

                          <Users className="h-5 w-5" />
                          Workers Needed

                          <Button
                            type="button"
                            variant="outline"
                            onClick={() =>
                              setWorkersNeeded(String(Math.max(1, parseInt(workersNeeded) - 1)))
                            }
                            className="h-14 w-14 text-2xl font-bold"
                          >
                            -
                          </Button>

                            {workersNeeded}

                          <Button
                            type="button"
                            variant="outline"
                            onClick={() =>
                              setWorkersNeeded(String(parseInt(workersNeeded) + 1))


Perfect! Now let me create a comprehensive report. Let me check a couple more things:


'use client';

import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { createClient } from '@/lib/supabase/client';

export default function PaymentSuccessPage() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
  const [message, setMessage] = useState('Confirming your payment...');
  const supabase = createClient();

  useEffect(() => {
    const paymentIntent = searchParams.get('payment_intent');
    const paymentIntentClientSecret = searchParams.get('payment_intent_client_secret');

    if (!paymentIntent || !paymentIntentClientSecret) {
      setStatus('error');
      setMessage('Invalid payment confirmation');
      return;
    }

    // Check payment status
    async function checkPayment() {
      try {
        // Wait a moment for webhook to process
        await new Promise(resolve => setTimeout(resolve, 2000));

        // Check if payment was recorded
        const { data: payment } = await supabase
          .from('payments')
          .select('status, match_id')
          .eq('stripe_payment_id', paymentIntent)
          .single();

        if (payment?.status === 'charged') {
          setStatus('success');
          setMessage('Your payment was successful and the worker has been notified!');
        } else {
          // Payment might still be processing via webhook
          setStatus('success');
          setMessage('Payment received! Processing your hire...');
        }
      } catch (error) {
        console.error('Error checking payment:', error);
        setStatus('success'); // Assume success since Stripe redirected here
        setMessage('Payment received! Processing your hire...');
      }
    }

    checkPayment();
  }, [searchParams, supabase]);

  return (

      <Card className="w-full max-w-md">
        <CardContent className="pt-8 pb-6 text-center">
          {status === 'loading' && (
            <>
              <Loader2 className="mx-auto h-12 w-12 animate-spin text-primary" />

Confirming Payment

{message}

            </>
          )}

          {status === 'success' && (
            <>

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

Payment Successful!

{message}

                <Button asChild className="w-full h-12 text-base">
                  <Link href="/contractor/matches">Back to Matches</Link>
                </Button>
                <Button asChild variant="outline" className="w-full h-12 text-base">
                  <Link href="/dashboard">Go to Dashboard</Link>
                </Button>

            </>
          )}

          {status === 'error' && (
            <>

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

Payment Error

{message}

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

            </>
          )}
        </CardContent>
      </Card>

  );
}


grep -r "max-w-screen" /home/ccuser/the-50-dollar-app/src --include="*.tsx" | head -5


/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx:

/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx:

/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx:

/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx:

/home/ccuser/the-50-dollar-app/src/app/faq/page.tsx: