feat(FN-208): implement sticky checkout bar on scroll (+1 more)
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled
Commits merged: - chore(FN-208): format code with biome - feat(FN-208): implement sticky checkout bar on scroll Files changed: .../subscription/__tests__/index.test.tsx | 10 ++++ .../routes/dashboard/subscription/index.test.tsx | 9 ++++ .../src/routes/dashboard/subscription/index.tsx | 54 +++++++++++++++++++++- 3 files changed, 71 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-208
This commit is contained in:
@@ -133,6 +133,16 @@ vi.mock("@/lib/toast", () => ({ toast: mockToast }));
|
||||
// ─── Mock canvas-confetti ───────────────────────────────────────────────────
|
||||
vi.mock("canvas-confetti", () => ({ default: vi.fn() }));
|
||||
|
||||
// ─── Mock IntersectionObserver (not available in jsdom) ────────────────────
|
||||
vi.stubGlobal(
|
||||
"IntersectionObserver",
|
||||
vi.fn(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
// ─── Mock BrandSelector ─────────────────────────────────────────────────────
|
||||
vi.mock("@/components/subscription/brand-selector", () => ({
|
||||
BrandSelector: ({
|
||||
|
||||
@@ -66,6 +66,15 @@ vi.mock("@/lib/user-settings", () => ({
|
||||
// Mock canvas-confetti
|
||||
vi.mock("canvas-confetti", () => ({ default: vi.fn() }));
|
||||
|
||||
// Mock IntersectionObserver (not available in jsdom)
|
||||
const mockIntersectionObserver = vi.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal("IntersectionObserver", mockIntersectionObserver);
|
||||
|
||||
// Mock lazy BrandSelector
|
||||
vi.mock("@/components/subscription/brand-selector", () => ({
|
||||
BrandSelector: () => <div data-testid="brand-selector">BrandSelector</div>,
|
||||
|
||||
@@ -373,6 +373,25 @@ export function SubscriptionPage() {
|
||||
expired: "outline",
|
||||
};
|
||||
|
||||
// ─── Sticky bar state (before early returns so hook count is consistent) ─
|
||||
const planCardsRef = useRef<HTMLDivElement>(null);
|
||||
const [stickyBarVisible, setStickyBarVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
const el = planCardsRef.current;
|
||||
if (!selectedPlanKey || !el) {
|
||||
setStickyBarVisible(false);
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setStickyBarVisible(!entry.isIntersecting);
|
||||
},
|
||||
{ threshold: 0, rootMargin: "-80px 0px 0px 0px" },
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [selectedPlanKey]);
|
||||
|
||||
// ─── Loading state ─────────────────────────────────────────────────────────
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -510,7 +529,7 @@ export function SubscriptionPage() {
|
||||
|
||||
// ─── Normal subscription page ──────────────────────────────────────────────
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl space-y-8">
|
||||
<div className="mx-auto max-w-5xl space-y-8 pb-16">
|
||||
<h2 className="text-2xl font-bold">{t("subscription.title")}</h2>
|
||||
|
||||
{/* Active Subscription Status */}
|
||||
@@ -828,7 +847,7 @@ export function SubscriptionPage() {
|
||||
{/* Plan Comparison Cards */}
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-semibold">{t("subscription.planComparison")}</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div ref={planCardsRef} className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{plans.map((plan) => {
|
||||
const isSelected = selectedPlanKey === plan.key;
|
||||
const price = billingPeriod === "monthly" ? plan.priceMonthly : plan.priceYearly;
|
||||
@@ -1012,6 +1031,37 @@ export function SubscriptionPage() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Sticky Checkout Bar — appears when user scrolls past plan cards with a plan selected */}
|
||||
{stickyBarVisible &&
|
||||
selectedPlanKey &&
|
||||
(() => {
|
||||
const selectedPlan = plans.find((p) => p.key === selectedPlanKey);
|
||||
const barPrice =
|
||||
billingPeriod === "monthly"
|
||||
? (selectedPlan?.priceMonthly ?? 0)
|
||||
: (selectedPlan?.priceYearly ?? 0);
|
||||
const barPeriodLabel =
|
||||
billingPeriod === "monthly" ? t("common.perMonth") : t("common.perYear");
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-0 z-50 animate-fade-in-up border-t border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80">
|
||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-3">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-sm font-semibold">
|
||||
{t(`subscription.plans.${selectedPlanKey}.name`)}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatTRY(barPrice)}
|
||||
<span className="text-xs">{barPeriodLabel}</span>
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" className="px-6" onClick={handleProceedToPayment}>
|
||||
{t("subscription.proceed")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user