From f79348312b15eefc2c987c4fd5610cd34962bdc6 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 16 May 2026 09:28:22 +0330 Subject: [PATCH] feat(tender-details): improve scrolling behavior and header offset management - Introduced a new constant for the tender section scroll offset to enhance scrolling behavior when navigating to sections. - Updated the scroll-to-section logic to account for the sticky header and added a small gap for better visual alignment. - Refactored the useTenderSectionScrollSpy hook to utilize the new scroll offset, ensuring consistent behavior across sections. - Improved code organization by separating scroll offset calculations into a dedicated function. --- src/app/tenders/[details]/constants.ts | 6 ++++- .../hooks/use-tender-section-scroll-spy.ts | 3 ++- .../tenders/[details]/scroll-to-section.ts | 27 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/app/tenders/[details]/constants.ts b/src/app/tenders/[details]/constants.ts index 427ab49..e5b8e57 100644 --- a/src/app/tenders/[details]/constants.ts +++ b/src/app/tenders/[details]/constants.ts @@ -13,7 +13,11 @@ export const TENDER_SECTION_IDS = { aiSummary: "tender-section-ai-summary", } as const; -export const HEADER_SCROLL_MARGIN = "scroll-mt-[5.5rem]"; +/** Sticky app header clearance — keep in sync with `getTenderSectionScrollOffset`. */ +export const TENDER_SECTION_SCROLL_OFFSET_PX = 96; + +/** For hash / native scroll-into-view (matches offset above). */ +export const HEADER_SCROLL_MARGIN = "scroll-mt-24"; /** Body area only: center content in the card; ShowcaseSection `title` is unchanged */ export const SHOWCASE_CENTERED_BODY = { diff --git a/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts b/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts index 32f5a6e..a3cab79 100644 --- a/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts +++ b/src/app/tenders/[details]/hooks/use-tender-section-scroll-spy.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { getTenderSectionScrollOffset } from "../scroll-to-section"; export function useTenderSectionScrollSpy(sectionIds: string[]) { const [activeId, setActiveId] = useState(sectionIds[0] ?? ""); @@ -17,7 +18,7 @@ export function useTenderSectionScrollSpy(sectionIds: string[]) { if (sectionIds.length === 0) return; const pickActive = () => { - const offset = 96; + const offset = getTenderSectionScrollOffset(); const scrollY = window.scrollY; const viewportHeight = window.innerHeight; const documentHeight = Math.max( diff --git a/src/app/tenders/[details]/scroll-to-section.ts b/src/app/tenders/[details]/scroll-to-section.ts index 1fc1c34..5dddd4a 100644 --- a/src/app/tenders/[details]/scroll-to-section.ts +++ b/src/app/tenders/[details]/scroll-to-section.ts @@ -1,11 +1,34 @@ +import { TENDER_SECTION_SCROLL_OFFSET_PX } from "./constants"; + +const SECTION_SCROLL_GAP_PX = 8; + +/** Pixels to leave above the section top (sticky header + small gap). */ +export function getTenderSectionScrollOffset(): number { + if (typeof document === "undefined") { + return TENDER_SECTION_SCROLL_OFFSET_PX + SECTION_SCROLL_GAP_PX; + } + const header = document.querySelector("header"); + const headerHeight = header?.getBoundingClientRect().height ?? 0; + return ( + Math.max(TENDER_SECTION_SCROLL_OFFSET_PX, headerHeight) + + SECTION_SCROLL_GAP_PX + ); +} + export function scrollToTenderSection(id: string) { const el = document.getElementById(id); if (!el) return; + const reduceMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - el.scrollIntoView({ + + const offset = getTenderSectionScrollOffset(); + const top = + el.getBoundingClientRect().top + window.scrollY - offset; + + window.scrollTo({ + top: Math.max(0, top), behavior: reduceMotion ? "auto" : "smooth", - block: "start", }); }