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→import { Card, CardContent, CardHeader } from '@/components/ui/card';
     2→import { Skeleton } from '@/components/ui/skeleton';
     3→
     4→export default function MessagesLoading() {
     5→  return (
     6→

     7→

     8→        {/* Conversations list skeleton */}
     9→        <Card className="lg:col-span-1">
    10→          <CardHeader>
    11→            <Skeleton className="h-6 w-32" />
    12→          </CardHeader>
    13→          <CardContent className="p-0">
    14→

    15→              {[...Array(8)].map((_, i) => (
    16→

    17→

    18→                    <Skeleton className="h-12 w-12 rounded-full" />
    19→

    20→

    21→                        <Skeleton className="h-4 w-24" />
    22→                        <Skeleton className="h-3 w-12" />
    23→

    24→                      <Skeleton className="h-3 w-full" />
    25→                      <Skeleton className="h-3 w-2/3" />
    26→

    27→

    28→

    29→              ))}
    30→

    31→          </CardContent>
    32→        </Card>
    33→
    34→        {/* Message area skeleton */}
    35→        <Card className="lg:col-span-2">
    36→          <CardHeader>
    37→

    38→              <Skeleton className="h-10 w-10 rounded-full" />
    39→

    40→                <Skeleton className="h-4 w-32" />
    41→                <Skeleton className="h-3 w-24" />
    42→

    43→

    44→          </CardHeader>
    45→          <CardContent className="flex flex-col h-[calc(100%-5rem)]">
    46→            {/* Messages skeleton */}
    47→

    48→              {[...Array(6)].map((_, i) => (
    49→                <div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
    50→                  <div className={`max-w-[70%] ${i % 2 === 0 ? 'items-start' : 'items-end'} space-y-2`}>
    51→                    <div className={`rounded-lg p-3 ${i % 2 === 0 ? 'bg-muted' : 'bg-primary/10'}`}>
    52→                      <Skeleton className={`h-4 ${i % 3 === 0 ? 'w-48' : i % 3 === 1 ? 'w-32' : 'w-64'}`} />
    53→

    54→                    <Skeleton className="h-2 w-12" />
    55→

    56→

    57→              ))}
    58→

    59→
    60→            {/* Message input skeleton */}
    61→

    62→

    63→                <Skeleton className="h-10 flex-1" />
    64→                <Skeleton className="h-10 w-20" />
    65→

    66→

    67→          </CardContent>
    68→        </Card>
    69→

    70→

    71→  );
    72→}

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


Now let me read the remaining files:


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→import Link from 'next/link';
     2→import { Home, MessageSquare, ArrowLeft } from 'lucide-react';
     3→import { Button } from '@/components/ui/button';
     4→import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
     5→
     6→export default function NotFound() {
     7→  return (
     8→

     9→

    10→        <Card className="border-muted-foreground/20">
    11→          <CardHeader className="text-center">
    12→

    13→              <MessageSquare className="h-10 w-10 text-primary" />
    14→

    15→            <CardTitle className="text-2xl">Conversation not found</CardTitle>
    16→            <CardDescription className="mt-2">
    17→              Sorry, we couldn't find the conversation you're looking for.
    18→            </CardDescription>
    19→          </CardHeader>
    20→
    21→          <CardContent className="space-y-4">
    22→

    23→

    24→                The conversation may have been deleted or you don't have access to it.
    25→

    26→

    27→
    28→

    29→

    30→                Need to send a message?

    31→                Head to your messages to start a new conversation.
    32→

    33→

    34→          </CardContent>
    35→
    36→          <CardFooter className="flex flex-col gap-3">
    37→            <Button asChild className="w-full">
    38→              <Link href="/messages">
    39→                <MessageSquare className="mr-2 h-4 w-4" />
    40→                View all messages
    41→              </Link>
    42→            </Button>
    43→
    44→            <Button
    45→              onClick={() => window.history.back()}
    46→              className="w-full"
    47→              variant="outline"
    48→            >
    49→              <ArrowLeft className="mr-2 h-4 w-4" />
    50→              Go back
    51→            </Button>
    52→          </CardFooter>
    53→        </Card>
    54→
    55→
    56→

    57→            RateRight — Hire Construction Workers Fast
    58→

    59→
    60→

    61→

    62→  );
    63→}

<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 Link from 'next/link';
     2→import { Home, User, ArrowLeft, Wrench } from 'lucide-react';
     3→import { Button } from '@/components/ui/button';
     4→import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
     5→
     6→export default function NotFound() {
     7→  return (
     8→

     9→

    10→        <Card className="border-muted-foreground/20">
    11→          <CardHeader className="text-center">
    12→

    13→              <User className="h-10 w-10 text-primary" />
    14→

    15→            <CardTitle className="text-2xl">Profile page not found</CardTitle>
    16→            <CardDescription className="mt-2">
    17→              Sorry, we couldn't find the profile you're looking for.
    18→            </CardDescription>
    19→          </CardHeader>
    20→
    21→          <CardContent className="space-y-4">
    22→

    23→

    24→                The profile may not exist or you don't have access to view it.
    25→

    26→

    27→
    28→

    29→

    30→                Looking to build your profile?

    31→                Create or update your worker profile to showcase your skills.
    32→

    33→

    34→          </CardContent>
    35→
    36→          <CardFooter className="flex flex-col gap-3">
    37→            <Button asChild className="w-full">
    38→              <Link href="/worker/profile">
    39→                <User className="mr-2 h-4 w-4" />
    40→                View profile
    41→              </Link>
    42→            </Button>
    43→
    44→            <Button asChild className="w-full" variant="outline">
    45→              <Link href="/worker/jobs">
    46→                <Wrench className="mr-2 h-4 w-4" />
    47→                Browse jobs
    48→              </Link>
    49→            </Button>
    50→
    51→            <Button
    52→              onClick={() => window.history.back()}
    53→              className="w-full"
    54→              variant="ghost"
    55→            >
    56→              <ArrowLeft className="mr-2 h-4 w-4" />
    57→              Go back
    58→            </Button>
    59→          </CardFooter>
    60→        </Card>
    61→
    62→
    63→

    64→            RateRight — Hire Construction Workers Fast
    65→

    66→
    67→

    68→

    69→  );
    70→}

<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 } from "react";
     4→import { Button } from "@/components/ui/button";
     5→import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
     6→import { AlertCircle, RefreshCw, ArrowLeft, HardHat } from "lucide-react";
     7→import Link from "next/link";
     8→
     9→export default function Error({
    10→  error,
    11→  reset,
    12→}: {
    13→  error: Error & { digest?: string };
    14→  reset: () => void;
    15→}) {
    16→  useEffect(() => {
    17→    // Log the error to an error reporting service
    18→    console.error("Worker profile error:", error);
    19→  }, [error]);
    20→
    21→  return (
    22→

    23→      {/* Header */}
    24→
    25→

    26→          <Link href="/dashboard" className="flex items-center gap-2">
    27→

    28→              <HardHat className="h-5 w-5 text-primary-foreground" />
    29→

    30→            RateRight
    31→          </Link>
    32→

    33→
    34→
    35→
    36→        <Card className="mt-8">
    37→          <CardHeader className="text-center">
    38→

    39→              <AlertCircle className="h-6 w-6 text-destructive" />
    40→

    41→            <CardTitle className="text-xl">Profile Build Error</CardTitle>
    42→            <CardDescription className="mt-2">
    43→              We couldn't load the profile builder. This might be a temporary issue.
    44→            </CardDescription>
    45→          </CardHeader>
    46→          <CardContent className="space-y-4">
    47→            {process.env.NODE_ENV === "development" && (
    48→

    49→
Error details:

    50→
{error.message}

    51→                {error.digest && (
    52→
Error ID: {error.digest}

    53→                )}
    54→

    55→            )}
    56→
    57→

    58→              <Button onClick={reset} className="w-full gap-2">
    59→                <RefreshCw className="h-4 w-4" />
    60→                Try again
    61→              </Button>
    62→
    63→              <Link href="/dashboard" className="w-full">
    64→                <Button variant="outline" className="w-full gap-2">
    65→                  <ArrowLeft className="h-4 w-4" />
    66→                  Back to Dashboard
    67→                </Button>
    68→              </Link>
    69→

    70→          </CardContent>
    71→        </Card>
    72→
    73→

    74→  );
    75→}

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


Perfect! I have now read all the page.tsx, layout.tsx, loading.tsx, error.tsx, and not-found.tsx files. Let me compile a comprehensive audit report.

## UI Consistency Audit Report: RateRight v2

I have completed a thorough audit of all page/layout/loading/error/not-found files. Here's my comprehensive analysis:

---

### FILE SUMMARY

**Total Files Analyzed: 29**

1. **Root Layout** - `/src/app/layout.tsx`
2. **Root Page** - `/src/app/page.tsx`
3. **Root Error/NotFound** - `/src/app/error.tsx`, `/src/app/not-found.tsx`
4. **Auth Pages** - signup, login, verify-email
5. **Public Pages** - FAQ, offline
6. **Authenticated Layout** - `/src/app/(authenticated)/layout.tsx`
7. **Authenticated Errors** - error.tsx (authenticated group)
8. **Dashboard** - `/src/app/(authenticated)/dashboard/page.tsx`, `loading.tsx`
9. **Contractor Pages** - signup, post-job, matches, payment-success, loading.tsx
10. **Worker Pages** - signup, jobs, jobs/loading.tsx, profile/build, profile/error.tsx, not-found.tsx
11. **Messages** - page.tsx, loading.tsx, [conversationId]/page.tsx, [conversationId]/not-found.tsx

---

### TAILWIND CSS CLASSES USED

#### **Color Classes (Primary/Brand)**
- Primary: `text-primary`, `bg-primary`, `border-primary`, `hover:text-primary`, `text-primary-foreground`, `bg-primary/5`, `bg-primary/10`, `border-primary/30`, `border-primary/20`, `text-primary/foreground`
- Brand Orange: `bg-brand-orange`
- Muted: `text-muted-foreground`, `bg-muted`, `border-muted`, `hover:bg-muted`
- Foreground/Background: `text-foreground`, `bg-background`, `text-background`
- Success/Green: `bg-green-500/10`, `text-green-600`, `text-green-700`, `bg-green-50`, `bg-green-100`, `text-green-800`
- Destructive: `text-destructive`, `bg-destructive/10`
- Accent: `bg-accent/50`, `text-accent-foreground`

#### **Typography Classes**
- Font sizes: `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `text-3xl`, `text-[2rem]`, `text-5xl`, `text-6xl`
- Font weights: `font-bold`, `font-semibold`, `font-medium`, `font-normal`, `font-extrabold`
- Other: `tracking-tight`, `leading-relaxed`, `leading-[1.1]`, `line-clamp-2`, `truncate`

#### **Spacing Classes**
- Padding: `p-2`, `p-3`, `p-4`, `p-5`, `p-6`, `p-8`, `px-3`, `px-4`, `px-5`, `px-6`, `py-2`, `py-3`, `py-4`, `py-6`, `py-8`, `py-12`, `py-14`, `py-16`
- Margin: `m-0`, `m-1`, `m-2`, `m-3`, `m-4`, `m-6`, `mx-auto`, `mx-0.5`, `mb-2`, `mb-3`, `mb-4`, `mb-6`, `mt-1`, `mt-2`, `mt-3`, `mt-4`, `mt-6`, `mt-8`
- Gap: `gap-1`, `gap-1.5`, `gap-2`, `gap-3`, `gap-4`, `gap-6`
- Space: `space-y-2`, `space-y-3`, `space-y-4`, `space-y-6`, `space-x-4`

#### **Layout & Display**
- Display: `flex`, `grid`
- Flex props: `flex-col`, `flex-row`, `flex-1`, `flex-wrap`, `justify-center`, `justify-between`, `justify-around`, `items-center`, `items-start`, `items-end`
- Grid props: `grid-cols-2`, `grid-cols-4`, `gap-3`, `gap-4`
- Width: `w-full`, `w-auto`, `w-1/2`, `w-20`, `w-24`, `w-32`, `max-w-md`, `max-w-sm`, `max-w-screen-sm`, `max-w-screen-xl`, `min-w-0`
- Height: `h-1`, `h-1.5`, `h-2`, `h-4`, `h-5`, `h-6`, `h-8`, `h-10`, `h-12`, `h-14`, `h-16`, `h-20`, `min-h-screen`

#### **Border & Rounded**
- Border: `border`, `border-b`, `border-t`, `border-2`, `border-dashed`, `border-muted`, `border-primary`, `border-border`, `border-muted-foreground/20`
- Rounded: `rounded`, `rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-full`, `rounded-t-3xl`

#### **Interactive/State**
- Hover: `hover:text-foreground`, `hover:text-primary`, `hover:bg-primary`, `hover:bg-muted`, `hover:border-primary/50`, `hover:no-underline`, `hover:text-primary/80`
- Active: `active:scale-95`, `active:bg-muted`
- Opacity: `opacity-50`, `opacity-60`, `opacity-80`
- Transitions: `transition-colors`, `transition-all`, `transition`
- Focus: `focus:border-primary`, `focus:outline-none`, `focus:ring-2`

#### **Other Utility Classes**
- Animations: `animate-spin`, `animate-pulse`, `animate-in`, `fade-in`, `slide-in-from-bottom-2`, `animate-bounce`
- Positioning: `fixed`, `sticky`, `absolute`, `relative`, `z-50`, `z-40`, `z-60`
- Visibility: `hidden`, `invisible`, `opacity-0`
- Overflow: `overflow-y-auto`, `overflow-x-auto`, `overflow-hidden`
- Cursor: `cursor-pointer`
- Backdrop: `backdrop-blur`, `supports-[backdrop-filter]:bg-background/60`
- Touch: `touch-pan-y`
- Safe area: `safe-area-pb`, `pb-[env(safe-area-inset-bottom)]`
- Scrollbar: `scrollbar-hide`

---

### COMPONENT ARCHITECTURE PATTERNS

#### **Reusable UI Components Used**
- `Button` (variants: default, outline, ghost, secondary)
- `Card` with `CardContent`, `CardDescription`, `CardHeader`, `CardTitle`, `CardFooter`
- `Input` (various sizes: h-12, h-14, h-16)
- `Label` (for form fields)
- `Badge` (variants: secondary, outline, default)
- `Avatar`, `AvatarImage`, `AvatarFallback`
- `Skeleton` (for loading states)
- `Accordion` with `AccordionContent`, `AccordionItem`, `AccordionTrigger`

#### **Icons from lucide-react**
Used extensively throughout for visual indicators, action buttons, and status indicators. Key icons:
- Navigation: `ArrowLeft`, `ArrowRight`, `Home`, `Menu`, `X`
- User/Profile: `User`, `HardHat`, `Building2`, `Users`
- Status: `Check`, `CheckCircle2`, `AlertCircle`, `Loader2`, `Sparkles`
- Actions: `Plus`, `Search`, `Settings`, `Send`, `Phone`, `MoreVertical`
- Work-related: `Briefcase`, `MapPin`, `Calendar`, `Clock`, `DollarSign`, `Wrench`
- Various trades: `Hammer`, `Blocks`, `Shovel`, `CircleDot`, `Fence`, `Wrench`

---

### KEY DESIGN PATTERNS & CONSISTENCY OBSERVATIONS

#### **1. Card-Based Layouts**
- Consistent use of `Card` components for information presentation
- Padding: `p-4`, `p-5`, `p-6` inside `CardContent`
- Border styling: `border` with optional color variants (`border-primary/30`, `border-muted-foreground/20`)
- Rounded corners: `rounded-xl` for cards

#### **2. Button Standards**
- Default height: `h-12` (48px) for normal buttons, `h-14` (56px) for large CTAs, `h-16` (64px) for hero CTAs
- Padding: `px-4` to `px-8`
- Font size: `text-base` with `font-semibold`
- Width: Typically `w-full` in forms/modals, `w-auto` in headers

#### **3. Header/Navigation Consistency**
- Sticky headers: `sticky top-0 z-50 border-b bg-background/95 backdrop-blur`
- Fixed height: `h-14` (56px)
- Content padding: `px-4`
- Max width container: `max-w-screen-xl`
- Flex layout: `flex items-center justify-between`

#### **4. Form Input Consistency**
- Height: `h-12` or `h-14` with `text-base`
- Placeholder: `placeholder="..."`
- Border: `border border-input`
- Focus: `focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring`
- Label above input with `space-y-2` gap

#### **5. Typography Hierarchy**
- Page titles (h1): `text-xl font-bold` or `text-2xl font-bold`
- Section headers (h2/h3): `text-lg font-semibold` to `text-2xl font-bold`
- Body text: `text-sm` or `text-base`
- Secondary text: `text-sm text-muted-foreground`
- Small text: `text-xs text-muted-foreground`

#### **6. Color Coordination**
- Primary actions: `bg-primary text-primary-foreground`
- Secondary actions: `border border-border text-foreground` or `bg-muted text-muted-foreground`
- Info/Success states: `bg-primary/5` or `bg-green-50`
- Error states: `bg-red-50`, `text-red-700`
- Highlight backgrounds: `bg-primary/5`, `bg-primary/10`, `border-primary/20`

#### **7. Loading States**
- Skeleton components with consistent heights: `h-4`, `h-6`, `h-10`
- Spinner: `<Loader2 className="h-8 w-8 animate-spin text-primary" />`
- Loading text: `text-muted-foreground`
- Container: Full screen with flex centering

#### **8. Error/NotFound States**
- Centered card layout with icon
- Icon wrapper: `flex h-20 w-20 items-center justify-center rounded-full bg-primary/10`
- Title: `text-2xl` in card header
- Description: `text-muted-foreground` with helpful context
- Actions: Multiple buttons (primary + secondary)

#### **9. Mobile-First Spacing**
- Page padding: `px-4 py-6` or `px-4 py-4`
- Max width: `max-w-screen-sm` for most content
- Bottom padding (safe area): `pb-20` for pages with bottom nav, `pb-[env(safe-area-inset-bottom)]` for footers
- Section spacing: `space-y-3` to `space-y-6` between major sections

#### **10. Bottom Navigation**
- Fixed: `fixed bottom-0 left-0 right-0`
- Height: `h-14` or content height with safe area
- Border: `border-t`
- Background: `bg-background` with optional `backdrop-blur`
- Items: Flex centered with gap-2 to gap-4

---

### INCONSISTENCIES DETECTED

#### **Minor Inconsistencies:**

1. **Button Heights Vary**
   - Normal buttons: `h-12`
   - Large CTAs: `h-14`
   - Extra large (hero): `h-16`
   - Icon-only buttons: `h-10`, `h-12`, `w-10`, `w-12`
   - *This is actually intentional and consistent with design system*

2. **Input Heights**
   - Standard: `h-12`
   - Large: `h-14`
   - Depends on page context
   - *Consistent pattern*

3. **Card Padding**
   - ContentPadding: `p-4`, `p-5`, `p-6`
   - HeaderPadding: `pb-2`, `pb-3`, `pb-6`
   - *Varies by content type but follows pattern*

4. **Loading Skeleton Layout**
   - Dashboard loading uses: `container mx-auto p-6 space-y-6`
   - Messages loading uses: `container mx-auto p-6 h-[calc(100vh-8rem)]`
   - Jobs loading uses: `container mx-auto p-6 space-y-6`
   - *Should all be consistent - minor issue*

5. **Typography Scaling**
   - Some hero text: `text-[2rem]` (custom)
   - Most titles: `text-2xl` to `text-3xl`
   - Some headings: `text-xl`, `text-lg`, `text-base`
   - *Generally consistent, custom sizing used sparingly*

---

### DESIGN SYSTEM STRENGTHS

1. **Excellent brand consistency** with primary color (#2563eb blue) throughout
2. **Cohesive spacing system** - uses Tailwind's standard scale
3. **Strong mobile-first approach** with proper touch targets (48px minimum)
4. **Proper semantic HTML** with Cards, Buttons, Inputs from shadcn/ui
5. **Accessible contrast** - good use of foreground/muted-foreground hierarchy
6. **Consistent interaction patterns** - hover states, active states, loading states
7. **Orange accent bar** used strategically on landing pages (`bg-brand-orange`)

---

### RECOMMENDATIONS FOR FULL CONSISTENCY

1. **Standardize loading skeleton styling** - use single pattern across all loading.tsx files
2. **Create layout wrapper component** - for consistent max-width and padding
3. **Define button size constants** - ensure height consistency across app
4. **Standardize form field styling** - consistent label, input, error message patterns
5. **Create card wrapper pattern** - for consistent card usage throughout

---

### COMPLETE FILE LISTING WITH KEY CLASSES

| File | Key Classes | Primary Pattern |
|------|------------|-----------------|
| layout.tsx | `font-sans antialiased` | Global font setup |
| page.tsx | `px-4 py-12 mx-auto max-w-screen-sm` | Section-based layout |
| error.tsx | `min-h-screen flex items-center justify-center` | Centered error card |
| not-found.tsx | `min-h-screen bg-background flex items-center justify-center p-4` | Centered card in viewport |
| (authenticated)/layout.tsx | `min-h-screen flex flex-col` | Two-rail layout with nav |
| (authenticated)/error.tsx | Same as root error | Centered error display |
| dashboard/page.tsx | `min-h-screen bg-background pb-20` | Content with bottom nav |
| dashboard/loading.tsx | `container mx-auto p-6 space-y-6` | Grid skeleton layout |
| contractor/signup/page.tsx | `min-h-screen bg-background` with progress bar | Multi-step form |
| contractor/post-job/page.tsx | `min-h-screen bg-background pb-20` | Form with sticky CTA |
| contractor/matches/page.tsx | `min-h-screen bg-background pb-20` | Card stack layout |
| contractor/matches/loading.tsx | `container mx-auto p-6 space-y-6` | Grid skeleton |
| worker/signup/page.tsx | `min-h-screen bg-background` | Multi-step onboarding |
| worker/jobs/page.tsx | `min-h-screen bg-background pb-20` | Filter + card list |
| worker/jobs/loading.tsx | `container mx-auto p-6 space-y-6` | Grid skeleton |
| worker/profile/build/page.tsx | `min-h-screen bg-background pb-20` | Form + sticky CTA |
| worker/profile/error.tsx | `min-h-screen bg-background pb-20` | Error with header |
| messages/page.tsx | `min-h-screen bg-background pb-20` | Search + list |
| messages/[id]/page.tsx | `min-h-screen flex flex-col pb-20` | Chat layout |
| messages/loading.tsx | `container mx-auto p-6 h-[calc(...)]` | Grid skeleton |

---

This audit confirms that RateRight v2 has **strong UI consistency** with only minor variations that serve purposeful design patterns. The design system effectively uses Tailwind CSS and shadcn/ui components throughout, maintaining visual coherence across all pages.