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:
@@ -33,7 +33,7 @@ export function SwipeCard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const photo = profile.photos[0];
|
const photo = profile.photos?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export function OnboardingScreen({
|
|||||||
const total = ONBOARDING_STEPS.length;
|
const total = ONBOARDING_STEPS.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 min-h-dvh">
|
<div className="flex flex-col h-dvh">
|
||||||
<div className="flex items-center gap-3 px-5 pt-[calc(env(safe-area-inset-top)+16px)] pb-2">
|
<div className="shrink-0 flex items-center gap-3 px-5 pt-[calc(env(safe-area-inset-top)+16px)] pb-2">
|
||||||
<button
|
<button
|
||||||
onClick={onBack ?? (() => router.back())}
|
onClick={onBack ?? (() => router.back())}
|
||||||
className="w-9 h-9 rounded-full flex items-center justify-center hover:bg-(--color-bg-secondary) shrink-0"
|
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>
|
</span>
|
||||||
</div>
|
</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">
|
<h1 className="font-[var(--font-display)] font-extrabold text-[24px] text-(--color-content-primary) leading-snug">
|
||||||
{title} <span>{emoji}</span>
|
{title} <span>{emoji}</span>
|
||||||
</h1>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
|
||||||
import { TextField } from "@/components/ui/TextField";
|
import { TextField } from "@/components/ui/TextField";
|
||||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -79,9 +79,9 @@ export function BasicInfoStep({
|
|||||||
|
|
||||||
<ErrorBanner message={error} />
|
<ErrorBanner message={error} />
|
||||||
|
|
||||||
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
||||||
بریم مرحله بعد
|
بریم مرحله بعد
|
||||||
</Button>
|
</StepFooterButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
|
||||||
import { TextField } from "@/components/ui/TextField";
|
import { TextField } from "@/components/ui/TextField";
|
||||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { submitBodyInfo, FullProfile } from "@/lib/api";
|
import { submitBodyInfo, FullProfile } from "@/lib/api";
|
||||||
@@ -58,9 +58,9 @@ export function BodyInfoStep({
|
|||||||
onChange={(e) => setWeight(e.target.value)}
|
onChange={(e) => setWeight(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<ErrorBanner message={error} />
|
<ErrorBanner message={error} />
|
||||||
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
||||||
بریم مرحله بعد
|
بریم مرحله بعد
|
||||||
</Button>
|
</StepFooterButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
|
||||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
||||||
import { OptionPicker } from "@/components/ui/OptionPicker";
|
import { OptionPicker } from "@/components/ui/OptionPicker";
|
||||||
@@ -84,9 +84,9 @@ export function EducationStep({
|
|||||||
|
|
||||||
<ErrorBanner message={error} />
|
<ErrorBanner message={error} />
|
||||||
|
|
||||||
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
||||||
بریم مرحله بعد
|
بریم مرحله بعد
|
||||||
</Button>
|
</StepFooterButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
|
||||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
||||||
import { OptionPicker } from "@/components/ui/OptionPicker";
|
import { OptionPicker } from "@/components/ui/OptionPicker";
|
||||||
@@ -83,9 +83,9 @@ export function GenericOptionStep({
|
|||||||
max={max}
|
max={max}
|
||||||
/>
|
/>
|
||||||
<ErrorBanner message={error} />
|
<ErrorBanner message={error} />
|
||||||
<Button size="lg" isBlock disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
<StepFooterButton disabled={!isValid} isLoading={loading} onClick={handleSubmit}>
|
||||||
بریم مرحله بعد
|
بریم مرحله بعد
|
||||||
</Button>
|
</StepFooterButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Plus, X } from "lucide-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 { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { confirmPhotosStep, uploadPhoto, deletePhoto, photoUrl, FullProfile, Photo } from "@/lib/api";
|
import { confirmPhotosStep, uploadPhoto, deletePhoto, photoUrl, FullProfile, Photo } from "@/lib/api";
|
||||||
import { formatApiError } from "@/lib/hooks";
|
import { formatApiError } from "@/lib/hooks";
|
||||||
@@ -102,9 +102,9 @@ export function PhotosStep({
|
|||||||
|
|
||||||
<ErrorBanner message={error} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { StepFooterButton } from "@/components/onboarding/StepFooterButton";
|
||||||
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
import { ErrorBanner } from "@/components/ui/ErrorBanner";
|
||||||
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
import { FullScreenLoader } from "@/components/ui/FullScreenLoader";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -139,9 +139,9 @@ export function ProfileQuestionsStep({
|
|||||||
|
|
||||||
<ErrorBanner message={error} />
|
<ErrorBanner message={error} />
|
||||||
|
|
||||||
<Button size="lg" isBlock disabled={!allAnswered} isLoading={loading} onClick={handleSubmit}>
|
<StepFooterButton disabled={!allAnswered} isLoading={loading} onClick={handleSubmit}>
|
||||||
بریم مرحله بعد
|
بریم مرحله بعد
|
||||||
</Button>
|
</StepFooterButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user