mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2026-02-13 23:56:22 +08:00
[questions][ui] restyle landing, question pages (#517)
This commit is contained in:
@@ -25,7 +25,7 @@ export default function AnswerCommentListItem({
|
||||
useAnswerCommentVote(answerCommentId);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 border bg-white p-2 ">
|
||||
<div className="flex gap-4 rounded-md border bg-white p-2">
|
||||
<VotingButtons
|
||||
size="sm"
|
||||
upvoteCount={upvoteCount}
|
||||
|
||||
@@ -20,15 +20,13 @@ export type LandingQueryData = {
|
||||
companySlug: string;
|
||||
location: string;
|
||||
questionType: QuestionsQuestionType;
|
||||
};
|
||||
} | null;
|
||||
|
||||
export type LandingComponentProps = {
|
||||
onLanded: (data: LandingQueryData) => void;
|
||||
};
|
||||
|
||||
export default function LandingComponent({
|
||||
onLanded: handleLandingQuery,
|
||||
}: LandingComponentProps) {
|
||||
export default function LandingComponent({ onLanded }: LandingComponentProps) {
|
||||
const defaultCompany = useDefaultCompany();
|
||||
const defaultLocation = useDefaultLocation();
|
||||
|
||||
@@ -70,17 +68,17 @@ export default function LandingComponent({
|
||||
<main className="flex flex-1 flex-col items-center overflow-y-auto bg-white">
|
||||
<div className="flex flex-1 flex-col items-start justify-center gap-12 px-4">
|
||||
<header className="flex flex-col items-start gap-16">
|
||||
<div className="flex flex-col items-center self-stretch">
|
||||
<div className="flex flex-col items-center">
|
||||
<img
|
||||
alt="Questions Bank"
|
||||
className="h-40 w-40"
|
||||
src="/bank-logo.png"
|
||||
/>
|
||||
<h1 className="text-center text-4xl font-bold text-slate-900">
|
||||
<h1 className="text-primary-700 text-center text-5xl font-bold">
|
||||
Tech Interview Question Bank
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mb-2 max-w-lg text-5xl font-semibold text-slate-900 sm:max-w-3xl">
|
||||
<p className="mb-2 max-w-lg text-4xl font-semibold text-slate-900 sm:max-w-3xl">
|
||||
Know the{' '}
|
||||
<span className="text-primary-700">
|
||||
latest SWE interview questions
|
||||
@@ -118,22 +116,34 @@ export default function LandingComponent({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
addonPosition="end"
|
||||
icon={ArrowSmallRightIcon}
|
||||
label="Go"
|
||||
size="md"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (company !== undefined && location !== undefined) {
|
||||
return handleLandingQuery({
|
||||
companySlug: companyOptionToSlug(company),
|
||||
location: locationOptionToSlug(location),
|
||||
questionType,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
addonPosition="end"
|
||||
icon={ArrowSmallRightIcon}
|
||||
label="Go"
|
||||
size="md"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (company !== undefined && location !== undefined) {
|
||||
onLanded({
|
||||
companySlug: companyOptionToSlug(company),
|
||||
location: locationOptionToSlug(location),
|
||||
questionType,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
addonPosition="end"
|
||||
icon={ArrowSmallRightIcon}
|
||||
label="View all questions"
|
||||
size="md"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
onLanded(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<iframe
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function QuestionAggregateBadge({
|
||||
<Badge label={label} {...badgeProps} />
|
||||
</button>
|
||||
{visible && (
|
||||
<div ref={setTooltipRef} {...getTooltipProps()}>
|
||||
<div ref={setTooltipRef} {...getTooltipProps()} className="z-10">
|
||||
<div className="flex flex-col gap-2 rounded-md bg-white p-2 shadow-md">
|
||||
<ul>
|
||||
{sortedStatistics.map(({ key, value }) => (
|
||||
|
||||
@@ -116,6 +116,7 @@ export type BaseQuestionCardProps = ActionButtonProps &
|
||||
ReceivedStatisticsProps &
|
||||
UpvoteProps & {
|
||||
content: string;
|
||||
hideCard?: boolean;
|
||||
questionId: string;
|
||||
showHover?: boolean;
|
||||
timestamp: string | null;
|
||||
@@ -140,6 +141,7 @@ export default function BaseQuestionCard({
|
||||
actionButtonLabel,
|
||||
onActionButtonClick,
|
||||
upvoteCount,
|
||||
hideCard,
|
||||
timestamp,
|
||||
roles,
|
||||
countries,
|
||||
@@ -152,7 +154,6 @@ export default function BaseQuestionCard({
|
||||
}: BaseQuestionCardProps) {
|
||||
const [showReceivedForm, setShowReceivedForm] = useState(false);
|
||||
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
|
||||
const hoverClass = showHover ? 'hover:bg-slate-50' : '';
|
||||
|
||||
const locations = useMemo(() => {
|
||||
if (countries === undefined) {
|
||||
@@ -185,7 +186,7 @@ export default function BaseQuestionCard({
|
||||
)}
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<div className="flex items-baseline justify-between self-stretch">
|
||||
<div className="z-10 flex items-center gap-2 text-slate-500">
|
||||
<div className="flex items-center gap-2 text-slate-500">
|
||||
{showAggregateStatistics && (
|
||||
<>
|
||||
<QuestionTypeBadge type={type} />
|
||||
@@ -274,7 +275,11 @@ export default function BaseQuestionCard({
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`group flex gap-4 rounded-md border border-slate-300 bg-white p-4 ${hoverClass}`}>
|
||||
className={clsx(
|
||||
'group flex gap-4 border-slate-300',
|
||||
showHover && 'hover:bg-slate-50',
|
||||
!hideCard && 'rounded-md border bg-white p-4',
|
||||
)}>
|
||||
{cardContent}
|
||||
{showDeleteButton && (
|
||||
<div className="fill-danger-700 invisible self-center group-hover:visible">
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function FullQuestionCard(props: QuestionOverviewCardProps) {
|
||||
return (
|
||||
<BaseQuestionCard
|
||||
{...props}
|
||||
hideCard={true}
|
||||
showActionButton={false}
|
||||
showAddToList={true}
|
||||
showAggregateStatistics={true}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||
import { Button } from '@tih/ui';
|
||||
|
||||
export type BackButtonLayoutProps = PropsWithChildren<{
|
||||
href: string;
|
||||
}>;
|
||||
|
||||
export default function BackButtonLayout({
|
||||
href,
|
||||
children,
|
||||
}: BackButtonLayoutProps) {
|
||||
return (
|
||||
<div className="flex w-full flex-1 flex-col items-stretch gap-4 p-4 lg:flex-row">
|
||||
<div>
|
||||
<Button
|
||||
addonPosition="start"
|
||||
display="inline"
|
||||
href={href}
|
||||
icon={ArrowSmallLeftIcon}
|
||||
label="Back"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, TextArea } from '@tih/ui';
|
||||
|
||||
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
||||
import FullAnswerCard from '~/components/questions/card/FullAnswerCard';
|
||||
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
||||
import BackButtonLayout from '~/components/questions/layout/BackButtonLayout';
|
||||
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
|
||||
|
||||
@@ -104,83 +104,72 @@ export default function QuestionPage() {
|
||||
{answer.content} - {APP_TITLE}
|
||||
</title>
|
||||
</Head>
|
||||
<div className="flex w-full flex-1 items-stretch pb-4">
|
||||
<div className="flex items-baseline gap-2 py-4 pl-4">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
display="inline"
|
||||
href={`/questions/${router.query.questionId}/${router.query.questionSlug}`}
|
||||
icon={ArrowSmallLeftIcon}
|
||||
label="Back"
|
||||
variant="secondary"
|
||||
<BackButtonLayout
|
||||
href={`/questions/${router.query.questionId}/${router.query.questionSlug}`}>
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<FullAnswerCard
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<FullAnswerCard
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
<div className="mx-2">
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
<div className="mx-2">
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmit(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
sortTypeValue={commentSortType}
|
||||
onSortOrderChange={setCommentSortOrder}
|
||||
onSortTypeChange={setCommentSortType}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
sortTypeValue={commentSortType}
|
||||
onSortOrderChange={setCommentSortOrder}
|
||||
onSortTypeChange={setCommentSortType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Allow to load more pages */}
|
||||
{(answerCommentsData?.pages ?? []).flatMap(
|
||||
({ processedQuestionAnswerCommentsData: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<AnswerCommentListItem
|
||||
key={comment.id}
|
||||
answerCommentId={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerCommentInfiniteQuery} />
|
||||
</div>
|
||||
{/* TODO: Allow to load more pages */}
|
||||
{(answerCommentsData?.pages ?? []).flatMap(
|
||||
({ processedQuestionAnswerCommentsData: comments }) =>
|
||||
comments.map((comment) => (
|
||||
<AnswerCommentListItem
|
||||
key={comment.id}
|
||||
answerCommentId={comment.id}
|
||||
authorImageUrl={comment.userImage}
|
||||
authorName={comment.user}
|
||||
content={comment.content}
|
||||
createdAt={comment.createdAt}
|
||||
upvoteCount={comment.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerCommentInfiniteQuery} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BackButtonLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
|
||||
import { Button, Collapsible, HorizontalDivider, TextArea } from '@tih/ui';
|
||||
|
||||
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
|
||||
import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
|
||||
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
|
||||
import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
|
||||
import BackButtonLayout from '~/components/questions/layout/BackButtonLayout';
|
||||
import PaginationLoadMoreButton from '~/components/questions/PaginationLoadMoreButton';
|
||||
import SortOptionsSelect from '~/components/questions/SortOptionsSelect';
|
||||
|
||||
@@ -182,19 +182,9 @@ export default function QuestionPage() {
|
||||
{question.content} - {APP_TITLE}
|
||||
</title>
|
||||
</Head>
|
||||
<div className="flex w-full flex-1 items-stretch pb-4">
|
||||
<div className="flex items-baseline gap-2 py-4 pl-4">
|
||||
<Button
|
||||
addonPosition="start"
|
||||
display="inline"
|
||||
href="/questions/browse"
|
||||
icon={ArrowSmallLeftIcon}
|
||||
label="Back"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center overflow-y-auto py-4 px-5">
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<BackButtonLayout href="/questions/browse">
|
||||
<div className="flex max-w-7xl flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 rounded-md border bg-white p-4">
|
||||
<FullQuestionCard
|
||||
{...question}
|
||||
companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
|
||||
@@ -220,35 +210,15 @@ export default function QuestionPage() {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="mx-2">
|
||||
<Collapsible label={`View ${question.numComments} comment(s)`}>
|
||||
<div className="mt-4 px-4">
|
||||
<form
|
||||
className="mb-2"
|
||||
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
{/* TODO: Add button to load more */}
|
||||
<div className="ml-16 mr-2">
|
||||
<Collapsible
|
||||
defaultOpen={true}
|
||||
label={
|
||||
<div className="text-primary-700">{`${question.numComments} comment(s)`}</div>
|
||||
}>
|
||||
<div className="">
|
||||
<div className="flex flex-col gap-2 text-black">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-lg">Comments</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={commentSortOrder}
|
||||
@@ -273,65 +243,93 @@ export default function QuestionPage() {
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={commentInfiniteQuery} />
|
||||
<form
|
||||
className="mt-4"
|
||||
onSubmit={handleCommentSubmitClick(handleSubmitComment)}>
|
||||
<TextArea
|
||||
{...commentRegister('commentContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
label="Post a comment"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="my-3 flex justify-between">
|
||||
<Button
|
||||
disabled={!isCommentDirty || !isCommentValid}
|
||||
label="Post"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
<form onSubmit={handleSubmit(handleSubmitAnswer)}>
|
||||
</div>
|
||||
<HorizontalDivider />
|
||||
<form onSubmit={handleSubmit(handleSubmitAnswer)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-md font-semibold">Contribute your answer</p>
|
||||
<TextArea
|
||||
{...answerRegister('answerContent', {
|
||||
minLength: 1,
|
||||
required: true,
|
||||
})}
|
||||
isLabelHidden={true}
|
||||
label="Contribute your answer"
|
||||
required={true}
|
||||
resize="vertical"
|
||||
rows={5}
|
||||
/>
|
||||
<div className="mt-3 mb-1 flex justify-between">
|
||||
<Button
|
||||
disabled={!isDirty || !isValid}
|
||||
label="Contribute"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xl">{question.numAnswers} answers</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={answerSortOrder}
|
||||
sortTypeValue={answerSortType}
|
||||
onSortOrderChange={setAnswerSortOrder}
|
||||
onSortTypeChange={setAnswerSortType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Add button to load more */}
|
||||
{(answerData?.pages ?? []).flatMap(
|
||||
({ processedAnswersData: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||
<div className="mt-3 mb-1 flex justify-between">
|
||||
<Button
|
||||
disabled={!isDirty || !isValid}
|
||||
label="Contribute"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xl font-semibold">
|
||||
{question.numAnswers} answers
|
||||
</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<SortOptionsSelect
|
||||
sortOrderValue={answerSortOrder}
|
||||
sortTypeValue={answerSortType}
|
||||
onSortOrderChange={setAnswerSortOrder}
|
||||
onSortTypeChange={setAnswerSortType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: Add button to load more */}
|
||||
{(answerData?.pages ?? []).flatMap(
|
||||
({ processedAnswersData: answers }) =>
|
||||
answers.map((answer) => (
|
||||
<QuestionAnswerCard
|
||||
key={answer.id}
|
||||
answerId={answer.id}
|
||||
authorImageUrl={answer.userImage}
|
||||
authorName={answer.user}
|
||||
commentCount={answer.numComments}
|
||||
content={answer.content}
|
||||
createdAt={answer.createdAt}
|
||||
href={`${router.asPath}/answer/${answer.id}/${createSlug(
|
||||
answer.content,
|
||||
)}`}
|
||||
upvoteCount={answer.numVotes}
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<PaginationLoadMoreButton query={answerInfiniteQuery} />
|
||||
</div>
|
||||
</div>
|
||||
</BackButtonLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ export default function QuestionsHomePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleLandingQuery = async (data: LandingQueryData) => {
|
||||
if (data === null) {
|
||||
// Go to browse page
|
||||
router.push({
|
||||
pathname: '/questions/browse',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { companySlug, location, questionType } = data;
|
||||
|
||||
// Go to browse page
|
||||
|
||||
Reference in New Issue
Block a user