Fix onboarding bottom button not staying pinned, and swipe card crash

- OnboardingScreen used min-h-dvh instead of h-dvh, so the scrollable content
  area never actually clipped/scrolled internally; the page just grew and the
  primary CTA button scrolled away with it instead of staying visible. Switch
  to h-dvh and give each step's button a sticky footer (StepFooterButton).
- SwipeCard crashed with "Cannot read properties of null" when a candidate
  had no photos, because the backend could return null instead of an empty
  array; guard with optional chaining.
This commit is contained in:
2026-09-19 22:17:29 +03:30
parent dca8c5e9e3
commit 4bd7b93b4a
9 changed files with 47 additions and 22 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ export function SwipeCard({
}
}
const photo = profile.photos[0];
const photo = profile.photos?.[0];
return (
<motion.div
@@ -30,8 +30,8 @@ export function OnboardingScreen({
const total = ONBOARDING_STEPS.length;
return (
<div className="flex flex-col flex-1 min-h-dvh">
<div className="flex items-center gap-3 px-5 pt-[calc(env(safe-area-inset-top)+16px)] pb-2">
<div className="flex flex-col h-dvh">
<div className="shrink-0 flex items-center gap-3 px-5 pt-[calc(env(safe-area-inset-top)+16px)] pb-2">
<button
onClick={onBack ?? (() => router.back())}
className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary) shrink-0"
@@ -65,7 +65,7 @@ export function OnboardingScreen({
</span>
</div>
<div className="flex flex-col gap-2 px-6 pt-6 pb-2">
<div className="shrink-0 flex flex-col gap-2 px-6 pt-6 pb-2">
<h1 className="font-[var(--font-display)] font-extrabold text-[24px] text-(--color-content-primary) leading-snug">
{title} <span>{emoji}</span>
</h1>
@@ -0,0 +1,25 @@
"use client";
import { ButtonHTMLAttributes } from "react";
import { Button } from "@/components/ui/Button";
interface StepFooterButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading?: boolean;
}
// دکمه‌ی اصلی هر مرحله‌ی onboarding؛ چون داخل ناحیه‌ی overflow-y-auto صفحه است،
// sticky bottom-0 نسبت به همون ناحیه چسبیده می‌مونه، نه اینکه ته لیست اسکرول بشه.
export function StepFooterButton({
children,
isLoading,
disabled,
...props
}: StepFooterButtonProps) {
return (
<div className="sticky bottom-0 -mx-6 px-6 pt-4 pb-[calc(env(safe-area-inset-bottom)+16px)] bg-(--color-bg-primary) border-t border-(--color-border-subtle)">
<Button size="lg" isBlock isLoading={isLoading} disabled={disabled} {...props}>
{children}
</Button>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { TextField } from "@/components/ui/TextField";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { cn } from "@/lib/utils";
@@ -79,9 +79,9 @@ export function BasicInfoStep({
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { TextField } from "@/components/ui/TextField";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { submitBodyInfo, FullProfile } from "@/lib/api";
@@ -58,9 +58,9 @@ export function BodyInfoStep({
onChange={(e) => setWeight(e.target.value)}
/>
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
import { OptionPicker } from "@/components/ui/OptionPicker";
@@ -84,9 +84,9 @@ export function EducationStep({
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
import { OptionPicker } from "@/components/ui/OptionPicker";
@@ -83,9 +83,9 @@ export function GenericOptionStep({
max={max}
/>
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}
@@ -2,7 +2,7 @@
import { useRef, useState } from "react";
import { Plus, X } from "lucide-react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { confirmPhotosStep, uploadPhoto, deletePhoto, photoUrl, FullProfile, Photo } from "@/lib/api";
import { formatApiError } from "@/lib/hooks";
@@ -102,9 +102,9 @@ export function PhotosStep({
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={photos.length === 0} isLoading={loading} onClick={handleNext}>
<StepFooterButton disabled={photos.length === 0} isLoading={loading} onClick={handleNext}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/Button";
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
import { cn } from "@/lib/utils";
@@ -139,9 +139,9 @@ export function ProfileQuestionsStep({
<ErrorBanner message={error} />
<Button size="lg" isBlock disabled={!allAnswered} isLoading={loading} onClick={handleSubmit}>
<StepFooterButton disabled={!allAnswered} isLoading={loading} onClick={handleSubmit}>
بریم مرحله بعد
</Button>
</StepFooterButton>
</div>
);
}