[web] qns/quiz: add quiz scrolling mode (#1661)
Co-authored-by: Yangshun <tay.yang.shun@gmail.com>
This commit is contained in:
parent
9a23093e76
commit
9a1bba26e7
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)} />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}));
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
Looking to ace your next CSS interview questions? You’re 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, you’ll 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!
|
||||
|
|
@ -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, you’ll 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!
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Tired of scrolling through low-quality JavaScript interview questions? You’ve 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.), you’ll 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. Don’t waste time elsewhere—start practicing with the best!
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
In real-world scenarios, mastering React goes far beyond just building components. It’s 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, you’ll 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. Don’t waste time—prepare with real, experienced-backed React interview questions!
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')!
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue