[web] qns/quiz: persist quiz scroll mode between questions navigation (#1671)

This commit is contained in:
Nitesh Seram 2025-09-04 07:32:01 +05:30 committed by GitHub
parent 9a1bba26e7
commit 9d7210480b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 116 additions and 42 deletions

View File

@ -60,6 +60,7 @@ export function questionHrefWithListType(
href: string,
listType?: QuestionListTypeData | null,
questionMetadata?: QuestionMetadata,
currentPathname?: string | null, // We need this prop and cannot use window.location instead because Next.js doesn't update location when we do the navigation
): string {
if (listType == null) {
return href;
@ -96,33 +97,38 @@ export function questionHrefWithListType(
case 'framework': {
switch (listType.value) {
case 'react':
return (
`/questions/quiz/${QuestionFrameworkRawToSEOMapping.react}` +
urlObject.search +
`#${questionMetadata.slug}`
return getQuizQuestionUrl(
`/questions/quiz/${QuestionFrameworkRawToSEOMapping.react}`,
questionMetadata.slug,
urlObject,
currentPathname,
);
}
break;
}
case 'language': {
switch (listType.value) {
case 'js':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.js}` +
urlObject.search +
`#${questionMetadata.slug}`
case 'js': {
return getQuizQuestionUrl(
`/questions/quiz/${QuestionLanguageRawToSEOMapping.js}`,
questionMetadata.slug,
urlObject,
currentPathname,
);
}
case 'html':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.html}` +
urlObject.search +
`#${questionMetadata.slug}`
return getQuizQuestionUrl(
`/questions/quiz/${QuestionLanguageRawToSEOMapping.html}`,
questionMetadata.slug,
urlObject,
currentPathname,
);
case 'css':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.css}` +
urlObject.search +
`#${questionMetadata.slug}`
return getQuizQuestionUrl(
`/questions/quiz/${QuestionLanguageRawToSEOMapping.css}`,
questionMetadata.slug,
urlObject,
currentPathname,
);
}
}
@ -136,6 +142,7 @@ export function questionHrefFrameworkSpecificAndListType(
questionMetadata: QuestionMetadata,
listType?: QuestionListTypeData | null,
framework?: QuestionFramework,
currentPathname?: string | null,
): string {
const hrefWithMaybeFramework = questionHrefFrameworkSpecific(
questionMetadata,
@ -147,12 +154,19 @@ export function questionHrefFrameworkSpecificAndListType(
hrefWithMaybeFramework,
listType,
questionMetadata,
currentPathname,
);
return questionHrefStripSamePathnameAndSearch(hrefWithListType);
return questionHrefStripSamePathnameAndSearch(
hrefWithListType,
currentPathname,
);
}
function questionHrefStripSamePathnameAndSearch(href: string): string {
function questionHrefStripSamePathnameAndSearch(
href: string,
currentPathname?: string | null,
): string {
if (typeof window === 'undefined') {
return href;
}
@ -162,7 +176,7 @@ function questionHrefStripSamePathnameAndSearch(href: string): string {
// Leave only the hash if the current URL is the same as the href
// Next.js has problems pushing to the same URL with a hash
if (
window.location.pathname === urlObj.pathname &&
currentPathname === urlObj.pathname &&
window.location.search === urlObj.search
) {
return urlObj.hash || '#';
@ -170,3 +184,33 @@ function questionHrefStripSamePathnameAndSearch(href: string): string {
return href;
}
function getQuizQuestionUrl(
base: string,
slug: string,
urlObject: URL,
currentPathname?: string | null,
) {
const scrollModeUrls = [
`/questions/quiz/${QuestionFrameworkRawToSEOMapping.react}`,
`/questions/quiz/${QuestionLanguageRawToSEOMapping.js}`,
`/questions/quiz/${QuestionLanguageRawToSEOMapping.html}`,
`/questions/quiz/${QuestionLanguageRawToSEOMapping.css}`,
];
const scrollModeUrl = base + urlObject.search + `#${slug}`;
if (typeof window === 'undefined') {
return scrollModeUrl;
}
const isQuizPage = currentPathname?.includes('/questions/quiz');
// Check if the current URL is scroll mode url type
const isScrollMode = scrollModeUrls.includes(currentPathname ?? '');
if (!isQuizPage || isScrollMode) {
return scrollModeUrl;
}
// Return quiz page by page url if the current page is page by page mode
return `/questions/quiz/${slug}` + urlObject.search;
}

View File

@ -1,6 +1,8 @@
'use client';
import clsx from 'clsx';
import { usePathname } from 'next/navigation';
import type { ComponentProps } from 'react';
import { Suspense } from 'react';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
@ -142,17 +144,8 @@ function InterviewsQuestionsListSlideOutButtonImpl({
return (
<div className="flex h-7 gap-x-2">
<Button
<NavigationButton
addonPosition="start"
href={
prevQuestion
? questionHrefFrameworkSpecificAndListType(
prevQuestion,
listType,
framework,
)
: undefined
}
icon={RiArrowLeftSLine}
isDisabled={prevQuestion == null}
isLabelHidden={true}
@ -161,6 +154,17 @@ function InterviewsQuestionsListSlideOutButtonImpl({
description: 'Previous question',
id: 'WPfIhl',
})}
questionHref={
prevQuestion
? (pathname: string | null) =>
questionHrefFrameworkSpecificAndListType(
prevQuestion,
listType,
framework,
pathname,
)
: undefined
}
size="xs"
tooltip={prevQuestion ? prevQuestion?.title : undefined}
variant="tertiary"
@ -178,17 +182,8 @@ function InterviewsQuestionsListSlideOutButtonImpl({
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE
}
/>
<Button
<NavigationButton
addonPosition="start"
href={
nextQuestion
? questionHrefFrameworkSpecificAndListType(
nextQuestion,
listType,
framework,
)
: undefined
}
icon={RiArrowRightSLine}
isDisabled={nextQuestion == null}
isLabelHidden={true}
@ -197,6 +192,17 @@ function InterviewsQuestionsListSlideOutButtonImpl({
description: 'Next question',
id: 'DqvEKB',
})}
questionHref={
nextQuestion
? (pathname: string | null) =>
questionHrefFrameworkSpecificAndListType(
nextQuestion,
listType,
framework,
pathname,
)
: undefined
}
size="xs"
tooltip={nextQuestion ? nextQuestion?.title : undefined}
variant="tertiary"
@ -204,3 +210,17 @@ function InterviewsQuestionsListSlideOutButtonImpl({
</div>
);
}
function NavigationButton({
questionHref,
...props
}: ComponentProps<typeof Button> &
Readonly<{
questionHref?: (pathname: string | null) => string;
}>) {
const pathname = usePathname();
const href = questionHref?.(pathname);
return <Button {...props} href={href} />;
}

View File

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useRouter } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import {
@ -220,6 +220,7 @@ export default function InterviewsQuestionsListSlideOutContents({
}: Props) {
const intl = useIntl();
const router = useRouter();
const pathname = usePathname();
const { userProfile } = useUserProfile();
const [showFilters, setShowFilters] = useState(false);
@ -379,6 +380,7 @@ export default function InterviewsQuestionsListSlideOutContents({
processedQuestions[0],
listType,
framework,
pathname,
),
);
}
@ -388,6 +390,7 @@ export default function InterviewsQuestionsListSlideOutContents({
processedQuestions,
setFirstQuestionHref,
showCompanyPaywall,
pathname,
]);
const label = intl.formatMessage({

View File

@ -1,4 +1,5 @@
import clsx from 'clsx';
import { usePathname } from 'next/navigation';
import { questionHrefFrameworkSpecificAndListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
import QuestionAuthor from '~/components/interviews/questions/metadata/QuestionAuthor';
@ -27,6 +28,8 @@ export default function InterviewsQuestionsListSlideOutHovercardContents({
question,
size = 'body3',
}: Props) {
const pathname = usePathname();
return (
<div
className={clsx(
@ -45,11 +48,12 @@ export default function InterviewsQuestionsListSlideOutHovercardContents({
question,
listType,
framework,
pathname,
)}
variant="flat">
{question.title}
</Anchor>
<Text className="text-pretty grow" color="secondary" size="body2">
<Text className="grow text-pretty" color="secondary" size="body2">
{question.excerpt}
</Text>
</div>

View File

@ -1,4 +1,5 @@
import clsx from 'clsx';
import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
import VignetteOverlay from '~/components/common/VignetteOverlay';
@ -49,6 +50,7 @@ export default function InterviewsQuestionsListSlideOutQuestionList<
showCompanyPaywall,
}: Props<Q>) {
const intl = useIntl();
const pathname = usePathname();
if (questions.length === 0) {
return (
@ -138,6 +140,7 @@ export default function InterviewsQuestionsListSlideOutQuestionList<
questionMetadata,
listType,
framework,
pathname,
);
return (