[questions][refactor] refactor question cards

pull/411/head
Jeff Sieu 3 years ago
parent e8ba344ecb
commit c9af85a48b

@ -0,0 +1,39 @@
import type { ComponentProps } from 'react';
import { useMemo } from 'react';
import { Badge } from '@tih/ui';
type BadgeProps = ComponentProps<typeof Badge>;
export type QuestionAggregateBadgeProps = Omit<BadgeProps, 'label'> & {
statistics: Record<string, number>;
};
export default function QuestionAggregateBadge({
statistics,
...badgeProps
}: QuestionAggregateBadgeProps) {
const mostCommonStatistic = useMemo(
() =>
Object.entries(statistics).reduce(
(mostCommon, [key, value]) => {
if (value > mostCommon.value) {
return { key, value };
}
return mostCommon;
},
{ key: '', value: 0 },
),
[statistics],
);
const additionalStatisticCount = Object.keys(statistics).length - 1;
const label = useMemo(() => {
if (additionalStatisticCount === 0) {
return mostCommonStatistic.key;
}
return `${mostCommonStatistic.key} (+${additionalStatisticCount})`;
}, [mostCommonStatistic, additionalStatisticCount]);
return <Badge label={label} {...badgeProps} />;
}

@ -1,31 +0,0 @@
import type { QuestionCardProps } from './QuestionCard';
import QuestionCard from './QuestionCard';
export type SimilarQuestionCardProps = Omit<
QuestionCardProps & {
showActionButton: true;
showUserStatistics: false;
showVoteButtons: false;
},
| 'actionButtonLabel'
| 'answerCount'
| 'onActionButtonClick'
| 'showActionButton'
| 'showUserStatistics'
| 'showVoteButtons'
| 'upvoteCount'
> & {
onSimilarQuestionClick: () => void;
};
export default function SimilarQuestionCard(props: SimilarQuestionCardProps) {
const { onSimilarQuestionClick, ...rest } = props;
return (
<QuestionCard
{...rest}
actionButtonLabel="Yes, this is my question"
showActionButton={true}
onActionButtonClick={onSimilarQuestionClick}
/>
);
}

@ -7,14 +7,15 @@ import {
TrashIcon, TrashIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import { Badge, Button } from '@tih/ui'; import { Button } from '@tih/ui';
import { useQuestionVote } from '~/utils/questions/useVote'; import { useQuestionVote } from '~/utils/questions/useVote';
import type { CreateQuestionEncounterData } from '../forms/CreateQuestionEncounterForm'; import type { CreateQuestionEncounterData } from '../../forms/CreateQuestionEncounterForm';
import CreateQuestionEncounterForm from '../forms/CreateQuestionEncounterForm'; import CreateQuestionEncounterForm from '../../forms/CreateQuestionEncounterForm';
import QuestionTypeBadge from '../QuestionTypeBadge'; import QuestionAggregateBadge from '../../QuestionAggregateBadge';
import VotingButtons from '../VotingButtons'; import QuestionTypeBadge from '../../QuestionTypeBadge';
import VotingButtons from '../../VotingButtons';
type UpvoteProps = type UpvoteProps =
| { | {
@ -60,35 +61,44 @@ type ActionButtonProps =
type ReceivedStatisticsProps = type ReceivedStatisticsProps =
| { | {
onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
receivedCount: number; receivedCount: number;
showReceivedStatistics: true; showReceivedStatistics: true;
} }
| { | {
onReceivedSubmit?: never;
receivedCount?: never; receivedCount?: never;
showReceivedStatistics?: false; showReceivedStatistics?: false;
}; };
export type QuestionCardProps = ActionButtonProps & type CreateEncounterProps =
| {
onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
showCreateEncounterButton: true;
}
| {
onReceivedSubmit?: never;
showCreateEncounterButton?: false;
};
export type BaseQuestionCardProps = ActionButtonProps &
AnswerStatisticsProps & AnswerStatisticsProps &
CreateEncounterProps &
DeleteProps & DeleteProps &
ReceivedStatisticsProps & ReceivedStatisticsProps &
UpvoteProps & { UpvoteProps & {
company: string; companies: Record<string, number>;
content: string; content: string;
location: string; locations: Record<string, number>;
questionId: string; questionId: string;
role: string; roles: Record<string, number>;
showHover?: boolean; showHover?: boolean;
timestamp: string; timestamp: string;
truncateContent?: boolean; truncateContent?: boolean;
type: QuestionsQuestionType; type: QuestionsQuestionType;
}; };
export default function QuestionCard({ export default function BaseQuestionCard({
questionId, questionId,
company, companies,
answerCount, answerCount,
content, content,
receivedCount, receivedCount,
@ -96,19 +106,20 @@ export default function QuestionCard({
showVoteButtons, showVoteButtons,
showAnswerStatistics, showAnswerStatistics,
showReceivedStatistics, showReceivedStatistics,
showCreateEncounterButton,
showActionButton, showActionButton,
actionButtonLabel, actionButtonLabel,
onActionButtonClick, onActionButtonClick,
upvoteCount, upvoteCount,
timestamp, timestamp,
role, roles,
location, locations,
showHover, showHover,
onReceivedSubmit, onReceivedSubmit,
showDeleteButton, showDeleteButton,
onDelete, onDelete,
truncateContent = true, truncateContent = true,
}: QuestionCardProps) { }: BaseQuestionCardProps) {
const [showReceivedForm, setShowReceivedForm] = useState(false); const [showReceivedForm, setShowReceivedForm] = useState(false);
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId); const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
const hoverClass = showHover ? 'hover:bg-slate-50' : ''; const hoverClass = showHover ? 'hover:bg-slate-50' : '';
@ -125,11 +136,11 @@ export default function QuestionCard({
<div className="flex flex-col items-start gap-2"> <div className="flex flex-col items-start gap-2">
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-baseline gap-2 text-slate-500"> <div className="flex items-baseline gap-2 text-slate-500">
<Badge label={company} variant="primary" />
<QuestionTypeBadge type={type} /> <QuestionTypeBadge type={type} />
<p className="text-xs"> <QuestionAggregateBadge statistics={companies} variant="primary" />
{timestamp} · {location} · {role} <QuestionAggregateBadge statistics={locations} variant="success" />
</p> <QuestionAggregateBadge statistics={roles} variant="danger" />
<p className="text-xs">{timestamp}</p>
</div> </div>
{showActionButton && ( {showActionButton && (
<Button <Button
@ -143,19 +154,21 @@ export default function QuestionCard({
<p className={clsx(truncateContent && 'line-clamp-2 text-ellipsis')}> <p className={clsx(truncateContent && 'line-clamp-2 text-ellipsis')}>
{content} {content}
</p> </p>
{!showReceivedForm && (showAnswerStatistics || showReceivedStatistics) && ( {!showReceivedForm &&
<div className="flex gap-2"> (showAnswerStatistics ||
{showAnswerStatistics && ( showReceivedStatistics ||
<Button showCreateEncounterButton) && (
addonPosition="start" <div className="flex gap-2">
icon={ChatBubbleBottomCenterTextIcon} {showAnswerStatistics && (
label={`${answerCount} answers`} <Button
size="sm" addonPosition="start"
variant="tertiary" icon={ChatBubbleBottomCenterTextIcon}
/> label={`${answerCount} answers`}
)} size="sm"
{showReceivedStatistics && ( variant="tertiary"
<> />
)}
{showReceivedStatistics && (
<Button <Button
addonPosition="start" addonPosition="start"
icon={EyeIcon} icon={EyeIcon}
@ -163,6 +176,8 @@ export default function QuestionCard({
size="sm" size="sm"
variant="tertiary" variant="tertiary"
/> />
)}
{showCreateEncounterButton && (
<Button <Button
addonPosition="start" addonPosition="start"
icon={CheckIcon} icon={CheckIcon}
@ -174,10 +189,9 @@ export default function QuestionCard({
setShowReceivedForm(true); setShowReceivedForm(true);
}} }}
/> />
</> )}
)} </div>
</div> )}
)}
{showReceivedForm && ( {showReceivedForm && (
<CreateQuestionEncounterForm <CreateQuestionEncounterForm
onCancel={() => { onCancel={() => {

@ -1,29 +1,33 @@
import type { QuestionCardProps } from './QuestionCard'; import type { BaseQuestionCardProps } from './BaseQuestionCard';
import QuestionCard from './QuestionCard'; import BaseQuestionCard from './BaseQuestionCard';
export type QuestionOverviewCardProps = Omit< export type QuestionOverviewCardProps = Omit<
QuestionCardProps & { BaseQuestionCardProps & {
showActionButton: false; showActionButton: false;
showAnswerStatistics: false; showAnswerStatistics: false;
showCreateEncounterButton: true;
showDeleteButton: false; showDeleteButton: false;
showReceivedStatistics: true; showReceivedStatistics: false;
showVoteButtons: true; showVoteButtons: true;
}, },
| 'actionButtonLabel' | 'actionButtonLabel'
| 'onActionButtonClick' | 'onActionButtonClick'
| 'showActionButton' | 'showActionButton'
| 'showAnswerStatistics' | 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
| 'showReceivedStatistics' | 'showReceivedStatistics'
| 'showVoteButtons' | 'showVoteButtons'
>; >;
export default function FullQuestionCard(props: QuestionOverviewCardProps) { export default function FullQuestionCard(props: QuestionOverviewCardProps) {
return ( return (
<QuestionCard <BaseQuestionCard
{...props} {...props}
showActionButton={false} showActionButton={false}
showAnswerStatistics={false} showAnswerStatistics={false}
showReceivedStatistics={true} showCreateEncounterButton={true}
showReceivedStatistics={false}
showVoteButtons={true} showVoteButtons={true}
truncateContent={false} truncateContent={false}
/> />

@ -1,10 +1,10 @@
import withHref from '~/utils/questions/withHref'; import withHref from '~/utils/questions/withHref';
import type { QuestionCardProps } from './QuestionCard'; import type { BaseQuestionCardProps } from './BaseQuestionCard';
import QuestionCard from './QuestionCard'; import BaseQuestionCard from './BaseQuestionCard';
export type QuestionListCardProps = Omit< export type QuestionListCardProps = Omit<
QuestionCardProps & { BaseQuestionCardProps & {
showActionButton: false; showActionButton: false;
showAnswerStatistics: false; showAnswerStatistics: false;
showDeleteButton: true; showDeleteButton: true;
@ -20,7 +20,8 @@ export type QuestionListCardProps = Omit<
function QuestionListCardWithoutHref(props: QuestionListCardProps) { function QuestionListCardWithoutHref(props: QuestionListCardProps) {
return ( return (
<QuestionCard <BaseQuestionCard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(props as any)} {...(props as any)}
showActionButton={false} showActionButton={false}
showAnswerStatistics={false} showAnswerStatistics={false}

@ -1,12 +1,13 @@
import withHref from '~/utils/questions/withHref'; import withHref from '~/utils/questions/withHref';
import type { QuestionCardProps } from './QuestionCard'; import type { BaseQuestionCardProps } from './BaseQuestionCard';
import QuestionCard from './QuestionCard'; import BaseQuestionCard from './BaseQuestionCard';
export type QuestionOverviewCardProps = Omit< export type QuestionOverviewCardProps = Omit<
QuestionCardProps & { BaseQuestionCardProps & {
showActionButton: false; showActionButton: false;
showAnswerStatistics: true; showAnswerStatistics: true;
showCreateEncounterButton: false;
showDeleteButton: false; showDeleteButton: false;
showReceivedStatistics: true; showReceivedStatistics: true;
showVoteButtons: true; showVoteButtons: true;
@ -16,6 +17,7 @@ export type QuestionOverviewCardProps = Omit<
| 'onDelete' | 'onDelete'
| 'showActionButton' | 'showActionButton'
| 'showAnswerStatistics' | 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton' | 'showDeleteButton'
| 'showReceivedStatistics' | 'showReceivedStatistics'
| 'showVoteButtons' | 'showVoteButtons'
@ -23,10 +25,11 @@ export type QuestionOverviewCardProps = Omit<
function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) { function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) {
return ( return (
<QuestionCard <BaseQuestionCard
{...props} {...props}
showActionButton={false} showActionButton={false}
showAnswerStatistics={true} showAnswerStatistics={true}
showCreateEncounterButton={false}
showDeleteButton={false} showDeleteButton={false}
showHover={true} showHover={true}
showReceivedStatistics={true} showReceivedStatistics={true}

@ -0,0 +1,44 @@
import type { BaseQuestionCardProps } from './BaseQuestionCard';
import BaseQuestionCard from './BaseQuestionCard';
export type SimilarQuestionCardProps = Omit<
BaseQuestionCardProps & {
showActionButton: true;
showAnswerStatistics: true;
showCreateEncounterButton: false;
showDeleteButton: false;
showHover: true;
showReceivedStatistics: false;
showVoteButtons: false;
},
| 'actionButtonLabel'
| 'onActionButtonClick'
| 'showActionButton'
| 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
| 'showHover'
| 'showReceivedStatistics'
| 'showVoteButtons'
> & {
onSimilarQuestionClick: () => void;
};
export default function SimilarQuestionCard(props: SimilarQuestionCardProps) {
const { onSimilarQuestionClick, ...rest } = props;
return (
<BaseQuestionCard
actionButtonLabel="Yes, this is my question"
showActionButton={true}
showAnswerStatistics={true}
showCreateEncounterButton={false}
showDeleteButton={false}
showHover={true}
showReceivedStatistics={true}
showVoteButtons={true}
onActionButtonClick={onSimilarQuestionClick}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(rest as any)}
/>
);
}

@ -5,9 +5,9 @@ import { Button } from '@tih/ui';
import type { Month } from '~/components/shared/MonthYearPicker'; import type { Month } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker'; import MonthYearPicker from '~/components/shared/MonthYearPicker';
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
import LocationTypeahead from '../typeahead/LocationTypeahead'; import LocationTypeahead from '../typeahead/LocationTypeahead';
import RoleTypeahead from '../typeahead/RoleTypeahead'; import RoleTypeahead from '../typeahead/RoleTypeahead';
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
export type CreateQuestionEncounterData = { export type CreateQuestionEncounterData = {
company: string; company: string;
@ -40,15 +40,15 @@ export default function CreateQuestionEncounterForm({
{step === 0 && ( {step === 0 && (
<div> <div>
<CompanyTypeahead <CompanyTypeahead
isLabelHidden={true}
placeholder="Other company" placeholder="Other company"
suggestedCount={3} suggestedCount={3}
onSuggestionClick={({ value: company }) => { onSelect={({ value: company }) => {
setSelectedCompany(company); setSelectedCompany(company);
setStep(step + 1);
}} }}
isLabelHidden={true} onSuggestionClick={({ value: company }) => {
onSelect={({ value: company }) => {
setSelectedCompany(company); setSelectedCompany(company);
setStep(step + 1);
}} }}
/> />
</div> </div>
@ -56,18 +56,18 @@ export default function CreateQuestionEncounterForm({
{step === 1 && ( {step === 1 && (
<div> <div>
<LocationTypeahead <LocationTypeahead
suggestedCount={3}
onSuggestionClick={({ value: location }) => {
setSelectedLocation(location);
setStep(step + 1);
}}
isLabelHidden={true} isLabelHidden={true}
placeholder="Other location" placeholder="Other location"
suggestedCount={3}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}} onQueryChange={() => {}}
onSelect={({ value: location }) => { onSelect={({ value: location }) => {
setSelectedLocation(location); setSelectedLocation(location);
}} }}
onSuggestionClick={({ value: location }) => {
setSelectedLocation(location);
setStep(step + 1);
}}
/> />
</div> </div>
)} )}
@ -75,28 +75,28 @@ export default function CreateQuestionEncounterForm({
<div> <div>
<RoleTypeahead <RoleTypeahead
isLabelHidden={true} isLabelHidden={true}
suggestedCount={3}
onSuggestionClick={({ value: role }) => {
setSelectedRole(role);
setStep(step + 1);
}}
placeholder="Other role" placeholder="Other role"
suggestedCount={3}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}} onQueryChange={() => {}}
onSelect={({ value: role }) => { onSelect={({ value: role }) => {
setSelectedRole(role); setSelectedRole(role);
}} }}
onSuggestionClick={({ value: role }) => {
setSelectedRole(role);
setStep(step + 1);
}}
/> />
</div> </div>
)} )}
{step === 3 && ( {step === 3 && (
<MonthYearPicker <MonthYearPicker
yearLabel={''} monthLabel=""
monthLabel={''}
value={{ value={{
month: ((selectedDate?.getMonth() ?? 0) + 1) as Month, month: ((selectedDate?.getMonth() ?? 0) + 1) as Month,
year: selectedDate?.getFullYear() as number, year: selectedDate?.getFullYear() as number,
}} }}
yearLabel=""
onChange={(value) => { onChange={(value) => {
setSelectedDate( setSelectedDate(
startOfMonth(new Date(value.year, value.month - 1)), startOfMonth(new Date(value.year, value.month - 1)),
@ -106,6 +106,11 @@ export default function CreateQuestionEncounterForm({
)} )}
{step < 3 && ( {step < 3 && (
<Button <Button
disabled={
(step === 0 && selectedCompany === null) ||
(step === 1 && selectedLocation === null) ||
(step === 2 && selectedRole === null)
}
label="Next" label="Next"
variant="primary" variant="primary"
onClick={() => { onClick={() => {

@ -4,7 +4,7 @@ import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Collapsible, Select, TextArea } from '@tih/ui'; import { Button, Collapsible, Select, TextArea } from '@tih/ui';
import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem'; import AnswerCommentListItem from '~/components/questions/AnswerCommentListItem';
import FullQuestionCard from '~/components/questions/card/FullQuestionCard'; import FullQuestionCard from '~/components/questions/card/question/FullQuestionCard';
import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard'; import QuestionAnswerCard from '~/components/questions/card/QuestionAnswerCard';
import FullScreenSpinner from '~/components/questions/FullScreenSpinner'; import FullScreenSpinner from '~/components/questions/FullScreenSpinner';
@ -130,8 +130,11 @@ export default function QuestionPage() {
<div className="flex max-w-7xl flex-1 flex-col gap-2"> <div className="flex max-w-7xl flex-1 flex-col gap-2">
<FullQuestionCard <FullQuestionCard
{...question} {...question}
companies={aggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',

@ -5,7 +5,7 @@ import { NoSymbolIcon } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import { SlideOut, Typeahead } from '@tih/ui'; import { SlideOut, Typeahead } from '@tih/ui';
import QuestionOverviewCard from '~/components/questions/card/QuestionOverviewCard'; import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard'; import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
import FilterSection from '~/components/questions/filter/FilterSection'; import FilterSection from '~/components/questions/filter/FilterSection';
import type { LandingQueryData } from '~/components/questions/LandingComponent'; import type { LandingQueryData } from '~/components/questions/LandingComponent';
@ -375,25 +375,21 @@ export default function QuestionsHomePage() {
<QuestionOverviewCard <QuestionOverviewCard
key={question.id} key={question.id}
answerCount={question.numAnswers} answerCount={question.numAnswers}
company={question.company} companies={{ [question.company]: 1 }}
content={question.content} content={question.content}
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
location={question.location} locations={{ [question.location]: 1 }}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
role={question.role} roles={{ [question.role]: 1 }}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
})} })}
type={question.type} type={question.type}
upvoteCount={question.numVotes} upvoteCount={question.numVotes}
onReceivedSubmit={(data) => {
// eslint-disable-next-line no-console
console.log(data);
}}
/> />
))} ))}
{questions?.length === 0 && ( {questions?.length === 0 && (

@ -1,6 +1,6 @@
import { NoSymbolIcon } from '@heroicons/react/24/outline'; import { NoSymbolIcon } from '@heroicons/react/24/outline';
import QuestionListCard from '~/components/questions/card/QuestionListCard'; import QuestionListCard from '~/components/questions/card/question/QuestionListCard';
import { SAMPLE_QUESTION } from '~/utils/questions/constants'; import { SAMPLE_QUESTION } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug'; import createSlug from '~/utils/questions/createSlug';
@ -14,13 +14,13 @@ export default function ListPage() {
{(questions ?? []).map((question) => ( {(questions ?? []).map((question) => (
<QuestionListCard <QuestionListCard
key={question.id} key={question.id}
company={question.company} companies={{ [question.company]: 1 }}
content={question.content} content={question.content}
href={`/questions/${question.id}/${createSlug(question.content)}`} href={`/questions/${question.id}/${createSlug(question.content)}`}
location={question.location} locations={{ [question.location]: 1 }}
questionId={question.id} questionId={question.id}
receivedCount={0} receivedCount={question.receivedCount}
role={question.role} roles={{ [question.role]: 1 }}
timestamp={question.seenAt.toLocaleDateString(undefined, { timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',

@ -71,9 +71,9 @@ export const LOCATIONS: FilterChoices = [
value: 'Menlo Park', value: 'Menlo Park',
}, },
{ {
id: 'california', id: 'California',
label: 'California', label: 'California',
value: 'california', value: 'California',
}, },
{ {
id: 'Hong Kong', id: 'Hong Kong',

Loading…
Cancel
Save