[web] qns/quiz: add quiz scrolling mode (#1661)

Co-authored-by: Yangshun <tay.yang.shun@gmail.com>
This commit is contained in:
Nitesh Seram 2025-09-02 08:55:17 +05:30 committed by GitHub
parent 9a23093e76
commit 9a1bba26e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 2609 additions and 272 deletions

View File

@ -152,6 +152,7 @@
"unified": "10.1.2",
"usehooks-ts": "2.9.1",
"uuid": "9.0.1",
"virtua": "^0.41.5",
"web-vitals": "5.0.3",
"zod": "^3.22.4"
},

View File

@ -1,4 +1,3 @@
import FeedbackWidget from '~/components/global/feedback/FeedbackWidget';
import QuestionsQuizContentLayout from '~/components/interviews/questions/content/quiz/QuestionsQuizContentLayout';
type Props = Readonly<{
@ -6,10 +5,5 @@ type Props = Readonly<{
}>;
export default async function QuizContentLayout({ children }: Props) {
return (
<QuestionsQuizContentLayout>
{children}
<FeedbackWidget bottomClassname="bottom-12" />
</QuestionsQuizContentLayout>
);
return <QuestionsQuizContentLayout>{children}</QuestionsQuizContentLayout>;
}

View File

@ -0,0 +1,121 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import QuestionQuizScrollableList from '~/components/interviews/questions/content/quiz/QuestionQuizScrollableList';
import { fetchInterviewsQuestionQuizScrollScrollableContent } from '~/db/contentlayer/InterviewsQuestionQuizScrollableContentReader';
import { readQuestionQuizContentsAll } from '~/db/QuestionsContentsReader';
import { fetchQuestionsList } from '~/db/QuestionsListReader';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
import { getIntlServerOnly } from '~/i18n';
import defaultMetadata from '~/seo/defaultMetadata';
type Props = Readonly<{
params: Readonly<{
locale: string;
}>;
}>;
const listType: QuestionListTypeData = {
tab: 'quiz',
type: 'language',
value: 'css',
};
const category = 'CSS';
export const dynamic = 'force-static';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = params;
const [intl, { questions }] = await Promise.all([
getIntlServerOnly(locale),
fetchQuestionsList(listType, locale),
]);
const seoTitle = intl.formatMessage(
{
defaultMessage: '{category} Interview Questions and Answers',
description: 'SEO title of quiz scrolling mode page',
id: 'bYOK+T',
},
{ category },
);
return defaultMetadata({
description: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ most important {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of quiz scrolling mode page',
id: 'xtk4b8',
},
{
category,
questionCount: roundQuestionCountToNearestTen(questions.length),
},
),
locale,
ogImagePageType: intl.formatMessage({
defaultMessage: 'Frameworks or languages',
description: 'OG image page title of framework and language page',
id: '+XLpUw',
}),
ogImageTitle: seoTitle,
pathname: `/questions/quiz/css-interview-questions`,
socialTitle: `${seoTitle} | GreatFrontEnd`,
title: intl.formatMessage(
{
defaultMessage:
'{category} Interview Questions and Answers | by Ex-FAANG interviewers',
description: 'Title of quiz scrolling mode page',
id: '2HQlrS',
},
{ category },
),
});
}
export default async function Page({ params }: Props) {
const { locale } = params;
const [intl, quizQuestions, longDescription] = await Promise.all([
getIntlServerOnly(locale),
readQuestionQuizContentsAll(listType, locale),
fetchInterviewsQuestionQuizScrollScrollableContent('css', locale),
]);
if (quizQuestions == null || longDescription == null) {
return notFound();
}
return (
<QuestionQuizScrollableList
description={intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of scroll mode quiz questions page',
id: 'EtDVci',
},
{
category,
questionCount: roundQuestionCountToNearestTen(quizQuestions.length),
},
)}
languageOrFramework="css"
listType={listType}
longDescription={longDescription}
questionsList={quizQuestions.map((item) => item.question)}
title={intl.formatMessage(
{
defaultMessage: '{category} Interview Questions',
description: 'Title of scrolling mode page',
id: '4gumtt',
},
{ category },
)}
/>
);
}

View File

@ -0,0 +1,120 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import QuestionQuizScrollableList from '~/components/interviews/questions/content/quiz/QuestionQuizScrollableList';
import { fetchInterviewsQuestionQuizScrollScrollableContent } from '~/db/contentlayer/InterviewsQuestionQuizScrollableContentReader';
import { readQuestionQuizContentsAll } from '~/db/QuestionsContentsReader';
import { fetchQuestionsList } from '~/db/QuestionsListReader';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
import { getIntlServerOnly } from '~/i18n';
import defaultMetadata from '~/seo/defaultMetadata';
type Props = Readonly<{
params: Readonly<{
locale: string;
}>;
}>;
const listType: QuestionListTypeData = {
tab: 'quiz',
type: 'language',
value: 'html',
};
const category = 'HTML';
export const dynamic = 'force-static';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = params;
const [intl, { questions }] = await Promise.all([
getIntlServerOnly(locale),
fetchQuestionsList(listType, locale),
]);
const seoTitle = intl.formatMessage(
{
defaultMessage: '{category} Interview Questions and Answers',
description: 'SEO title of quiz scrolling mode page',
id: 'bYOK+T',
},
{ category },
);
return defaultMetadata({
description: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ most important {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of quiz scrolling mode page',
id: 'xtk4b8',
},
{
category,
questionCount: roundQuestionCountToNearestTen(questions.length),
},
),
locale,
ogImagePageType: intl.formatMessage({
defaultMessage: 'Frameworks or languages',
description: 'OG image page title of framework and language page',
id: '+XLpUw',
}),
ogImageTitle: seoTitle,
pathname: `/questions/quiz/html-interview-questions`,
socialTitle: `${seoTitle} | GreatFrontEnd`,
title: intl.formatMessage(
{
defaultMessage:
'{category} Interview Questions and Answers | by Ex-FAANG interviewers',
description: 'Title of quiz scrolling mode page',
id: '2HQlrS',
},
{ category },
),
});
}
export default async function Page({ params }: Props) {
const { locale } = params;
const [intl, quizQuestions, longDescription] = await Promise.all([
getIntlServerOnly(locale),
readQuestionQuizContentsAll(listType, locale),
fetchInterviewsQuestionQuizScrollScrollableContent('html', locale),
]);
if (quizQuestions == null || longDescription == null) {
return notFound();
}
return (
<QuestionQuizScrollableList
description={intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of scroll mode quiz questions page',
id: 'EtDVci',
},
{
category,
questionCount: roundQuestionCountToNearestTen(quizQuestions.length),
},
)}
languageOrFramework="html"
listType={listType}
longDescription={longDescription}
questionsList={quizQuestions.map((item) => item.question)}
title={intl.formatMessage(
{
defaultMessage: '{category} Interview Questions',
description: 'Title of scrolling mode page',
id: '4gumtt',
},
{ category },
)}
/>
);
}

View File

@ -0,0 +1,121 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import QuestionQuizScrollableList from '~/components/interviews/questions/content/quiz/QuestionQuizScrollableList';
import { fetchInterviewsQuestionQuizScrollScrollableContent } from '~/db/contentlayer/InterviewsQuestionQuizScrollableContentReader';
import { readQuestionQuizContentsAll } from '~/db/QuestionsContentsReader';
import { fetchQuestionsList } from '~/db/QuestionsListReader';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
import { getIntlServerOnly } from '~/i18n';
import defaultMetadata from '~/seo/defaultMetadata';
type Props = Readonly<{
params: Readonly<{
locale: string;
}>;
}>;
const listType: QuestionListTypeData = {
tab: 'quiz',
type: 'language',
value: 'js',
};
const category = 'JavaScript';
export const dynamic = 'force-static';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = params;
const [intl, { questions }] = await Promise.all([
getIntlServerOnly(locale),
fetchQuestionsList(listType, locale),
]);
const seoTitle = intl.formatMessage(
{
defaultMessage: '{category} Interview Questions and Answers',
description: 'SEO title of quiz scrolling mode page',
id: 'bYOK+T',
},
{ category },
);
return defaultMetadata({
description: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ most important JavaScript interview questions and answers, covering everything from core concepts to advanced JavaScript features',
description: 'Description of quiz scrolling mode page',
id: 'nnXIFO',
},
{
category,
questionCount: roundQuestionCountToNearestTen(questions.length),
},
),
locale,
ogImagePageType: intl.formatMessage({
defaultMessage: 'Frameworks or languages',
description: 'OG image page title of framework and language page',
id: '+XLpUw',
}),
ogImageTitle: seoTitle,
pathname: `/questions/quiz/javascript-interview-questions`,
socialTitle: `${seoTitle} | GreatFrontEnd`,
title: intl.formatMessage(
{
defaultMessage:
'{category} Interview Questions and Answers | by Ex-FAANG interviewers',
description: 'Title of quiz scrolling mode page',
id: '2HQlrS',
},
{ category },
),
});
}
export default async function Page({ params }: Props) {
const { locale } = params;
const [intl, quizQuestions, longDescription] = await Promise.all([
getIntlServerOnly(locale),
readQuestionQuizContentsAll(listType, locale),
fetchInterviewsQuestionQuizScrollScrollableContent('js', locale),
]);
if (quizQuestions == null || longDescription == null) {
return notFound();
}
return (
<QuestionQuizScrollableList
description={intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of scroll mode quiz questions page',
id: 'EtDVci',
},
{
category,
questionCount: roundQuestionCountToNearestTen(quizQuestions.length),
},
)}
languageOrFramework="js"
listType={listType}
longDescription={longDescription}
questionsList={quizQuestions.map((item) => item.question)}
title={intl.formatMessage(
{
defaultMessage: '{category} Interview Questions',
description: 'Title of scrolling mode page',
id: '4gumtt',
},
{ category },
)}
/>
);
}

View File

@ -0,0 +1,121 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import QuestionQuizScrollableList from '~/components/interviews/questions/content/quiz/QuestionQuizScrollableList';
import { fetchInterviewsQuestionQuizScrollScrollableContent } from '~/db/contentlayer/InterviewsQuestionQuizScrollableContentReader';
import { readQuestionQuizContentsAll } from '~/db/QuestionsContentsReader';
import { fetchQuestionsList } from '~/db/QuestionsListReader';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
import { getIntlServerOnly } from '~/i18n';
import defaultMetadata from '~/seo/defaultMetadata';
type Props = Readonly<{
params: Readonly<{
locale: string;
}>;
}>;
const listType: QuestionListTypeData = {
tab: 'quiz',
type: 'framework',
value: 'react',
};
const category = 'React';
export const dynamic = 'force-static';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = params;
const [intl, { questions }] = await Promise.all([
getIntlServerOnly(locale),
fetchQuestionsList(listType, locale),
]);
const seoTitle = intl.formatMessage(
{
defaultMessage: '{category} Interview Questions and Answers',
description: 'SEO title of quiz scrolling mode page',
id: 'bYOK+T',
},
{ category },
);
return defaultMetadata({
description: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ most important {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of quiz scrolling mode page',
id: 'xtk4b8',
},
{
category,
questionCount: roundQuestionCountToNearestTen(questions.length),
},
),
locale,
ogImagePageType: intl.formatMessage({
defaultMessage: 'Frameworks or languages',
description: 'OG image page title of framework and language page',
id: '+XLpUw',
}),
ogImageTitle: seoTitle,
pathname: `/questions/quiz/javascript-interview-questions`,
socialTitle: `${seoTitle} | GreatFrontEnd`,
title: intl.formatMessage(
{
defaultMessage:
'{category} Interview Questions and Answers | by Ex-FAANG interviewers',
description: 'Title of quiz scrolling mode page',
id: '2HQlrS',
},
{ category },
),
});
}
export default async function Page({ params }: Props) {
const { locale } = params;
const [intl, quizQuestions, longDescription] = await Promise.all([
getIntlServerOnly(locale),
readQuestionQuizContentsAll(listType, locale),
fetchInterviewsQuestionQuizScrollScrollableContent('react', locale),
]);
if (quizQuestions == null || longDescription == null) {
return notFound();
}
return (
<QuestionQuizScrollableList
description={intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers',
description: 'Description of scroll mode quiz questions page',
id: 'EtDVci',
},
{
category,
questionCount: roundQuestionCountToNearestTen(quizQuestions.length),
},
)}
languageOrFramework="react"
listType={listType}
longDescription={longDescription}
questionsList={quizQuestions.map((item) => item.question)}
title={intl.formatMessage(
{
defaultMessage: '{category} Interview Questions',
description: 'Title of scrolling mode page',
id: '4gumtt',
},
{ category },
)}
/>
);
}

View File

@ -12,11 +12,7 @@ type Props = Readonly<{
export default function InterviewsPageFeatures({ features }: Props) {
return (
<div
className={clsx(
'flex flex-wrap sm:flex-row',
'gap-x-6 gap-y-4 sm:gap-x-8 md:gap-x-12',
)}>
<div className={clsx('flex flex-wrap sm:flex-row', 'gap-x-12 gap-y-4')}>
{features.map(({ icon: FeatureIcon, label }) => (
<div key={label} className={clsx('flex items-center gap-2')}>
<FeatureIcon className={clsx('size-5', themeTextSubtitleColor)} />

View File

@ -15,6 +15,7 @@ type Props = Readonly<{
children?: ReactNode;
className?: string;
description: ReactNode;
descriptionWidthFull?: boolean;
features: ReadonlyArray<{
icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
label: string;
@ -37,6 +38,7 @@ export default function InterviewsPageHeader({
children,
className,
description,
descriptionWidthFull,
features,
headingAddOnElement,
logoImgSrc,
@ -95,7 +97,10 @@ export default function InterviewsPageHeader({
</div>
</div>
<Text
className={clsx('block max-w-2xl text-sm xl:text-base')}
className={clsx(
'block text-sm xl:text-base',
!descriptionWidthFull && 'max-w-2xl',
)}
color="subtitle"
size="inherit"
weight="medium">

View File

@ -1,3 +1,10 @@
import url from 'url';
import {
QuestionFrameworkRawToSEOMapping,
QuestionLanguageRawToSEOMapping,
} from '~/data/QuestionCategories';
import { getSiteOrigin } from '~/seo/siteUrl';
import type {
@ -52,6 +59,7 @@ function questionHrefFrameworkSpecific(
export function questionHrefWithListType(
href: string,
listType?: QuestionListTypeData | null,
questionMetadata?: QuestionMetadata,
): string {
if (listType == null) {
return href;
@ -82,6 +90,45 @@ export function questionHrefWithListType(
urlObject.searchParams.set('title', listType.title);
}
// Special URLs for quiz questions that support scrolling mode
if (questionMetadata?.format === 'quiz') {
switch (listType.type) {
case 'framework': {
switch (listType.value) {
case 'react':
return (
`/questions/quiz/${QuestionFrameworkRawToSEOMapping.react}` +
urlObject.search +
`#${questionMetadata.slug}`
);
}
break;
}
case 'language': {
switch (listType.value) {
case 'js':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.js}` +
urlObject.search +
`#${questionMetadata.slug}`
);
case 'html':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.html}` +
urlObject.search +
`#${questionMetadata.slug}`
);
case 'css':
return (
`/questions/quiz/${QuestionLanguageRawToSEOMapping.css}` +
urlObject.search +
`#${questionMetadata.slug}`
);
}
}
}
}
return urlObject.pathname + urlObject.search + urlObject.hash;
}
@ -90,11 +137,36 @@ export function questionHrefFrameworkSpecificAndListType(
listType?: QuestionListTypeData | null,
framework?: QuestionFramework,
): string {
const maybeFrameworkHref = questionHrefFrameworkSpecific(
const hrefWithMaybeFramework = questionHrefFrameworkSpecific(
questionMetadata,
listType,
framework,
);
return questionHrefWithListType(maybeFrameworkHref, listType);
const hrefWithListType = questionHrefWithListType(
hrefWithMaybeFramework,
listType,
questionMetadata,
);
return questionHrefStripSamePathnameAndSearch(hrefWithListType);
}
function questionHrefStripSamePathnameAndSearch(href: string): string {
if (typeof window === 'undefined') {
return href;
}
const urlObj = url.parse(href);
// 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 &&
window.location.search === urlObj.search
) {
return urlObj.hash || '#';
}
return href;
}

View File

@ -33,6 +33,8 @@ type Props = Readonly<{
studyListKey?: string;
}>;
const ButtonIcon = FaCheck;
export default function QuestionProgressAction({
metadata,
signInModalContents,
@ -41,6 +43,11 @@ export default function QuestionProgressAction({
const intl = useIntl();
const pathname = usePathname();
const user = useUser();
const buttonLabelMarkComplete = intl.formatMessage({
defaultMessage: 'Mark complete',
description: 'Mark question as complete',
id: 'C4am9n',
});
const [isLoginDialogShown, setIsLoginDialogShown] = useState(false);
const markCompleteMutation = useMutationQuestionProgressAdd();
@ -54,6 +61,10 @@ export default function QuestionProgressAction({
studyListKey ?? null,
);
if (data?.isQuestionLockedForViewer) {
return null;
}
if (user == null) {
if (metadata.access === 'premium') {
return null;
@ -63,12 +74,8 @@ export default function QuestionProgressAction({
<>
<Button
addonPosition="start"
icon={FaCheck}
label={intl.formatMessage({
defaultMessage: 'Mark complete',
description: 'Mark question as complete',
id: 'C4am9n',
})}
icon={ButtonIcon}
label={buttonLabelMarkComplete}
size="xs"
variant="secondary"
onClick={() => setIsLoginDialogShown(true)}
@ -122,14 +129,23 @@ export default function QuestionProgressAction({
);
}
if (isFetching || data?.isQuestionLockedForViewer) {
return null;
if (isFetching) {
return (
<Button
addonPosition="start"
className="cursor-disabled"
icon={ButtonIcon}
label={buttonLabelMarkComplete}
size="xs"
variant="secondary"
/>
);
}
if (data?.questionProgress?.status === 'complete') {
return (
<Button
icon={FaCheck}
icon={ButtonIcon}
isDisabled={deleteProgressMutation.isLoading}
isLoading={deleteProgressMutation.isLoading}
label={intl.formatMessage({
@ -186,14 +202,10 @@ export default function QuestionProgressAction({
return (
<Button
addonPosition="start"
icon={FaCheck}
icon={ButtonIcon}
isDisabled={markCompleteMutation.isLoading}
isLoading={markCompleteMutation.isLoading}
label={intl.formatMessage({
defaultMessage: 'Mark complete',
description: 'Mark the question as complete',
id: 'pj07uD',
})}
label={buttonLabelMarkComplete}
size="xs"
variant="secondary"
onClick={() => {

View File

@ -0,0 +1,29 @@
import { defineDocumentType } from 'contentlayer2/source-files';
import path from 'node:path';
function parseSlug(sourceFilePath: string) {
return sourceFilePath.split(path.posix.sep)[2];
}
function parseLocale(sourceFilePath: string) {
return path.basename(sourceFilePath).split('.')[0];
}
export const InterviewsQuestionQuizScrollableContentDocument =
defineDocumentType(() => ({
computedFields: {
locale: {
description: 'Locale',
resolve: (doc) => parseLocale(doc._raw.sourceFilePath),
type: 'string',
},
slug: {
description: 'Unique identifier of the list',
resolve: (doc) => parseSlug(doc._raw.sourceFilePath),
type: 'string',
},
},
contentType: 'mdx',
filePathPattern: 'interviews/quiz-scroll-mode/**/*.mdx',
name: 'InterviewsQuestionQuizScrollableContent',
}));

View File

@ -1,30 +1,22 @@
'use client';
import clsx from 'clsx';
import { getMDXComponent } from 'mdx-bundler/client';
import { useMemo } from 'react';
import { RiEditBoxLine } from 'react-icons/ri';
import QuestionMetadataSection from '~/components/interviews/questions/metadata/QuestionMetadataSection';
import type { QuestionQuiz } from '~/components/interviews/questions/common/QuestionsTypes';
import { FormattedMessage, useIntl } from '~/components/intl';
import MDXCodeBlock from '~/components/mdx/MDXCodeBlock';
import MDXComponentsForQuiz from '~/components/mdx/MDXComponentsForQuiz';
import SponsorsAdFormatInContentContainer from '~/components/sponsors/ads/SponsorsAdFormatInContentContainer';
import Button from '~/components/ui/Button';
import Container from '~/components/ui/Container';
import Divider from '~/components/ui/Divider';
import Heading from '~/components/ui/Heading';
import Section from '~/components/ui/Heading/HeadingContext';
import Prose from '~/components/ui/Prose';
import Text from '~/components/ui/Text';
import { hashQuestion } from '~/db/QuestionsUtils';
import QuestionReportIssueButton from '../../common/QuestionReportIssueButton';
import type { QuestionQuiz } from '../../common/QuestionsTypes';
import useQuestionLogEventCopyContents from '../../common/useQuestionLogEventCopyContents';
import useQuestionsAutoMarkAsComplete from '../../common/useQuestionsAutoMarkAsComplete';
import InterviewsStudyListBottomBar from '../../listings/study-list/InterviewsStudyListBottomBar';
import QuestionQuizItem from './QuestionQuizItem';
import QuestionQuizScrollModeToggle from './QuestionQuizScrollModeToggle';
type Props = Readonly<{
listIsShownInSidebarOnDesktop: boolean;
@ -63,20 +55,6 @@ export default function QuestionQuizContents({
question,
studyListKey,
}: Props) {
const copyRef = useQuestionLogEventCopyContents<HTMLDivElement>();
const { solution } = question;
// It's generally a good idea to memoize this function call to
// avoid re-creating the component every render.
const Solution = useMemo(() => {
if (!solution) {
return null;
}
return getMDXComponent(solution, {
MDXCodeBlock,
});
}, [solution]);
useQuestionsAutoMarkAsComplete(question.metadata, studyListKey);
return (
@ -85,7 +63,14 @@ export default function QuestionQuizContents({
'flex flex-col',
'min-h-[calc(100vh_-_var(--global-sticky-height))]',
)}>
<Container className={clsx('grow', 'py-6 lg:py-8 xl:py-12')} width="3xl">
<div
className={clsx(
'mx-auto w-full',
'grow',
'py-6 lg:py-8 xl:py-12',
'px-4 sm:px-6 lg:px-12 min-[1101px]:px-0',
'w-full min-[1101px]:max-w-[756px] xl:max-w-[864px]',
)}>
<div className="flex flex-col gap-y-6">
<div className="overflow-auto">
<Text className="mb-1 block" color="secondary" size="body2">
@ -99,49 +84,7 @@ export default function QuestionQuizContents({
<div
key={hashQuestion(question.metadata)}
className="relative mx-auto flex min-w-0 flex-1 flex-col">
<article aria-labelledby="question-title" className="grow">
<div className="min-h-0 flex-1">
<header className={clsx('flex flex-col gap-y-4')}>
<Heading
className="pb-4"
id="question-title"
level="heading4">
{question.metadata.title}
</Heading>
{question.metadata.subtitle && (
<Text className="block pb-4 text-lg sm:text-xl">
{question.metadata.subtitle}
</Text>
)}
<div className="flex items-start justify-between">
<QuestionMetadataSection
elements={['importance', 'difficulty', 'topics']}
metadata={question.metadata}
/>
<GitHubEditButton question={question} />
</div>
</header>
<Divider className="my-8" />
<Section>
{/* Contents section */}
<div ref={copyRef}>
{Solution == null ? (
<div>
<FormattedMessage
defaultMessage="Something went wrong"
description="Text that appears when the solution fails to load"
id="6UytmZ"
/>
</div>
) : (
<Prose>
<Solution components={MDXComponentsForQuiz} />
</Prose>
)}
</div>
</Section>
</div>
</article>
<QuestionQuizItem question={question} />
</div>
</div>
<Divider />
@ -161,8 +104,14 @@ export default function QuestionQuizContents({
adPlacement="questions_quiz"
size="md"
/>
</Container>
</div>
<InterviewsStudyListBottomBar
leftAddOnItem={
<QuestionQuizScrollModeToggle
isScrollModeValue={false}
slug={question.metadata.slug}
/>
}
listIsShownInSidebarOnDesktop={listIsShownInSidebarOnDesktop}
metadata={question.metadata}
studyListKey={studyListKey}

View File

@ -0,0 +1,139 @@
'use client';
import clsx from 'clsx';
import { getMDXComponent } from 'mdx-bundler/client';
import type { ForwardedRef } from 'react';
import { forwardRef, useId, useMemo } from 'react';
import { RiEditBoxLine } from 'react-icons/ri';
import QuestionReportIssueButton from '~/components/interviews/questions/common/QuestionReportIssueButton';
import QuestionMetadataSection from '~/components/interviews/questions/metadata/QuestionMetadataSection';
import { FormattedMessage, useIntl } from '~/components/intl';
import MDXCodeBlock from '~/components/mdx/MDXCodeBlock';
import {
MDXComponentsForQuiz,
MDXComponentsForScrollableQuiz,
} from '~/components/mdx/MDXComponentsForQuiz';
import Button from '~/components/ui/Button';
import Divider from '~/components/ui/Divider';
import Heading from '~/components/ui/Heading';
import Section from '~/components/ui/Heading/HeadingContext';
import Prose from '~/components/ui/Prose';
import Text from '~/components/ui/Text';
import type { QuestionQuiz } from '../../common/QuestionsTypes';
import useQuestionLogEventCopyContents from '../../common/useQuestionLogEventCopyContents';
type Props = Readonly<{
question: QuestionQuiz;
scrollMode?: boolean;
}>;
function GitHubEditButton({
question,
}: Readonly<{
question: QuestionQuiz;
}>) {
const intl = useIntl();
if (!question.metadata.gitHubEditUrl) {
return null;
}
return (
<Button
href={question.metadata.gitHubEditUrl}
icon={RiEditBoxLine}
label={intl.formatMessage({
defaultMessage: 'Edit on GitHub',
description: 'Edit on GitHub button',
id: '1wrVTx',
})}
size="xs"
variant="secondary"
/>
);
}
function QuestionQuizItem(
{ question, scrollMode }: Props,
ref: ForwardedRef<HTMLElement>,
) {
const titleId = useId();
const copyRef = useQuestionLogEventCopyContents<HTMLDivElement>();
const { solution } = question;
// It's generally a good idea to memoize this function call to
// avoid re-creating the component every render.
const Solution = useMemo(() => {
if (!solution) {
return null;
}
return getMDXComponent(solution, {
MDXCodeBlock,
});
}, [solution]);
return (
<article ref={ref} aria-labelledby={titleId} className="grow">
<div className={clsx('min-h-0 flex-1', scrollMode && 'space-y-9')}>
<header
className={clsx('flex flex-col', scrollMode ? 'gap-y-5' : 'ga-y-4')}>
<Heading
className="pb-4"
id={titleId}
level={scrollMode ? 'heading5' : 'heading4'}>
{question.metadata.title}
</Heading>
{question.metadata.subtitle && (
<Text className="block pb-4 text-lg sm:text-xl">
{question.metadata.subtitle}
</Text>
)}
<div className="flex items-start justify-between">
<QuestionMetadataSection
className="gap-x-8"
elements={['importance', 'difficulty', 'topics']}
metadata={question.metadata}
/>
<div className="flex gap-2">
<QuestionReportIssueButton
entity="question"
format={question.metadata.format}
slug={question.metadata.slug}
/>
<GitHubEditButton question={question} />
</div>
</div>
</header>
{!scrollMode && <Divider className="my-8" />}
<Section>
{/* Contents section */}
<div ref={copyRef}>
{Solution == null ? (
<div>
<FormattedMessage
defaultMessage="Something went wrong"
description="Text that appears when the solution fails to load"
id="6UytmZ"
/>
</div>
) : (
<Prose>
<Solution
components={
scrollMode
? MDXComponentsForScrollableQuiz
: MDXComponentsForQuiz
}
/>
</Prose>
)}
</div>
</Section>
</div>
</article>
);
}
export default forwardRef(QuestionQuizItem);

View File

@ -0,0 +1,74 @@
import clsx from 'clsx';
import type { InterviewsQuestionQuizScrollableContent } from 'contentlayer/generated';
import { useEnterViewport } from '~/hooks/useEnterViewport';
import InterviewsPageHeader from '~/components/interviews/common/InterviewsPageHeader';
import useInterviewsQuestionsFeatures from '~/components/interviews/common/useInterviewsQuestionsFeatures';
import type {
QuestionFramework,
QuestionLanguage,
} from '~/components/interviews/questions/common/QuestionsTypes';
import MDXContent from '~/components/mdx/MDXContent';
import { themeTextSecondaryColor } from '~/components/ui/theme';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
import QuestionQuizPageHeaderCodingSection from './QuestionQuizPageHeaderCodingSection';
type LanguageOrFramework =
| Extract<QuestionFramework, 'react'>
| QuestionLanguage;
type Props = Readonly<{
description: string;
languageOrFramework: LanguageOrFramework;
longDescription: InterviewsQuestionQuizScrollableContent;
onEnterViewport: (isInView: boolean) => void;
questionCount: number;
title: string;
}>;
export default function QuestionQuizPageHeader({
description,
languageOrFramework,
longDescription,
onEnterViewport,
questionCount,
title,
}: Props) {
const questionFeatures = useInterviewsQuestionsFeatures();
const features = [
questionFeatures.solvedByExInterviewers,
questionFeatures.criticalTopics,
];
const ref = useEnterViewport((isInView) => {
onEnterViewport(isInView);
});
return (
<div ref={ref}>
<InterviewsPageHeader
description={description}
descriptionWidthFull={true}
features={features}
title={title}>
<div className="space-y-12">
<MDXContent
components={{
QuestionCount: () => (
<span>{roundQuestionCountToNearestTen(questionCount)}</span>
),
}}
fontSize="md"
mdxCode={longDescription.body.code}
proseClassName={clsx('prose-sm', themeTextSecondaryColor)}
/>
<QuestionQuizPageHeaderCodingSection
languageOrFramework={languageOrFramework}
/>
</div>
</InterviewsPageHeader>
</div>
);
}

View File

@ -0,0 +1,291 @@
import clsx from 'clsx';
import { RiArrowRightLine, RiCheckLine } from 'react-icons/ri';
import {
QuestionFrameworkLabels,
QuestionFrameworkRawToSEOMapping,
QuestionLanguageLabels,
QuestionLanguageRawToSEOMapping,
} from '~/data/QuestionCategories';
import type {
QuestionFramework,
QuestionLanguage,
} from '~/components/interviews/questions/common/QuestionsTypes';
import {
QuestionCountCSSCoding,
QuestionCountHTMLCoding,
QuestionCountJavaScriptCoding,
QuestionCountReactCoding,
QuestionCountTypeScriptCoding,
} from '~/components/interviews/questions/listings/stats/QuestionCount';
import { useIntl } from '~/components/intl';
import Button from '~/components/ui/Button';
import Img from '~/components/ui/Img';
import Text from '~/components/ui/Text';
import { themeTextSuccessColor } from '~/components/ui/theme';
import { roundQuestionCountToNearestTen } from '~/db/QuestionsUtils';
type LanguageOrFramework =
| Extract<QuestionFramework, 'react'>
| QuestionLanguage;
type Props = Readonly<{
languageOrFramework: LanguageOrFramework;
}>;
export default function QuestionQuizPageHeaderCodingSection({
languageOrFramework,
}: Props) {
const intl = useIntl();
const labels: Record<LanguageOrFramework, string> = {
...QuestionLanguageLabels,
...QuestionFrameworkLabels,
};
const codingFeatures = useCodingFeatures(languageOrFramework);
const features =
languageOrFramework === 'js'
? [
codingFeatures['js-questions'],
codingFeatures['js-browser-coding'],
codingFeatures['js-solutions'],
codingFeatures['js-test-cases'],
codingFeatures['code-preview'],
]
: [
codingFeatures.questions,
codingFeatures['browser-coding'],
codingFeatures.solutions,
...(languageOrFramework !== 'react'
? [codingFeatures['test-cases']]
: []),
...(languageOrFramework === 'react'
? [codingFeatures['ui-preview']]
: [codingFeatures['related-ui-preview']]),
];
const codingQuestionsHref = Object.keys(
QuestionFrameworkRawToSEOMapping,
).includes(languageOrFramework)
? `/questions/${QuestionFrameworkRawToSEOMapping[languageOrFramework as QuestionFramework]}`
: `/questions/${QuestionLanguageRawToSEOMapping[languageOrFramework as QuestionLanguage]}`;
function getImageSrc(value: LanguageOrFramework) {
switch (value) {
case 'js':
return '/img/interviews/quiz/js-coding.png';
case 'html':
return '/img/interviews/quiz/html-coding.png';
case 'css':
return '/img/interviews/quiz/css-coding.png';
default:
return '/img/interviews/quiz/react-coding.png';
}
}
return (
<div>
<Text size="body1" weight="bold">
{intl.formatMessage(
{
defaultMessage:
"If you're looking for {languageOrFramework} coding questions -",
description: 'Header for coding section in interview quiz page',
id: 'GPQg5j',
},
{ languageOrFramework: labels[languageOrFramework] },
)}
</Text>
<Text className="mb-6 mt-2 block" color="secondary" size="body2">
{intl.formatMessage({
defaultMessage: "We've got you covered as well, with:",
description: 'Subheader for coding section in interview quiz page',
id: 'P6sA19',
})}
</Text>
<div className="flex flex-col gap-6 sm:flex-row">
<Img
alt={intl.formatMessage({
defaultMessage: 'Javascript coding',
description: 'Alt text for ads in content placement preview',
id: 'ofJOh2',
})}
className={clsx(
'object-cover object-left',
'h-full w-full sm:h-[196px] sm:w-[234px] lg:w-[343px]',
)}
src={getImageSrc(languageOrFramework)}
/>
<div className="space-y-6">
<ul className="flex flex-col gap-2.5" role="list">
{features.map((item) => (
<li key={item.key} className="flex items-center gap-x-2">
<RiCheckLine
aria-hidden="true"
className={clsx('size-3.5 shrink-0', themeTextSuccessColor)}
/>
<Text color="secondary" size="body2">
{item.label}
</Text>
</li>
))}
</ul>
<div className="space-x-6">
<Button
href={codingQuestionsHref}
icon={RiArrowRightLine}
label={intl.formatMessage({
defaultMessage: 'Get Started',
description: 'Label for get started button',
id: 'XCqQUO',
})}
size="sm"
variant="primary"
/>
<Text color="secondary" size="body3">
{intl.formatMessage(
{
defaultMessage: 'Join {engineersCount}+ engineers',
description: 'Label for total engineers using the platform',
id: 'jL6ul2',
},
{ engineersCount: '50,000' },
)}
</Text>
</div>
</div>
</div>
</div>
);
}
function useCodingFeatures(languageOrFramework: LanguageOrFramework) {
const intl = useIntl();
const labels: Record<LanguageOrFramework, string> = {
...QuestionLanguageLabels,
...QuestionFrameworkLabels,
};
const questionCount: Record<LanguageOrFramework, number> = {
css: QuestionCountCSSCoding,
html: QuestionCountHTMLCoding,
js: QuestionCountJavaScriptCoding,
react: QuestionCountReactCoding,
ts: QuestionCountTypeScriptCoding,
};
return {
'browser-coding': {
key: 'browser-coding',
label: intl.formatMessage({
defaultMessage:
'An in-browser coding workspace that mimics real interview conditions',
description: 'Label for browser-based coding environment',
id: 'fbZwwj',
}),
},
'code-preview': {
key: 'ui-questions-preview',
label: intl.formatMessage({
defaultMessage: 'Instantly preview your code for UI questions',
description: 'Label for code preview features',
id: 'zI3w61',
}),
},
'js-browser-coding': {
key: 'js-browser-coding',
label: intl.formatMessage({
defaultMessage:
'In-browser coding workspace similar to real interview environment',
description: 'Label for browser-based coding environment',
id: 'aA3ERo',
}),
},
'js-questions': {
key: 'js-questions',
label: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {languageOrFramework} coding questions',
description: 'Number of questions in coding section',
id: 'wG+caE',
},
{
languageOrFramework: labels[languageOrFramework],
questionCount: roundQuestionCountToNearestTen(
questionCount[languageOrFramework],
),
},
),
},
'js-solutions': {
key: 'js-solutions',
label: intl.formatMessage({
defaultMessage: 'Reference solutions from Big Tech Ex-interviewers',
description: 'Label for JavaScript solutions in coding questions',
id: 'JnZMCy',
}),
},
'js-test-cases': {
key: 'automated-test-cases',
label: intl.formatMessage({
defaultMessage: 'Automated test cases',
description: 'Label for automated test cases in coding questions',
id: 'VWfO56',
}),
},
questions: {
key: 'questions',
label: intl.formatMessage(
{
defaultMessage:
'{questionCount}+ {languageOrFramework} coding interview questions',
description: 'Number of questions in coding section',
id: 'ej+6cH',
},
{
languageOrFramework: labels[languageOrFramework],
questionCount: roundQuestionCountToNearestTen(
questionCount[languageOrFramework],
),
},
),
},
'related-ui-preview': {
key: 'ui-questions-preview',
label: intl.formatMessage({
defaultMessage: 'Instant UI preview for UI-related questions',
description: 'Label for related UI preview features',
id: 'A5JdsM',
}),
},
solutions: {
key: 'solutions',
label: intl.formatMessage({
defaultMessage:
'Reference solutions from ex-interviewers at Big Tech companies',
description: 'Label for coding questions solution',
id: '71Hlcg',
}),
},
'test-cases': {
key: 'test-cases',
label: intl.formatMessage({
defaultMessage: 'One-click automated, transparent test cases',
description: 'Label for test cases in coding questions',
id: 'Q891Ht',
}),
},
'ui-preview': {
key: 'ui-questions-preview',
label: intl.formatMessage({
defaultMessage: 'Instant UI preview for UI questions',
description: 'Label for UI preview features',
id: 'tmk5rv',
}),
},
};
}

View File

@ -0,0 +1,160 @@
import { useEffect, useState } from 'react';
import { RiPagesLine, RiTerminalWindowLine } from 'react-icons/ri';
import url from 'url';
import { useMediaQuery } from 'usehooks-ts';
import {
QuestionFrameworkRawToSEOMapping,
QuestionLanguageRawToSEOMapping,
} from '~/data/QuestionCategories';
import type {
QuestionFramework,
QuestionLanguage,
} from '~/components/interviews/questions/common/QuestionsTypes';
import { useIntl } from '~/components/intl';
import DropdownMenu from '~/components/ui/DropdownMenu';
type Props = Readonly<{
isScrollModeValue: boolean;
slug: string;
}>;
export default function QuestionQuizScrollModeToggle({
isScrollModeValue,
slug,
}: Props) {
const intl = useIntl();
const [isScrollMode, setIsScrollMode] = useState(isScrollModeValue);
const [questionHref, setQuestionHref] = useState({
pageByPage: '#',
scroll: '#',
});
const [renderToggleButton, setRenderToggleButton] = useState(false);
const isSmallTablet = useMediaQuery(
'(min-width: 641px) and (max-width: 768px)',
);
const isMobile = useMediaQuery('(max-width: 640px)');
useEffect(() => {
function getPageURL() {
if (typeof window === 'undefined') {
return '#';
}
const urlObject = new URL(window.location.href);
const language = urlObject.searchParams.get(
'language',
) as QuestionLanguage;
const framework = urlObject.searchParams.get(
'framework',
) as QuestionFramework;
// Becasue quiz scroll mode is only supported for languages and frameworks
// Also don't show when the language is ts
setRenderToggleButton(
(language !== null && language !== 'ts') || framework !== null,
);
if (isScrollModeValue) {
urlObject.pathname = `/questions/quiz/${slug}`;
urlObject.hash = '';
} else {
urlObject.hash = slug;
if (framework) {
urlObject.pathname = `/questions/quiz/${QuestionFrameworkRawToSEOMapping[framework as QuestionFramework]}`;
} else {
urlObject.pathname = `/questions/quiz/${QuestionLanguageRawToSEOMapping[language as QuestionLanguage]}`;
}
}
const newURL = url.format({
hash: urlObject.hash,
pathname: urlObject.pathname,
search: urlObject.search,
});
const oldURL = url.format({
hash: window.location.hash,
pathname: window.location.pathname,
search: window.location.search,
});
if (isScrollModeValue) {
setQuestionHref({
pageByPage: newURL,
scroll: oldURL,
});
} else {
setQuestionHref({
pageByPage: oldURL,
scroll: newURL,
});
}
}
getPageURL();
}, [slug, isScrollModeValue]);
const options = [
{
href: questionHref.scroll,
icon: RiPagesLine,
isSelected: isScrollMode,
label: intl.formatMessage({
defaultMessage: 'Scrolling view',
description: 'Label for quiz scroll mode toggle button',
id: '/mpSVj',
}),
value: 'scroll',
},
{
href: questionHref.pageByPage,
icon: RiTerminalWindowLine,
isSelected: !isScrollMode,
label: intl.formatMessage({
defaultMessage: 'Page-by-page',
description: 'Label for quiz scroll mode toggle button',
id: 'rCqW54',
}),
value: 'page-by-page',
},
];
const selectedOption = options.filter((item) => item.isSelected)[0];
if (!renderToggleButton) {
return null;
}
return (
<DropdownMenu
align="end"
icon={isSmallTablet ? undefined : selectedOption.icon}
isLabelHidden={isMobile}
label={selectedOption.label}
showChevron={true}
size="xs"
tooltip={
isScrollMode
? intl.formatMessage({
defaultMessage: 'All questions appear on a single page',
description: 'Tooltip for quiz scroll mode toggle button',
id: 'jjdVOP',
})
: intl.formatMessage({
defaultMessage: 'Questions appear on different pages',
description: 'Tooltip for quiz scroll mode toggle button',
id: 'THbGFn',
})
}>
{options.map(({ href, icon, isSelected, label, value }) => (
<DropdownMenu.Item
key={value}
href={href}
icon={icon}
isSelected={isSelected}
label={label}
onClick={() => setIsScrollMode(value === 'scroll')}
/>
))}
</DropdownMenu>
);
}

View File

@ -0,0 +1,222 @@
'use client';
import clsx from 'clsx';
import type { InterviewsQuestionQuizScrollableContent } from 'contentlayer/generated';
import { useEffect, useRef, useState } from 'react';
import type { VListHandle } from 'virtua';
import { WindowVirtualizer } from 'virtua';
import { useEnterViewport } from '~/hooks/useEnterViewport';
import useHashChange from '~/hooks/useHashChange';
import type {
QuestionFramework,
QuestionLanguage,
QuestionListTypeData,
QuestionQuiz,
} from '~/components/interviews/questions/common/QuestionsTypes';
import QuestionsQuizContentLayout from '~/components/interviews/questions/content/quiz/QuestionsQuizContentLayout';
import useQuestionCodingSorting from '~/components/interviews/questions/listings/filters/hooks/useQuestionCodingSorting';
import { sortQuestionsMultiple } from '~/components/interviews/questions/listings/filters/QuestionsProcessor';
import useQuestionsWithCompletionStatus from '~/components/interviews/questions/listings/items/useQuestionsWithCompletionStatus';
import InterviewsStudyListBottomBar from '~/components/interviews/questions/listings/study-list/InterviewsStudyListBottomBar';
import Divider from '~/components/ui/Divider';
import Section from '~/components/ui/Heading/HeadingContext';
import useQuestionsAutoMarkAsComplete from '../../common/useQuestionsAutoMarkAsComplete';
import QuestionQuizItem from './QuestionQuizItem';
import QuestionQuizPageHeader from './QuestionQuizPageHeader';
import QuestionQuizScrollableListIntroduction from './QuestionQuizScrollableListIntroduction';
import QuestionQuizScrollModeToggle from './QuestionQuizScrollModeToggle';
type Props = Readonly<{
description: string;
languageOrFramework: Extract<QuestionFramework, 'react'> | QuestionLanguage;
listType: QuestionListTypeData;
longDescription: InterviewsQuestionQuizScrollableContent;
questionsList: ReadonlyArray<QuestionQuiz>;
title: string;
}>;
function QuestionQuizScrollableListItem({
onEnterViewport,
question,
}: {
onEnterViewport: (isInView: boolean) => void;
question: QuestionQuiz;
}) {
const ref = useEnterViewport((isInView) => {
onEnterViewport(isInView);
});
return <QuestionQuizItem ref={ref} question={question} scrollMode={true} />;
}
export default function QuestionQuizScrollableList({
description,
languageOrFramework,
listType,
longDescription,
questionsList,
title,
}: Props) {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const currentHash = useHashChange();
const virtuaContainerRef = useRef<VListHandle>(null);
const [isIntroductionSectionInView, setIsIntroductionSectionInView] =
useState(false);
const isUserScroll = useRef(false);
const questionsWithCompletionStatus = useQuestionsWithCompletionStatus(
questionsList.map((item) => item.metadata),
);
// Sorting.
const { sortFields } = useQuestionCodingSorting({
listType,
});
// Processing.
const questions = sortQuestionsMultiple(
questionsWithCompletionStatus,
sortFields,
);
const currentQuestionIndex = questions.findIndex(
(question) => currentHash?.substring(1) === question.slug,
);
const questionIndex = currentQuestionIndex === -1 ? 0 : currentQuestionIndex;
useEffect(() => {
// Don't scroll to current question if hash change is triggered by user scrolling
if (currentQuestionIndex === -1 || isUserScroll.current) {
return;
}
virtuaContainerRef.current?.scrollToIndex(currentQuestionIndex, {
offset: -120,
});
}, [currentQuestionIndex, currentHash]);
const currentQuestion = questions[questionIndex];
const questionsMap = questionsList.reduce(
(acc, item) => {
acc[item.metadata.slug] = item;
return acc;
},
{} as Record<string, QuestionQuiz>,
);
// TODO: Later update with useDebounceCallback from 'useHooks-ts' when we upgrade the library
const debouncedHashChange = (hash: string) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
history.replaceState(null, '', `#${hash}`);
// Dispatch hashChange event manually to trigger any listeners because replaceState
// does not trigger it.
window.dispatchEvent(new Event('hashchange'));
isUserScroll.current = true;
setTimeout(() => {
isUserScroll.current = false;
}, 100);
}, 250);
};
useQuestionsAutoMarkAsComplete(currentQuestion);
return (
<QuestionsQuizContentLayout
initialListType={listType}
renderQuestionsListTopAddOnItem={({
listType: questionListType,
onClick,
tab,
}) => {
if (tab !== 'quiz') {
return null;
}
return (
<QuestionQuizScrollableListIntroduction
isActive={isIntroductionSectionInView}
listType={questionListType}
onClick={onClick}
/>
);
}}>
<div
className={clsx(
'flex flex-col',
'min-h-[calc(100vh_-_var(--global-sticky-height))]',
)}>
<div
className={clsx(
'mx-auto w-full',
'h-full grow overflow-y-hidden',
'py-8 lg:py-12',
'px-4 sm:px-6 lg:px-12 min-[1101px]:px-0',
'w-full min-[1101px]:max-w-[756px] xl:max-w-[864px]',
)}>
<QuestionQuizPageHeader
description={description}
languageOrFramework={languageOrFramework}
longDescription={longDescription}
questionCount={questions.length}
title={title}
onEnterViewport={(isInView) => {
setIsIntroductionSectionInView(isInView);
if (!isInView) {
return;
}
debouncedHashChange('introduction');
}}
/>
<Divider className="my-12" />
<Section>
<WindowVirtualizer
ref={virtuaContainerRef}
ssrCount={questions.length}
onScroll={() => {}}>
{questions.map((question, index) => (
<div key={question.slug}>
<QuestionQuizScrollableListItem
question={questionsMap[question.slug]}
onEnterViewport={(isInView) => {
if (isIntroductionSectionInView || !isInView) {
return;
}
debouncedHashChange(question.slug);
}}
/>
{index !== questions.length - 1 && (
<Divider className="my-12" />
)}
</div>
))}
</WindowVirtualizer>
</Section>
</div>
<InterviewsStudyListBottomBar
allowMarkComplete={!isIntroductionSectionInView}
initialListType={listType}
leftAddOnItem={
<QuestionQuizScrollModeToggle
isScrollModeValue={true}
slug={currentQuestion.slug}
/>
}
listIsShownInSidebarOnDesktop={true}
metadata={currentQuestion}
questionTitle={currentQuestion.title}
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE={null}
/>
</div>
</QuestionsQuizContentLayout>
);
}

View File

@ -0,0 +1,99 @@
import clsx from 'clsx';
import { useEffect, useRef } from 'react';
import url from 'url';
import {
QuestionFrameworkRawToSEOMapping,
QuestionLanguageRawToSEOMapping,
} from '~/data/QuestionCategories';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import { useIntl } from '~/components/intl';
import Anchor from '~/components/ui/Anchor';
import Text from '~/components/ui/Text';
import {
themeBackgroundElementEmphasizedStateColor,
themeBackgroundElementEmphasizedStateColor_Hover,
} from '~/components/ui/theme';
type QuestionClickEvent = Parameters<
NonNullable<React.ComponentProps<typeof Anchor>['onClick']>
>[0];
type Props = Readonly<{
isActive: boolean;
listType: QuestionListTypeData;
onClick?: (event: QuestionClickEvent, href: string) => void;
}>;
export default function QuestionQuizScrollableListIntroduction({
isActive,
listType,
onClick,
}: Props) {
const ref = useRef<HTMLDivElement>(null);
const intl = useIntl();
const href =
listType.type === 'framework'
? `/questions/quiz/${QuestionFrameworkRawToSEOMapping[listType.value]}`
: listType.type === 'language'
? `/questions/quiz/${QuestionLanguageRawToSEOMapping[listType.value]}`
: '#';
const finalHref = url.format({
pathname: href,
query: {
...(listType.type === 'framework'
? { framework: listType.value }
: listType.type === 'language'
? { language: listType.value }
: {}),
tab: 'quiz',
},
});
useEffect(() => {
if (!isActive) {
return;
}
ref.current?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
}, [isActive]);
return (
<div
ref={ref}
className={clsx(
'group relative',
'px-6 py-4',
'gap-4',
'transition-colors',
'focus-within:ring-brand focus-within:ring-2 focus-within:ring-inset',
themeBackgroundElementEmphasizedStateColor_Hover,
isActive && themeBackgroundElementEmphasizedStateColor,
)}>
<Anchor
className="focus:outline-none"
href={finalHref}
variant="unstyled"
onClick={(event) => {
onClick?.(event, finalHref);
}}>
{/* Extend touch target to entire panel */}
<span aria-hidden="true" className="absolute inset-0" />
<Text size="body3" weight="medium">
{intl.formatMessage({
defaultMessage: 'Introduction',
description:
'Title for introduction section in quiz questions list',
id: 'Rxyjw3',
})}
</Text>
</Anchor>
</div>
);
}

View File

@ -1,35 +1,68 @@
import clsx from 'clsx';
import { useSelectedLayoutSegment } from 'next/navigation';
import type { ReactNode } from 'react';
import { Suspense, useState } from 'react';
import { questionListFilterNamespace } from '~/components/interviews/questions/common/QuestionHrefUtils';
import useHashChange from '~/hooks/useHashChange';
import {
questionListFilterNamespace,
QuestionListTypeDefault,
} from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionListTypeData,
QuestionPracticeFormat,
} from '~/components/interviews/questions/common/QuestionsTypes';
import InterviewsQuestionsListSlideOutSwitcher from '~/components/interviews/questions/listings/slideout/InterviewsQuestionsListSlideOutSwitcher';
import {
useQuestionsListDataForType,
useQuestionsListTypeCurrent,
} from '~/components/interviews/questions/listings/utils/useQuestionsListDataForType';
import { themeBorderColor } from '~/components/ui/theme';
import type Anchor from '~/components/ui/Anchor';
import { hashQuestion } from '~/db/QuestionsUtils';
import InterviewsQuestionsListSlideOutContents from '../../listings/slideout/InterviewsQuestionsListSlideOutContents';
import type { QuestionListTypeWithLabel } from '../../listings/slideout/InterviewsQuestionsListSlideOutSwitcher';
export default function QuestionQuizSidebarQuestionList() {
type QuestionClickEvent = Parameters<
NonNullable<React.ComponentProps<typeof Anchor>['onClick']>
>[0];
type Props = Readonly<{
initialListType?: QuestionListTypeData;
renderQuestionsListTopAddOnItem?: ({
listType,
onClick,
tab,
}: {
listType: QuestionListTypeData;
onClick?: (event: QuestionClickEvent, href: string) => void;
tab: QuestionPracticeFormat | undefined;
}) => ReactNode;
}>;
export default function QuestionQuizSidebarQuestionList(props: Props) {
// Because useQuestionsListTypeCurrent() uses useSearchParams()
// Because of nuqs
return (
<Suspense>
<QuestionQuizSidebarQuestionListLoader />
<QuestionQuizSidebarQuestionListLoader {...props} />
</Suspense>
);
}
function QuestionQuizSidebarQuestionListLoader() {
function QuestionQuizSidebarQuestionListLoader({
initialListType: initialListTypeParam,
renderQuestionsListTopAddOnItem,
}: Props) {
const studyListKey = undefined;
const framework = undefined;
const listType = useQuestionsListTypeCurrent(studyListKey, framework);
const listType =
useQuestionsListTypeCurrent(studyListKey, framework) ??
initialListTypeParam ??
QuestionListTypeDefault;
const { data, isLoading } = useQuestionsListDataForType(listType);
const hidden = isLoading || data == null;
@ -41,16 +74,29 @@ function QuestionQuizSidebarQuestionListLoader() {
const initialListType = { ...data.listType, label: data.title };
return (
<QuestionsQuizSidebarQuestionListImpl initialListType={initialListType} />
<QuestionsQuizSidebarQuestionListImpl
initialListType={initialListType}
renderQuestionsListTopAddOnItem={renderQuestionsListTopAddOnItem}
/>
);
}
function QuestionsQuizSidebarQuestionListImpl({
initialListType,
renderQuestionsListTopAddOnItem,
}: Readonly<{
initialListType: QuestionListTypeWithLabel;
renderQuestionsListTopAddOnItem?: ({
listType,
tab,
}: {
listType: QuestionListTypeData;
onClick?: (event: QuestionClickEvent, href: string) => void;
tab: QuestionPracticeFormat | undefined;
}) => ReactNode;
}>) {
const segment = useSelectedLayoutSegment();
const hash = useHashChange();
const [currentListType, setCurrentListType] =
useState<QuestionListTypeWithLabel>(initialListType);
@ -74,7 +120,7 @@ function QuestionsQuizSidebarQuestionListImpl({
}));
}
const questionSlug = segment;
const questionSlug = (hash || segment || '').replace(/#/g, '');
const currentQuestionHash = questionSlug
? hashQuestion({
@ -87,32 +133,41 @@ function QuestionsQuizSidebarQuestionListImpl({
initialListType.type !== currentListType.type ||
initialListType.value !== currentListType.value;
function handleOnClickQuestion(event: QuestionClickEvent, href: string) {
if (hasChangedList) {
event.preventDefault();
setShowSwitchQuestionListDialog({
href,
show: true,
type: 'question-click',
});
}
}
return (
<div className={clsx('flex flex-col', 'w-full')}>
<div className={clsx('px-6 py-2', ['border-b', themeBorderColor])}>
<div className={clsx('px-6 py-2')}>
<InterviewsQuestionsListSlideOutSwitcher
listType={currentListType}
onChangeListType={setCurrentListType}
/>
</div>
<div className="h-0 grow pt-4">
<div className="h-0 grow">
<InterviewsQuestionsListSlideOutContents
key={filterNamespace}
currentQuestionHash={currentQuestionHash}
isDifferentListFromInitial={hasChangedList}
listType={currentListType}
mode="embedded"
renderQuestionsListTopAddOnItem={({ listType, tab }) =>
renderQuestionsListTopAddOnItem?.({
listType,
onClick: handleOnClickQuestion,
tab,
})
}
showSwitchQuestionListDialog={showSwitchQuestionListDialog}
onClickQuestion={(event, href: string) => {
if (hasChangedList) {
event.preventDefault();
setShowSwitchQuestionListDialog({
href,
show: true,
type: 'question-click',
});
}
}}
onClickQuestion={handleOnClickQuestion}
onCloseSwitchQuestionListDialog={onCloseSwitchQuestionListDialog}
onListTabChange={(newTab) =>
setCurrentListType({

View File

@ -9,14 +9,23 @@ import QuestionsQuizSidebarCollapser from '~/components/interviews/questions/con
import Section from '~/components/ui/Heading/HeadingContext';
import { themeBorderColor } from '~/components/ui/theme';
import type { QuestionListTypeData } from '../../common/QuestionsTypes';
import QuestionsQuizSidebarQuestionList from './QuestionQuizSidebarQuestionList';
import useQuestionsQuizSidebarExpanded from './useQuestionsQuizSidebarExpanded';
type Props = Readonly<{
children: React.ReactNode;
initialListType?: QuestionListTypeData;
renderQuestionsListTopAddOnItem?: React.ComponentProps<
typeof QuestionsQuizSidebarQuestionList
>['renderQuestionsListTopAddOnItem'];
}>;
export default function QuestionsQuizContentLayout({ children }: Props) {
export default function QuestionsQuizContentLayout({
children,
initialListType,
renderQuestionsListTopAddOnItem,
}: Props) {
const [questionsQuizSidebarExpanded] = useQuestionsQuizSidebarExpanded();
const pathname = usePathname();
@ -32,11 +41,16 @@ export default function QuestionsQuizContentLayout({ children }: Props) {
{questionsQuizSidebarExpanded && (
<Section>
<div
className={clsx('hidden w-[380px] xl:flex', [
className={clsx('hidden w-[300px] lg:flex xl:w-80', [
'border-r',
themeBorderColor,
])}>
<QuestionsQuizSidebarQuestionList />
<QuestionsQuizSidebarQuestionList
initialListType={initialListType}
renderQuestionsListTopAddOnItem={
renderQuestionsListTopAddOnItem
}
/>
</div>
</Section>
)}

View File

@ -34,7 +34,7 @@ type Props = Readonly<{
listIsShownInSidebarOnDesktop: boolean;
listTabs?: ReadonlyArray<QuestionPracticeFormat>;
processedQuestions: ReadonlyArray<QuestionMetadataWithCompletedStatus>;
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE: string;
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE: string | null;
title?: string;
}>;
@ -64,12 +64,21 @@ function InterviewsQuestionsListSlideOutImpl({
// Have to be controlled because we don't want to
// fetch the question lists for nothing
const [isSlideOutShown, setIsSlideOutShown] = useQueryState(
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE,
parseAsBoolean.withDefault(false),
);
const [isSlideOutShownQueryState, setIsSlideOutShownQueryState] =
useQueryState(
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE ?? '',
parseAsBoolean.withDefault(false),
);
const [isSlideOutShownState, setIsSlideOutShownState] = useState(false);
const isSlideOutShown = slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE
? isSlideOutShownQueryState
: isSlideOutShownState;
const setIsSlideOutShown = slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE
? setIsSlideOutShownQueryState
: setIsSlideOutShownState;
const isMobile = useMediaQuery('(max-width: 640px)');
const isDesktop = useMediaQuery('(min-width: 1367px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
const router = useRouter();
const [currentListType, setCurrentListType] =
useState<QuestionListTypeWithLabel>(initialListType);

View File

@ -4,10 +4,14 @@ import clsx from 'clsx';
import { Suspense } from 'react';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { questionHrefFrameworkSpecificAndListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
import {
questionHrefFrameworkSpecificAndListType,
QuestionListTypeDefault,
} from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionFramework,
QuestionHash,
QuestionListTypeData,
QuestionMetadataWithCompletedStatus,
} from '~/components/interviews/questions/common/QuestionsTypes';
import useQuestionCodingSorting from '~/components/interviews/questions/listings/filters/hooks/useQuestionCodingSorting';
@ -32,8 +36,9 @@ import type { QuestionListTypeWithLabel } from './InterviewsQuestionsListSlideOu
type Props = Readonly<{
currentQuestionHash: QuestionHash;
framework?: QuestionFramework;
initialListType?: QuestionListTypeData;
listIsShownInSidebarOnDesktop: boolean;
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE: string;
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE: string | null;
studyListKey?: string;
}>;
@ -48,11 +53,15 @@ export default function InterviewsQuestionsListSlideOutButton(props: Props) {
function InterviewsQuestionsListSlideOutButtonWithLoader({
currentQuestionHash,
framework,
initialListType,
listIsShownInSidebarOnDesktop,
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE,
studyListKey,
}: Props) {
const listType = useQuestionsListTypeCurrent(studyListKey, framework);
const listType =
useQuestionsListTypeCurrent(studyListKey, framework) ??
initialListType ??
QuestionListTypeDefault;
const { data, isLoading } = useQuestionsListDataForType(listType);
const questionsWithCompletionStatus = useQuestionsWithCompletionStatus(

View File

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { useRouter } from 'next/navigation';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import {
RiArrowDownSLine,
@ -188,6 +189,13 @@ type Props = Readonly<{
>['onClickQuestion'];
onCloseSwitchQuestionListDialog: () => void;
onListTabChange?: (newTab: QuestionPracticeFormat) => void;
renderQuestionsListTopAddOnItem?: ({
listType,
tab,
}: {
listType: QuestionListTypeData;
tab: QuestionPracticeFormat | undefined;
}) => ReactNode;
setFirstQuestionHref?: (href: string) => void;
showSwitchQuestionListDialog: Readonly<{
href: string | null;
@ -206,6 +214,7 @@ export default function InterviewsQuestionsListSlideOutContents({
onClickQuestion,
onCloseSwitchQuestionListDialog,
onListTabChange,
renderQuestionsListTopAddOnItem,
setFirstQuestionHref,
showSwitchQuestionListDialog,
}: Props) {
@ -389,13 +398,13 @@ export default function InterviewsQuestionsListSlideOutContents({
return (
<>
<div className={clsx('flex flex-col gap-4', 'h-full')}>
<div className={clsx('flex flex-col', 'h-full')}>
<form
className="flex w-full flex-col gap-4"
className="flex w-full flex-col gap-4 pb-5"
onSubmit={(event) => {
event.preventDefault();
}}>
<div className={clsx('flex w-full items-center gap-3', 'px-6 pt-2')}>
<div className={clsx('flex w-full items-center gap-3', 'px-6')}>
<div className="flex-1">
<TextInput
autoComplete="off"
@ -447,8 +456,9 @@ export default function InterviewsQuestionsListSlideOutContents({
{showFilters && embedFilters}
</form>
{listTabs && (
<div className="px-6">
<div>
<TabsUnderline
className="px-6"
size="xs"
tabs={listTabs.map((listTabValue) => {
const labels: Record<QuestionPracticeFormat, string> = {
@ -482,7 +492,7 @@ export default function InterviewsQuestionsListSlideOutContents({
</div>
)}
{questionAttributesUnion.formats.size > 1 && (
<div className="mb-3 px-6">
<div className="my-3 px-6">
<QuestionListFilterFormats
formatFilterOptions={formatFilterOptions}
formatFilters={formatFilters}
@ -509,58 +519,68 @@ export default function InterviewsQuestionsListSlideOutContents({
? sortedQuestions.slice(0, 4)
: processedQuestions
}
renderQuestionsListTopAddOnItem={() =>
renderQuestionsListTopAddOnItem?.({
listType,
tab: data?.listType.tab,
})
}
showCompanyPaywall={showCompanyPaywall}
onClickQuestion={onClickQuestion}
/>
)}
</ScrollArea>
</div>
<ConfirmationDialog
cancelButtonLabel={intl.formatMessage({
defaultMessage: 'Stay on previous list',
description: 'Stay on previous question list',
id: 'IEnLEU',
})}
confirmButtonLabel={intl.formatMessage({
defaultMessage: 'Switch',
description: 'Button label for switch study list',
id: '09QDZQ',
})}
isShown={showSwitchQuestionListDialog.show}
title={intl.formatMessage({
defaultMessage: 'Switch study list',
description: 'Change study list dialog title',
id: '6rN7CN',
})}
onCancel={() => {
onCloseSwitchQuestionListDialog();
onCancelSwitchStudyList?.();
}}
onClose={() => {
onCloseSwitchQuestionListDialog();
}}
onConfirm={() => {
if (!showSwitchQuestionListDialog.href) {
return;
}
{/* Wrapped with showSwitchQuestionListDialog.show condition to avoid
dialog appearing twice when we trigger this */}
{showSwitchQuestionListDialog.show && (
<ConfirmationDialog
cancelButtonLabel={intl.formatMessage({
defaultMessage: 'Stay on previous list',
description: 'Stay on previous question list',
id: 'IEnLEU',
})}
confirmButtonLabel={intl.formatMessage({
defaultMessage: 'Switch',
description: 'Button label for switch study list',
id: '09QDZQ',
})}
isShown={showSwitchQuestionListDialog.show}
title={intl.formatMessage({
defaultMessage: 'Switch study list',
description: 'Change study list dialog title',
id: '6rN7CN',
})}
onCancel={() => {
onCloseSwitchQuestionListDialog();
onCancelSwitchStudyList?.();
}}
onClose={() => {
onCloseSwitchQuestionListDialog();
}}
onConfirm={() => {
if (!showSwitchQuestionListDialog.href) {
return;
}
onCloseSwitchQuestionListDialog();
router.push(showSwitchQuestionListDialog.href);
}}>
{showSwitchQuestionListDialog.type === 'question-click' ? (
<FormattedMessage
defaultMessage="You've selected a question from a different study list than the one you're currently using. Navigating to it will switch your current list. Do you want to proceed?"
description="Confirmation text for switching study list"
id="+C/8iu"
/>
) : (
<FormattedMessage
defaultMessage="You've selected a different study list than the one you're currently using. Navigating to it will switch your current list. Do you want to proceed?"
description="Confirmation text for switching study list"
id="/D6EXa"
/>
)}
</ConfirmationDialog>
onCloseSwitchQuestionListDialog();
router.push(showSwitchQuestionListDialog.href);
}}>
{showSwitchQuestionListDialog.type === 'question-click' ? (
<FormattedMessage
defaultMessage="You've selected a question from a different study list than the one you're currently using. Navigating to it will switch your current list. Do you want to proceed?"
description="Confirmation text for switching study list"
id="+C/8iu"
/>
) : (
<FormattedMessage
defaultMessage="You've selected a different study list than the one you're currently using. Navigating to it will switch your current list. Do you want to proceed?"
description="Confirmation text for switching study list"
id="/D6EXa"
/>
)}
</ConfirmationDialog>
)}
</>
);
}

View File

@ -1,4 +1,5 @@
import clsx from 'clsx';
import type { ReactNode } from 'react';
import VignetteOverlay from '~/components/common/VignetteOverlay';
import { questionHrefFrameworkSpecificAndListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
@ -29,6 +30,7 @@ type Props<Q extends QuestionMetadata> = Readonly<{
typeof InterviewsQuestionsListSlideOutQuestionListItem
>['onClick'];
questions: ReadonlyArray<Q>;
renderQuestionsListTopAddOnItem?: () => ReactNode;
showCompanyPaywall?: boolean;
}>;
@ -43,6 +45,7 @@ export default function InterviewsQuestionsListSlideOutQuestionList<
mode,
onClickQuestion,
questions,
renderQuestionsListTopAddOnItem,
showCompanyPaywall,
}: Props<Q>) {
const intl = useIntl();
@ -74,7 +77,7 @@ export default function InterviewsQuestionsListSlideOutQuestionList<
);
return (
<div className={clsx('size-full relative')}>
<div className={clsx('relative size-full')}>
<VignetteOverlay
className={clsx('min-h-[500px]')}
overlay={
@ -113,6 +116,7 @@ export default function InterviewsQuestionsListSlideOutQuestionList<
</Text>
</div>
)}
{renderQuestionsListTopAddOnItem && renderQuestionsListTopAddOnItem()}
{questions.map((questionMetadata, index) => {
const hasCompletedQuestion =
checkIfCompletedQuestion?.(questionMetadata);

View File

@ -85,8 +85,8 @@ export default function InterviewsQuestionsListSlideOutQuestionListItem<
themeBackgroundElementEmphasizedStateColor_Hover,
isActiveQuestion && themeBackgroundElementEmphasizedStateColor,
)}>
<div className="grow py-4">
<div className="flex items-center gap-x-4">
<div className="grow py-3">
<div className="flex items-center gap-x-3">
{checkIfCompletedQuestion != null && (
<QuestionsListItemProgressChip
className="z-[1]"

View File

@ -1,11 +1,15 @@
import clsx from 'clsx';
import { useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { RiFilterLine } from 'react-icons/ri';
import { trpc } from '~/hooks/trpc';
import useUserProfile from '~/hooks/user/useUserProfile';
import { useQuestionFormatsData } from '~/data/QuestionCategories';
import {
QuestionLanguageLabels,
useQuestionFormatsData,
} from '~/data/QuestionCategories';
import SidebarPremiumChip from '~/components/global/sidebar/SidebarPremiumChip';
import InterviewsPricingTableDialog from '~/components/interviews/purchase/InterviewsPricingTableDialog';
@ -23,7 +27,14 @@ import ScrollArea from '~/components/ui/ScrollArea';
import Spinner from '~/components/ui/Spinner';
import Text from '~/components/ui/Text';
import type { QuestionListTypeData } from '../../common/QuestionsTypes';
import type {
QuestionListTypeData,
QuestionPracticeFormat,
} from '../../common/QuestionsTypes';
import {
questionsFrameworkTabs,
questionsLanguageTabs,
} from '../utils/QuestionsListTabsConfig';
export type QuestionListTypeWithLabel = QuestionListTypeData &
Readonly<{ label: string }>;
@ -38,6 +49,8 @@ function DropdownContent({
openPricingDialog: (feature: InterviewsPurchasePremiumFeature) => void;
}>) {
const intl = useIntl();
const searchParams = useSearchParams();
const currentTab = searchParams?.get('tab');
const { data: questionLists, isLoading } = trpc.questionLists.get.useQuery();
const { userProfile } = useUserProfile();
const formatData = useQuestionFormatsData();
@ -154,6 +167,9 @@ function DropdownContent({
({
menuType: 'item',
...item,
tab: questionsFrameworkTabs(item.value)?.includes('quiz')
? ((currentTab || item.tab) as QuestionPracticeFormat)
: item.tab,
}) as const,
),
{ menuType: 'divider', value: 'divider-1' },
@ -171,6 +187,9 @@ function DropdownContent({
({
menuType: 'item',
...item,
tab: questionsLanguageTabs().includes('quiz')
? ((currentTab || item.tab) as QuestionPracticeFormat)
: item.tab,
}) as const,
),
],
@ -182,6 +201,54 @@ function DropdownContent({
}),
menuType: 'list',
},
{
items: [
...questionLists.languages
.filter((item) => item.value !== 'ts')
.map(
(item) =>
({
...item,
label: intl.formatMessage(
{
defaultMessage: '{category} Quiz Questions',
description: 'Label for Quiz question',
id: '3A1yG3',
},
{
category: QuestionLanguageLabels[item.value],
},
),
menuType: 'item',
tab: 'quiz',
type: 'language',
}) as const,
),
{
label: intl.formatMessage(
{
defaultMessage: '{category} Quiz Questions',
description: 'Label for Quiz question',
id: '3A1yG3',
},
{
category: 'React',
},
),
menuType: 'item',
tab: 'quiz',
type: 'framework',
value: 'react',
},
],
key: 'quiz',
label: intl.formatMessage({
defaultMessage: 'Quizzes',
description: 'Tile for quiz question type',
id: 'QqddKP',
}),
menuType: 'list',
},
];
function activeAccordionItem() {

View File

@ -4,3 +4,8 @@ export const QuestionCountReact = 100;
export const QuestionCountCoding = 251;
export const QuestionCountSystemDesign = 19;
export const QuestionCountQuiz = 283;
export const QuestionCountReactCoding = 91;
export const QuestionCountJavaScriptCoding = 288;
export const QuestionCountTypeScriptCoding = 288;
export const QuestionCountHTMLCoding = 70;
export const QuestionCountCSSCoding = 43;

View File

@ -1,82 +1,90 @@
'use client';
import { useUser } from '@supabase/auth-helpers-react';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { Suspense } from 'react';
import QuestionProgressAction from '~/components/interviews/questions/common/QuestionProgressAction';
import QuestionReportIssueButton from '~/components/interviews/questions/common/QuestionReportIssueButton';
import type { QuestionListTypeData } from '~/components/interviews/questions/common/QuestionsTypes';
import InterviewsQuestionsListSlideOutButton from '~/components/interviews/questions/listings/slideout/InterviewsQuestionsListSlideOutButton';
import {
themeBackgroundDarkColor,
themeBorderColor,
} from '~/components/ui/theme';
import Text from '~/components/ui/Text';
import { themeBackgroundColor, themeBorderColor } from '~/components/ui/theme';
import { useQueryQuestionProgress } from '~/db/QuestionsProgressClient';
import { hashQuestion } from '~/db/QuestionsUtils';
type Props = Readonly<{
allowMarkComplete?: boolean;
initialListType?: QuestionListTypeData;
leftAddOnItem?: ReactNode;
listIsShownInSidebarOnDesktop: boolean;
metadata: React.ComponentProps<typeof QuestionProgressAction>['metadata'];
questionTitle?: string;
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE?: string | null;
studyListKey?: string;
}>;
export default function InterviewsStudyListBottomBar({
allowMarkComplete = true,
initialListType,
leftAddOnItem,
listIsShownInSidebarOnDesktop,
metadata,
questionTitle,
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE = 'qns_slideout',
studyListKey,
}: Props) {
const user = useUser();
const { isLoading } = useQueryQuestionProgress(
metadata,
studyListKey ?? null,
);
return (
<div
className={clsx(
'sticky inset-x-0 bottom-0',
'flex items-center justify-between gap-2 px-3 py-3',
'px-6 py-2.5',
['border-t', themeBorderColor],
themeBackgroundDarkColor,
themeBackgroundColor,
)}>
<div className="flex shrink-0 justify-center sm:order-2 sm:flex-1">
<Suspense>
<InterviewsQuestionsListSlideOutButton
currentQuestionHash={hashQuestion(metadata)}
listIsShownInSidebarOnDesktop={listIsShownInSidebarOnDesktop}
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE="qns_slideout"
studyListKey={studyListKey}
{questionTitle && (
<Text className="block pb-2.5 lg:hidden" size="body3">
{questionTitle}
</Text>
)}
<div className={clsx('flex items-center justify-between gap-2')}>
<div className="flex shrink-0 justify-center sm:order-2 sm:flex-1">
<Suspense>
<InterviewsQuestionsListSlideOutButton
currentQuestionHash={hashQuestion(metadata)}
initialListType={initialListType}
listIsShownInSidebarOnDesktop={listIsShownInSidebarOnDesktop}
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE={
slideOutSearchParam_MUST_BE_UNIQUE_ON_PAGE
}
studyListKey={studyListKey}
/>
</Suspense>
</div>
<div className="hidden gap-2 sm:flex sm:flex-1">
<QuestionReportIssueButton
entity="question"
format={metadata.format}
slug={metadata.slug}
/>
</Suspense>
</div>
<div className="hidden sm:flex sm:flex-1">
<QuestionReportIssueButton
entity="question"
format={metadata.format}
slug={metadata.slug}
/>
</div>
<div
className={clsx(
'flex justify-end sm:order-3 sm:flex-1',
'transition-colors',
isLoading && user != null ? 'opacity-0' : 'opacity-100',
)}>
<QuestionReportIssueButton
className="mr-2 sm:hidden"
entity="question"
format={metadata.format}
slug={metadata.slug}
/>
{allowMarkComplete && (
<QuestionProgressAction
metadata={metadata}
studyListKey={studyListKey}
/>
)}
{leftAddOnItem}
</div>
<div className={clsx('flex justify-end gap-3 sm:order-3 sm:flex-1')}>
<div className="flex gap-3 sm:hidden">
<QuestionReportIssueButton
entity="question"
format={metadata.format}
slug={metadata.slug}
/>
{leftAddOnItem}
</div>
{allowMarkComplete && (
<QuestionProgressAction
metadata={metadata}
studyListKey={studyListKey}
/>
)}
</div>
</div>
</div>
);

View File

@ -1,8 +1,24 @@
import type {
QuestionFramework,
QuestionListTypeData,
QuestionPracticeFormat,
} from '~/components/interviews/questions/common/QuestionsTypes';
export function questionsLanguageTabs() {
return ['coding', 'quiz'] as const;
}
export function questionsFrameworkTabs(framework: QuestionFramework) {
switch (framework) {
case 'react': {
return ['coding', 'quiz'] as const;
}
default: {
return null;
}
}
}
export function questionsListTabsConfig(
listType: QuestionListTypeData | null | undefined,
): ReadonlyArray<QuestionPracticeFormat> | null {
@ -15,12 +31,12 @@ export function questionsListTabsConfig(
return ['coding', 'system-design', 'quiz'];
}
case 'language': {
return listType.tab ? ['coding', 'quiz'] : null;
return listType.tab ? questionsLanguageTabs() : null;
}
case 'framework': {
switch (listType.value) {
case 'react': {
return listType.tab ? ['coding', 'quiz'] : null;
return listType.tab ? questionsFrameworkTabs('react') : null;
}
default: {
return null;

View File

@ -2,7 +2,6 @@ import { useSearchParams } from 'next/navigation';
import { trpc } from '~/hooks/trpc';
import { QuestionListTypeDefault } from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionFormatForList,
QuestionFramework,
@ -78,7 +77,7 @@ export function useQuestionsListTypeCurrent(
};
}
return QuestionListTypeDefault;
return null;
}
export function useQuestionsListDataForType(

View File

@ -21,6 +21,7 @@ type MetadataElement =
| 'users_completed';
type Props = Readonly<{
className?: string;
elements?: ReadonlyArray<MetadataElement>;
justify?: 'center' | 'start';
metadata: QuestionMetadata;
@ -36,6 +37,7 @@ const DEFAULT_ELEMENTS: ReadonlyArray<MetadataElement> = [
];
export default function QuestionMetadataSection({
className,
elements = DEFAULT_ELEMENTS,
justify = 'start',
metadata,
@ -47,7 +49,8 @@ export default function QuestionMetadataSection({
'flex flex-wrap items-center',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
'gap-x-6 gap-y-4 ',
'gap-x-6 gap-y-4',
className,
)}>
{elements.includes('author') && metadata.author && (
<QuestionAuthor author={metadata.author} size={size} />

View File

@ -12,4 +12,24 @@ const MDXComponentsForQuiz = Object.freeze({
pre: (props: ComponentProps<'pre'>) => <MDXCodeBlockExecutable {...props} />,
});
export default MDXComponentsForQuiz;
const MDXComponentsForScrollableQuiz = Object.freeze({
...MDXComponentsForQuiz,
h1: (props: ComponentProps<typeof MDXComponents.h2>) => (
<MDXComponents.h2 {...props} />
),
h2: (props: ComponentProps<typeof MDXComponents.h3>) => (
<MDXComponents.h3 {...props} />
),
h3: (props: ComponentProps<typeof MDXComponents.h4>) => (
<MDXComponents.h4 {...props} />
),
h4: (props: ComponentProps<typeof MDXComponents.h5>) => (
<MDXComponents.h5 {...props} />
),
h5: (props: ComponentProps<typeof MDXComponents.h6>) => (
<MDXComponents.h6 {...props} />
),
pre: (props: ComponentProps<'pre'>) => <MDXCodeBlockExecutable {...props} />,
});
export { MDXComponentsForQuiz, MDXComponentsForScrollableQuiz };

View File

@ -61,6 +61,7 @@ function Anchor(
typeof href === 'string' ? /^(http|mailto)/.test(href ?? '') : false;
const finalHref = href ?? '#';
const rel = relProp ?? (isExternalURL ? 'noreferrer noopener' : undefined);
const className = anchorVariants({
className: clsx(

View File

@ -27,6 +27,7 @@ type TabAlignment = 'start' | 'stretch';
type Props<T> = Readonly<{
alignment?: TabAlignment;
className?: string;
display?: TabDisplay;
label: string;
onSelect?: (value: T) => void;
@ -76,6 +77,7 @@ const sizeClasses: Record<
function TabsUnderline<T>(
{
alignment = 'start',
className,
display = 'block',
label,
onSelect,
@ -95,6 +97,7 @@ function TabsUnderline<T>(
className={clsx('overflow-y-hidden', displayClasses[display], [
'border-b',
themeBorderElementColor,
className,
])}>
<nav aria-label={label} className={clsx('-mb-px flex', tabGapSize)}>
{tabs.map((tabItem) => {

View File

@ -8,7 +8,10 @@ import { useCallback, useEffect, useState } from 'react';
import { RiCodeLine } from 'react-icons/ri';
import InterviewsPremiumBadge from '~/components/interviews/common/InterviewsPremiumBadge';
import { questionHrefWithListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
import {
questionHrefWithListType,
QuestionListTypeDefault,
} from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionFramework,
QuestionMetadata,
@ -642,7 +645,9 @@ export default function UserInterfaceCodingWorkspace({
const { activeFile, visibleFiles } = sandpack;
const { framework, metadata } = question;
const listType = useQuestionsListTypeCurrent(studyListKey, framework);
const listType =
useQuestionsListTypeCurrent(studyListKey, framework) ??
QuestionListTypeDefault;
const frameworkSolutionPath = questionHrefWithListType(
questionUserInterfaceSolutionPath(metadata, framework),
listType,

View File

@ -1,6 +1,9 @@
'use client';
import { questionHrefWithListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
import {
questionHrefWithListType,
QuestionListTypeDefault,
} from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionFramework,
QuestionMetadata,
@ -36,7 +39,8 @@ export default function UserInterfaceCodingWorkspacePage({
question: { metadata },
} = props;
const listType = useQuestionsListTypeCurrent(studyListKey);
const listType =
useQuestionsListTypeCurrent(studyListKey) ?? QuestionListTypeDefault;
return (
<UserInterfaceCodingWorkspaceSection

View File

@ -3,7 +3,10 @@ import { RiArrowRightUpLine } from 'react-icons/ri';
import { useQuestionFrameworksData } from '~/data/QuestionCategories';
import { questionHrefFrameworkSpecificAndListType } from '~/components/interviews/questions/common/QuestionHrefUtils';
import {
questionHrefFrameworkSpecificAndListType,
QuestionListTypeDefault,
} from '~/components/interviews/questions/common/QuestionHrefUtils';
import type {
QuestionFramework,
QuestionMetadata,
@ -67,7 +70,9 @@ export default function UserInterfaceCodingWorkspaceWriteup({
writeup,
}: Props) {
const copyRef = useQuestionLogEventCopyContents<HTMLDivElement>();
const listType = useQuestionsListTypeCurrent(studyListKey, framework);
const listType =
useQuestionsListTypeCurrent(studyListKey, framework) ??
QuestionListTypeDefault;
const { data } = useQueryQuestionProgress(metadata, studyListKey ?? null);
const { save } = useUserInterfaceCodingWorkspaceSavesContext();
const intl = useIntl();

View File

@ -11,6 +11,7 @@ import { BlogPostDocument } from '../components/blog/contentlayer/BlogPostDocume
import { BlogSeriesDocument } from '../components/blog/contentlayer/BlogSeriesDocument';
import { BlogSubseriesDocument } from '../components/blog/contentlayer/BlogSubseriesDocument';
import { JobsPostingDocument } from '../components/hiring/contentlayer/JobsPostingDocument';
import { InterviewsQuestionQuizScrollableContentDocument } from '../components/interviews/questions/content/quiz/InterviewsQuestionQuizScrollableContentDocument';
import { InterviewsListingBottomContentDocument } from '../components/interviews/questions/listings/InterviewsListingBottomContentDocument';
import { InterviewsStudyListDocument } from '../components/interviews/questions/listings/study-list/InterviewsStudyListDocument';
import { ProjectsChallengeAPIWriteupDocument } from '../components/projects/contentlayer/ProjectsChallengeAPIWriteupDocument';
@ -36,6 +37,7 @@ export default makeSource({
BlogSubseriesDocument,
InterviewsStudyListDocument,
InterviewsListingBottomContentDocument,
InterviewsQuestionQuizScrollableContentDocument,
ProjectsCommonGuideDocument,
ProjectsChallengeAppendixDocument,
ProjectsChallengeBriefDocument,

View File

@ -0,0 +1,17 @@
Looking to ace your next CSS interview questions? Youre in the right place.
CSS interview questions test your core styling expertise. Interviewers typically focus on topics such as:
- **Specificity & Cascade:** Understanding how CSS rules compete and how to control which styles win.
- **Box Model:** Mastering content, padding, border, and margin to build precise layouts.
- **Flexbox & Grid:** Creating flexible, responsive layouts with modern CSS layout systems.
- **Responsive Design:** Making designs adapt gracefully across screen sizes using media queries and fluid units.
- **Selectors & Combinators:** Targeting elements efficiently with class, attribute, pseudo-class, and pseudo-element selectors.
- **Performance & Optimization:** Writing lean, maintainable CSS and minimizing repaint/reflow overhead.
Below, youll find **<QuestionCount/>+ curated CSS interview questions**, covering everything from foundational concepts to advanced layout and optimization strategies. Each question includes:
- **Quick Answers (TL;DR):** Concise responses to help you answer on the spot.
- **Detailed Explanations:** In-depth insights to ensure you understand not just the “how,” but the “why.”
These questions are crafted by **senior and staff engineers from top tech companies**—not anonymous contributors or AI-generated content. Start practicing below and get ready to stand out in your CSS interview!

View File

@ -0,0 +1,17 @@
HTML interview questions are designed to assess your understanding of web development fundamentals and best practices. Interviewers typically focus on key topics such as:
- **Accessibility:** Ensuring websites are accessible to users with disabilities using semantic HTML and ARIA roles.
- **Semantics:** Recognizing the importance of semantic HTML tags for SEO, accessibility, and code clarity.
- **Forms:** Building forms with proper input validation, accessibility features, and efficient handling of form submissions.
- **Multimedia:** Embedding and managing images, audio, and video in HTML while optimizing for performance and accessibility.
- **Best Practices:** Structuring HTML for readability, maintainability, and performance, including the proper use of meta tags, link attributes, and media queries.
- **SEO Optimization:** Using semantic HTML elements and metadata to boost search engine ranking and improve web performance.
Below, youll find **<QuestionCount/>+ carefully curated HTML interview questions** covering everything from core concepts to best practices and optimization strategies.
Each question includes:
- **Quick Answers (TL;DR):** Concise, clear responses to help you answer confidently.
- **Detailed Explanations:** In-depth insights to ensure you not only know the answers but understand the reasoning behind them.
Unlike most lists, our questions are carefully curated by **real senior and staff engineers from top tech companies** like Amazon, Meta, and more—not anonymous contributors or AI-generated content. Start practicing below and get ready to ace your HTML interview!

View File

@ -0,0 +1,10 @@
Tired of scrolling through low-quality JavaScript interview questions? Youve found the right place!
Our JavaScript interview questions are crafted by experienced ex-FAANG senior / staff engineers, not random unverified sources or AI.
With over <QuestionCount/>+ questions covering everything from core JavaScript concepts to advanced JavaScript features (async / await, promises, etc.), youll be fully prepared.
Each quiz question comes with:
- **Concise answers (TL;DR):** Clear and to-the-point solutions to help you respond confidently during interviews.
- **Comprehensive explanations:** In-depth insights to ensure you fully understand the concepts and can elaborate when required. Dont waste time elsewhere—start practicing with the best!

View File

@ -0,0 +1,15 @@
In real-world scenarios, mastering React goes far beyond just building components. Its about creating efficient, reusable, and performant applications. React interviewers typically focus on key areas such as:
- **Component Lifecycle:** Understanding how components mount, update, and unmount is crucial for managing UI and state.
- **State and Props Management:** Knowing when and how to use props, state, and context to share data across components.
- **Hooks:** Leveraging React hooks like useState, useEffect, and useReducer to simplify logic and manage side effects.
- **Performance Optimization:** Efficient rendering, memoization, and handling large datasets.
- **Testing:** Writing robust tests for React components using tools like Jest and React Testing Library.
- **Routing:** Managing views and navigation in single-page applications with React Router.
Below, youll find <QuestionCount />+ expertly curated questions covering everything from component lifecycle and state management to hooks and performance optimization. Each question includes:
- **Quick answers (TL;DR):** Clear, concise responses to help you answer confidently.
- **In-depth explanations:** Detailed insights to ensure you fully understand each concept.
Best of all, our list is crafted by senior and staff engineers from top tech companies, not unverified or AI-generated content. Dont waste time—prepare with real, experienced-backed React interview questions!

View File

@ -530,6 +530,17 @@ export const QuestionFrameworkLabels: Record<QuestionFramework, string> = {
vue: 'Vue',
};
export const QuestionFrameworkRawToSEOMapping: Record<
QuestionFramework,
QuestionFrameworkSEO
> = {
angular: 'angular-interview-questions',
react: 'react-interview-questions',
svelte: 'svelte-interview-questions',
vanilla: 'vanilla-javascript-interview-questions',
vue: 'vue-interview-questions',
};
export function getQuestionFrameworksData(
intl: IntlShape,
): QuestionCategoryLists<QuestionFramework, QuestionFrameworkSEO> {

View File

@ -7,6 +7,7 @@ import { questionsFindClosestToSlug } from '~/components/interviews/questions/co
import type {
QuestionFramework,
QuestionJavaScript,
QuestionListTypeData,
QuestionMetadata,
QuestionQuiz,
QuestionSystemDesign,
@ -179,6 +180,25 @@ export async function readQuestionQuizContents(
};
}
export async function readQuestionQuizContentsAll(
listType: QuestionListTypeData,
requestedLocale = 'en-US',
): Promise<ReadonlyArray<{
exactMatch: boolean;
loadedLocale: string;
question: QuestionQuiz;
}> | null> {
const { questions } = await fetchQuestionsList(listType, requestedLocale);
const questionsContents = await Promise.all(
questions.map((question) =>
readQuestionQuizContents(question.slug, requestedLocale),
),
);
return questionsContents.flatMap((qn) => (qn != null ? [qn] : []));
}
export function readQuestionSystemDesignContents(
slug: string,
requestedLocale = 'en-US',

View File

@ -0,0 +1,17 @@
import type { InterviewsQuestionQuizScrollableContent } from 'contentlayer/generated';
import { allInterviewsQuestionQuizScrollableContents } from '~/../.contentlayer/generated/InterviewsQuestionQuizScrollableContent/_index.mjs';
export async function fetchInterviewsQuestionQuizScrollScrollableContent(
slug: string,
locale: string,
): Promise<InterviewsQuestionQuizScrollableContent | undefined> {
const studyLists = (
allInterviewsQuestionQuizScrollableContents as ReadonlyArray<InterviewsQuestionQuizScrollableContent>
).filter((content) => content.slug === slug);
return (
studyLists.find((content) => content.locale === locale) ??
studyLists.find((content) => content.locale === 'en-US')!
);
}

View File

@ -0,0 +1,28 @@
import { useEffect, useRef } from 'react';
export function useEnterViewport(callback: (isInView: boolean) => void) {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const element = ref.current;
if (element == null) {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
callback(entry.isIntersecting);
},
{
threshold: 0.1,
},
);
observer.observe(element);
return () => observer.disconnect();
}, [callback]);
return ref;
}

View File

@ -0,0 +1,23 @@
import { useEffect, useState } from 'react';
export default function useHashChange() {
const [hash, setHash] = useState(
typeof window === 'undefined' ? null : window.location.hash,
);
useEffect(() => {
function handleHashChange() {
setHash(window.location.hash);
}
window.addEventListener('hashchange', handleHashChange);
handleHashChange();
return () => {
window.removeEventListener('hashchange', handleHashChange);
};
}, []);
return hash;
}

View File

@ -767,6 +767,12 @@
"value": "Warm up question"
}
],
"/mpSVj": [
{
"type": 0,
"value": "Scrolling view"
}
],
"/n0jtw": [
{
"type": 0,
@ -1910,6 +1916,16 @@
"value": "Gamification and progress tracking"
}
],
"2HQlrS": [
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " Interview Questions and Answers | by Ex-FAANG interviewers"
}
],
"2Hqklu": [
{
"type": 0,
@ -2050,6 +2066,12 @@
"value": "Algorithmic Coding Interview Questions"
}
],
"2TPnld": [
{
"type": 0,
"value": "Instant UI preview for UI-related questions"
}
],
"2a6qgD": [
{
"type": 0,
@ -2377,6 +2399,16 @@
"value": "Coding-focused"
}
],
"3A1yG3": [
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " Quiz Questions"
}
],
"3C1g+S": [
{
"type": 0,
@ -3115,6 +3147,16 @@
"value": "Start a new version"
}
],
"4gumtt": [
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " Interview Questions"
}
],
"4jymHU": [
{
"type": 1,
@ -4141,6 +4183,12 @@
"value": " users in our private Discord community for Premium users"
}
],
"71Hlcg": [
{
"type": 0,
"value": "Reference solutions from ex-interviewers at Big Tech companies"
}
],
"72GwzV": [
{
"type": 0,
@ -6897,6 +6945,16 @@
"value": "Progress imported successfully"
}
],
"Dabxa8": [
{
"type": 1,
"value": "questionCount"
},
{
"type": 0,
"value": "+ most important TypeScript interview questions and answers in quiz-style format, answered by ex-FAANG interviewers"
}
],
"Dc5QrJ": [
{
"type": 0,
@ -7450,6 +7508,24 @@
"value": "Project started! Leverage the provided resources and submit a link to your site once ready!"
}
],
"EtDVci": [
{
"type": 1,
"value": "questionCount"
},
{
"type": 0,
"value": "+ "
},
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " interview questions and answers in quiz-style format, answered by ex-FAANG interviewers"
}
],
"Euaee/": [
{
"type": 0,
@ -7770,6 +7846,12 @@
"value": "JavaScript Functions"
}
],
"Fxe4CE": [
{
"type": 0,
"value": "Total due"
}
],
"FyfO1X": [
{
"type": 0,
@ -7924,6 +8006,26 @@
"value": "Get full access"
}
],
"GPM7SF": [
{
"type": 0,
"value": "New File"
}
],
"GPQg5j": [
{
"type": 0,
"value": "If you're looking for "
},
{
"type": 1,
"value": "languageOrFramework"
},
{
"type": 0,
"value": " coding questions -"
}
],
"GPp0wf": [
{
"type": 0,
@ -11749,6 +11851,12 @@
"value": "Support us by starring our GitHub repo and consider contributing!"
}
],
"P6sA19": [
{
"type": 0,
"value": "We've got you covered as well, with:"
}
],
"P99cBr": [
{
"type": 0,
@ -12397,6 +12505,12 @@
"value": "Blog | GreatFrontEnd"
}
],
"Q891Ht": [
{
"type": 0,
"value": "One-click automated, transparent test cases"
}
],
"QAFkOT": [
{
"type": 0,
@ -13224,6 +13338,12 @@
"value": "Privacy policy"
}
],
"Rxyjw3": [
{
"type": 0,
"value": "Introduction"
}
],
"RyuIJe": [
{
"type": 0,
@ -13911,6 +14031,12 @@
"value": "Tell us anything - about your journey as a front end developer, your goals and next steps, or how you want to connect with others"
}
],
"THbGFn": [
{
"type": 0,
"value": "Questions appear on different pages"
}
],
"TJD+8A": [
{
"type": 0,
@ -15636,6 +15762,12 @@
"value": "How to use Figma for development"
}
],
"XCqQUO": [
{
"type": 0,
"value": "Get Started"
}
],
"XG1Wfg": [
{
"type": 0,
@ -16100,6 +16232,12 @@
"value": "As mentioned above, there are many ways to host your project for free. Our recommend hosts are:"
}
],
"YEEd13": [
{
"type": 0,
"value": "In-browser coding workspace that mimics real interview conditions"
}
],
"YFKKP4": [
{
"type": 0,
@ -17098,6 +17236,12 @@
"value": "Outdent line"
}
],
"al0GiV": [
{
"type": 0,
"value": "There was a technical error while processing the payment."
}
],
"alG3Yi": [
{
"type": 0,
@ -17546,6 +17690,16 @@
"value": "Now in BETA."
}
],
"bYOK+T": [
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " Interview Questions and Answers"
}
],
"bYY9Mh": [
{
"type": 0,
@ -20878,6 +21032,28 @@
"value": "In Content Ad Placement in system design"
}
],
"inlaDX": [
{
"type": 0,
"value": "Practice "
},
{
"type": 1,
"value": "questionCount"
},
{
"type": 0,
"value": "+ curated "
},
{
"type": 1,
"value": "category"
},
{
"type": 0,
"value": " Interview Questions in-browser, with solutions and test cases from big tech ex-interviewers"
}
],
"iqz/fo": [
{
"type": 0,
@ -21020,6 +21196,20 @@
"value": "Stopped at"
}
],
"jL6ul2": [
{
"type": 0,
"value": "Join "
},
{
"type": 1,
"value": "engineersCount"
},
{
"type": 0,
"value": "+ engineers"
}
],
"jLHxac": [
{
"type": 0,
@ -21316,6 +21506,12 @@
"value": "Suitable to get your foot in the door."
}
],
"jjdVOP": [
{
"type": 0,
"value": "All questions appear on a single page"
}
],
"jlTtgt": [
{
"type": 0,
@ -21360,6 +21556,24 @@
"value": "Regular payouts through PayPal"
}
],
"jrT2Ow": [
{
"type": 1,
"value": "questionCount"
},
{
"type": 0,
"value": "+ "
},
{
"type": 1,
"value": "languageOrFramework"
},
{
"type": 0,
"value": " Coding Questions"
}
],
"jtnskJ": [
{
"type": 0,
@ -22537,6 +22751,16 @@
"value": "In Content Ad Placement in guides"
}
],
"mLrZGz": [
{
"type": 1,
"value": "amountOff"
},
{
"type": 0,
"value": " off"
}
],
"mNb3b2": [
{
"type": 0,
@ -23105,6 +23329,12 @@
"value": "More settings"
}
],
"nSJ/yC": [
{
"type": 0,
"value": "Continue"
}
],
"nTu2nu": [
{
"type": 0,
@ -23285,6 +23515,16 @@
"value": "."
}
],
"nnXIFO": [
{
"type": 1,
"value": "questionCount"
},
{
"type": 0,
"value": "+ most important JavaScript interview questions and answers, covering everything from core concepts to advanced JavaScript features"
}
],
"nodFHk": [
{
"type": 0,
@ -23668,6 +23908,12 @@
"value": "."
}
],
"ofJOh2": [
{
"type": 0,
"value": "Javascript coding"
}
],
"ofUX5S": [
{
"type": 0,
@ -24245,22 +24491,6 @@
"value": "OFF"
}
],
"pj07uD": [
{
"type": 0,
"value": "Mark complete"
}
],
"pjH0jb": [
{
"type": 1,
"value": "name"
},
{
"type": 0,
"value": " front end interview questions"
}
],
"pjndkX": [
{
"type": 0,
@ -24983,6 +25213,12 @@
"value": "No Projects profile"
}
],
"rCqW54": [
{
"type": 0,
"value": "Page-by-page"
}
],
"rGbEEY": [
{
"type": 0,
@ -25793,6 +26029,16 @@
"value": "Starter"
}
],
"tRQG6L": [
{
"type": 1,
"value": "percentOff"
},
{
"type": 0,
"value": "% off"
}
],
"tWtbVf": [
{
"type": 0,
@ -26447,6 +26693,12 @@
"value": "View other's code and learn from their approach"
}
],
"vLsSQG": [
{
"type": 0,
"value": "Sub total"
}
],
"vMy9f3": [
{
"type": 0,
@ -28128,6 +28380,12 @@
"value": "What do I do once I'm done coding?"
}
],
"zI3w61": [
{
"type": 0,
"value": "Instantly preview your code for UI questions"
}
],
"zIQsmk": [
{
"type": 0,
@ -28549,4 +28807,4 @@
"value": "Delete ad"
}
]
}
}

View File

@ -403,6 +403,10 @@
"defaultMessage": "Warm up question",
"description": "Label for warm up questions"
},
"/mpSVj": {
"defaultMessage": "Scrolling view",
"description": "Label for quiz scroll mode toggle button"
},
"/n0jtw": {
"defaultMessage": "Contact your bank to get a new card issued.",
"description": "Reason label for new card issue"
@ -907,6 +911,10 @@
"defaultMessage": "Gamification and progress tracking",
"description": "Answer to 'What is unique about GreatFrontEnd Projects vs other challenge platforms?' on projects FAQs"
},
"2HQlrS": {
"defaultMessage": "{category} Interview Questions and Answers | by Ex-FAANG interviewers",
"description": "Title of quiz scrolling mode page"
},
"2Hqklu": {
"defaultMessage": "You contributed a code review for <recipientProfileLink>{recipient}</recipientProfileLink> on their submission <link>{submissionTitle}</link>: <comment>\"{description}\"</comment>",
"description": "Log message for you reviewing others submission"
@ -967,6 +975,10 @@
"defaultMessage": "Algorithmic Coding Interview Questions",
"description": "Social title for algo coding question format page"
},
"2TPnld": {
"defaultMessage": "Instant UI preview for UI-related questions",
"description": "Label for code preview features"
},
"2a6qgD": {
"defaultMessage": "Your code was restored from client storage. <link>Reset to default</link>",
"description": "Message that appears under the coding workspace when user has previously worked on this problem and we restored their code"
@ -1071,6 +1083,10 @@
"defaultMessage": "Coding-focused",
"description": "Coding workspace layout name"
},
"3A1yG3": {
"defaultMessage": "{category} Quiz Questions",
"description": "Label for Quiz question"
},
"3C1g+S": {
"defaultMessage": "Don't settle for single placements - we'll give you repeated ad exposure",
"description": "Advertise with us section title"
@ -1423,6 +1439,10 @@
"defaultMessage": "Start a new version",
"description": "Start a new version of the question button label"
},
"4gumtt": {
"defaultMessage": "{category} Interview Questions",
"description": "Title of scrolling mode page"
},
"4jymHU": {
"defaultMessage": "{skipCount} skipped",
"description": "Workspace test outcome skipped label"
@ -1903,6 +1923,10 @@
"defaultMessage": "Join over {userCount} users in our private Discord community for Premium users",
"description": "Button subtitle for joining the Premium Discord community on interviews payment success page"
},
"71Hlcg": {
"defaultMessage": "Reference solutions from ex-interviewers at Big Tech companies",
"description": "Label for coding questions solution"
},
"72GwzV": {
"defaultMessage": "Available in {frameworkLabel}",
"description": "Label indicating what JavaScript frameworks this question is available in"
@ -3255,6 +3279,10 @@
"defaultMessage": "Progress imported successfully",
"description": "Success message for import progress"
},
"Dabxa8": {
"defaultMessage": "{questionCount}+ most important TypeScript interview questions and answers in quiz-style format, answered by ex-FAANG interviewers",
"description": "Description of Typescript quiz scrolling mode page"
},
"Dc5QrJ": {
"defaultMessage": "Coding",
"description": "Tile for coding question type"
@ -3523,6 +3551,10 @@
"defaultMessage": "Project started! Leverage the provided resources and submit a link to your site once ready!",
"description": "Toast subtitle for project session started"
},
"EtDVci": {
"defaultMessage": "{questionCount}+ {category} interview questions and answers in quiz-style format, answered by ex-FAANG interviewers",
"description": "Description of scroll mode quiz questions page"
},
"Euaee/": {
"defaultMessage": "Resubscribe to unlock",
"description": "Title for a premium project paywall"
@ -3691,6 +3723,10 @@
"defaultMessage": "JavaScript Functions",
"description": "Page title for JavaScript coding question format"
},
"Fxe4CE": {
"defaultMessage": "Total due",
"description": "Label for total due price"
},
"FyfO1X": {
"defaultMessage": "Front End System Design Playbook",
"description": "Front end system design guidebook title"
@ -3779,6 +3815,14 @@
"defaultMessage": "Get full access",
"description": "Button CTA to encourage upgrading"
},
"GPM7SF": {
"defaultMessage": "New File",
"description": "Button tooltip for creating a new file"
},
"GPQg5j": {
"defaultMessage": "If you're looking for {languageOrFramework} coding questions -",
"description": "Header for coding section in interview quiz page"
},
"GPp0wf": {
"defaultMessage": "Welcome to GreatFrontEnd Projects!",
"description": "Title for Projects onboarding page"
@ -5747,6 +5791,10 @@
"defaultMessage": "Support us by starring our GitHub repo and consider contributing!",
"description": "Description for github star"
},
"P6sA19": {
"defaultMessage": "We've got you covered as well, with:",
"description": "Subheader for coding section in interview quiz page"
},
"P99cBr": {
"defaultMessage": "Overview of various question formats",
"description": "Overview of front end system design interview question formats"
@ -5995,6 +6043,10 @@
"defaultMessage": "Blog | GreatFrontEnd",
"description": "Title of GreatFrontEnd blog homepage"
},
"Q891Ht": {
"defaultMessage": "One-click automated, transparent test cases",
"description": "Label for test cases in coding questions"
},
"QAFkOT": {
"defaultMessage": "No coupons available",
"description": "Title of empty state on coupons page"
@ -6427,6 +6479,10 @@
"defaultMessage": "Privacy policy",
"description": "Link to privacy policy page"
},
"Rxyjw3": {
"defaultMessage": "Introduction",
"description": "Title for introduction section in quiz questions list"
},
"RyuIJe": {
"defaultMessage": "Your request has been submitted successfully",
"description": "Success message for request submission"
@ -6739,6 +6795,10 @@
"defaultMessage": "Tell us anything - about your journey as a front end developer, your goals and next steps, or how you want to connect with others",
"description": "Bio field description"
},
"THbGFn": {
"defaultMessage": "Questions appear on different pages",
"description": "Tooltip for quiz scroll mode toggle button"
},
"TJD+8A": {
"defaultMessage": "Project steps",
"description": "Label for Project steps tabs"
@ -7619,6 +7679,10 @@
"defaultMessage": "How to use Figma for development",
"description": "Figma guide link"
},
"XCqQUO": {
"defaultMessage": "Get Started",
"description": "Label for get started button"
},
"XG1Wfg": {
"defaultMessage": "Join Discord (premium)",
"description": "Tooltip for join premium discord"
@ -7831,6 +7895,10 @@
"defaultMessage": "As mentioned above, there are many ways to host your project for free. Our recommend hosts are:",
"description": "Text for Deployment Info in Projects"
},
"YEEd13": {
"defaultMessage": "In-browser coding workspace that mimics real interview conditions",
"description": "Label for browser-based coding environment"
},
"YFKKP4": {
"defaultMessage": "Expires",
"description": "Expiration date"
@ -8323,6 +8391,10 @@
"defaultMessage": "Outdent line",
"description": "Text describing outdent line command in the coding workspace shortcuts menu"
},
"al0GiV": {
"defaultMessage": "There was a technical error while processing the payment.",
"description": "Decline message for technical error"
},
"alG3Yi": {
"defaultMessage": "Reputation",
"description": "Subtext for Reputation Reputation"
@ -8583,6 +8655,10 @@
"defaultMessage": "Now in BETA.",
"description": "Project marketing hero section badge"
},
"bYOK+T": {
"defaultMessage": "{category} Interview Questions and Answers",
"description": "SEO title of quiz scrolling mode page"
},
"bYY9Mh": {
"defaultMessage": "Crop your new profile photo",
"description": "Dialog title for crop profile photo"
@ -10155,6 +10231,10 @@
"defaultMessage": "In Content Ad Placement in system design",
"description": "Alt text for ads in content placement preview"
},
"inlaDX": {
"defaultMessage": "Practice {questionCount}+ curated {category} Interview Questions in-browser, with solutions and test cases from big tech ex-interviewers",
"description": "Description of quiz scrolling mode page"
},
"iqz/fo": {
"defaultMessage": "Types of JS questions, concepts to cover and rubrics",
"description": "JavaScript questions, concepts and rubrics during front end interviews"
@ -10247,6 +10327,10 @@
"defaultMessage": "Stopped at",
"description": "Stopped at label"
},
"jL6ul2": {
"defaultMessage": "Join {engineersCount}+ engineers",
"description": "Label for total engineers using the platform"
},
"jLHxac": {
"defaultMessage": "Coming Soon",
"description": "Coming soon label"
@ -10367,6 +10451,10 @@
"defaultMessage": "Suitable to get your foot in the door.",
"description": "Resume review starter pricing tier description"
},
"jjdVOP": {
"defaultMessage": "All questions appear on a single page",
"description": "Tooltip for quiz scroll mode toggle button"
},
"jlTtgt": {
"defaultMessage": "Please <link>leave us an email</link> if you have any other needs or wants. We would love to discuss them!",
"description": "Question base section subtitle - third paragraph"
@ -10387,6 +10475,10 @@
"defaultMessage": "Regular payouts through PayPal",
"description": "Affiliate program payout section title"
},
"jrT2Ow": {
"defaultMessage": "{questionCount}+ {languageOrFramework} Coding Questions",
"description": "Number of questions in coding section"
},
"jtnskJ": {
"defaultMessage": "Creator's YOE",
"description": "Label for experience filter for submissions list"
@ -10939,6 +11031,10 @@
"defaultMessage": "In Content Ad Placement in guides",
"description": "Alt text for ads in content placement preview"
},
"mLrZGz": {
"defaultMessage": "{amountOff} off",
"description": "Label for promotion code discount"
},
"mNb3b2": {
"defaultMessage": "Hard",
"description": "Hard difficulty question"
@ -11163,6 +11259,10 @@
"defaultMessage": "More settings",
"description": "Button label to show more settings for the console in the coding workspace"
},
"nSJ/yC": {
"defaultMessage": "Continue",
"description": "Label for continue to checkout button"
},
"nTu2nu": {
"defaultMessage": "Privacy Policy",
"description": "Title of Privacy Policy page"
@ -11255,6 +11355,10 @@
"defaultMessage": "We have a \"fair use\" refund policy within 7 days of starting your first subscription. \"Fair use\" means that if you have accessed a significant amount of premium interview content (more than 5 premium content pieces), we reserve the right to reject the request for a refund. To request a refund, send an email to {contactEmail}.",
"description": "Answer to 'What's the refund policy for GreatFrontEnd Interviews?' on Purchase's FAQ sections"
},
"nnXIFO": {
"defaultMessage": "{questionCount}+ most important JavaScript interview questions and answers, covering everything from core concepts to advanced JavaScript features",
"description": "Description of quiz scrolling mode page"
},
"nodFHk": {
"defaultMessage": "Go to this submission's GitHub repository",
"description": "Tooltip for view code button"
@ -11399,6 +11503,10 @@
"defaultMessage": "Check out other available <link>promotions</link>.",
"description": "Promotional message"
},
"ofJOh2": {
"defaultMessage": "Javascript coding",
"description": "Alt text for ads in content placement preview"
},
"ofUX5S": {
"defaultMessage": "Challenge status",
"description": "Label for Status filter for submissions list"
@ -11647,14 +11755,6 @@
"defaultMessage": "OFF",
"description": "Amount cashback/discount"
},
"pj07uD": {
"defaultMessage": "Mark complete",
"description": "Mark the question as complete"
},
"pjH0jb": {
"defaultMessage": "{name} front end interview questions",
"description": "Label for company guides"
},
"pjndkX": {
"defaultMessage": "Unvote",
"description": "Vote button label"
@ -11975,6 +12075,10 @@
"defaultMessage": "No Projects profile",
"description": "User does not have a profile for the Projects product"
},
"rCqW54": {
"defaultMessage": "Page-by-page",
"description": "Label for quiz scroll mode toggle button"
},
"rGbEEY": {
"defaultMessage": "Resources & discussions",
"description": "Subtitle for \"Tips, Resources and Discussions\" tab on Projects project page"
@ -12395,6 +12499,10 @@
"defaultMessage": "Starter",
"description": "Starter blog"
},
"tRQG6L": {
"defaultMessage": "{percentOff}% off",
"description": "Label for promotion code discount"
},
"tWtbVf": {
"defaultMessage": "OOP",
"description": "Computer science topic"
@ -12759,6 +12867,10 @@
"defaultMessage": "View other's code and learn from their approach",
"description": "Description for recommended action for new projects platform user"
},
"vLsSQG": {
"defaultMessage": "Sub total",
"description": "Label for sub total price"
},
"vMy9f3": {
"defaultMessage": "Months",
"description": "Title of months accordion in Roadmap Slide out filter"
@ -13603,6 +13715,10 @@
"defaultMessage": "What do I do once I'm done coding?",
"description": "FAQ question for projects platform"
},
"zI3w61": {
"defaultMessage": "Instantly preview your code for UI questions",
"description": "Label for code preview features"
},
"zIQsmk": {
"defaultMessage": "Terms of Service",
"description": "Link to terms of service page"

View File

@ -668,6 +668,9 @@ importers:
uuid:
specifier: 9.0.1
version: 9.0.1
virtua:
specifier: ^0.41.5
version: 0.41.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.30.1)
web-vitals:
specifier: 5.0.3
version: 5.0.3
@ -9388,6 +9391,7 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
@ -10222,6 +10226,26 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
virtua@0.41.5:
resolution: {integrity: sha512-x1vsA9qIQNBFcCs1rzCjyYdMvDu/kT6o6zwwQnyqFOFdOyIzqyzU3WfR/hJC8WxUZXSCo2LkuoqapL8VDDMQPg==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'
solid-js: '>=1.0'
svelte: '>=5.0'
vue: '>=3.2'
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
solid-js:
optional: true
svelte:
optional: true
vue:
optional: true
vite-node@3.1.3:
resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -21805,6 +21829,12 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
virtua@0.41.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.30.1):
optionalDependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
svelte: 5.30.1
vite-node@3.1.3(@types/node@22.15.0)(jiti@1.21.7)(sugarss@4.0.1(postcss@8.5.6))(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
cac: 6.7.14