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

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

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

@ -1,12 +1,13 @@
import withHref from '~/utils/questions/withHref';
import type { QuestionCardProps } from './QuestionCard';
import QuestionCard from './QuestionCard';
import type { BaseQuestionCardProps } from './BaseQuestionCard';
import BaseQuestionCard from './BaseQuestionCard';
export type QuestionOverviewCardProps = Omit<
QuestionCardProps & {
BaseQuestionCardProps & {
showActionButton: false;
showAnswerStatistics: true;
showCreateEncounterButton: false;
showDeleteButton: false;
showReceivedStatistics: true;
showVoteButtons: true;
@ -16,6 +17,7 @@ export type QuestionOverviewCardProps = Omit<
| 'onDelete'
| 'showActionButton'
| 'showAnswerStatistics'
| 'showCreateEncounterButton'
| 'showDeleteButton'
| 'showReceivedStatistics'
| 'showVoteButtons'
@ -23,10 +25,11 @@ export type QuestionOverviewCardProps = Omit<
function QuestionOverviewCardWithoutHref(props: QuestionOverviewCardProps) {
return (
<QuestionCard
<BaseQuestionCard
{...props}
showActionButton={false}
showAnswerStatistics={true}
showCreateEncounterButton={false}
showDeleteButton={false}
showHover={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 MonthYearPicker from '~/components/shared/MonthYearPicker';
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
import LocationTypeahead from '../typeahead/LocationTypeahead';
import RoleTypeahead from '../typeahead/RoleTypeahead';
import CompanyTypeahead from '../typeahead/CompanyTypeahead';
export type CreateQuestionEncounterData = {
company: string;
@ -40,15 +40,15 @@ export default function CreateQuestionEncounterForm({
{step === 0 && (
<div>
<CompanyTypeahead
isLabelHidden={true}
placeholder="Other company"
suggestedCount={3}
onSuggestionClick={({ value: company }) => {
onSelect={({ value: company }) => {
setSelectedCompany(company);
setStep(step + 1);
}}
isLabelHidden={true}
onSelect={({ value: company }) => {
onSuggestionClick={({ value: company }) => {
setSelectedCompany(company);
setStep(step + 1);
}}
/>
</div>
@ -56,18 +56,18 @@ export default function CreateQuestionEncounterForm({
{step === 1 && (
<div>
<LocationTypeahead
suggestedCount={3}
onSuggestionClick={({ value: location }) => {
setSelectedLocation(location);
setStep(step + 1);
}}
isLabelHidden={true}
placeholder="Other location"
suggestedCount={3}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}}
onSelect={({ value: location }) => {
setSelectedLocation(location);
}}
onSuggestionClick={({ value: location }) => {
setSelectedLocation(location);
setStep(step + 1);
}}
/>
</div>
)}
@ -75,28 +75,28 @@ export default function CreateQuestionEncounterForm({
<div>
<RoleTypeahead
isLabelHidden={true}
suggestedCount={3}
onSuggestionClick={({ value: role }) => {
setSelectedRole(role);
setStep(step + 1);
}}
placeholder="Other role"
suggestedCount={3}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onQueryChange={() => {}}
onSelect={({ value: role }) => {
setSelectedRole(role);
}}
onSuggestionClick={({ value: role }) => {
setSelectedRole(role);
setStep(step + 1);
}}
/>
</div>
)}
{step === 3 && (
<MonthYearPicker
yearLabel={''}
monthLabel={''}
monthLabel=""
value={{
month: ((selectedDate?.getMonth() ?? 0) + 1) as Month,
year: selectedDate?.getFullYear() as number,
}}
yearLabel=""
onChange={(value) => {
setSelectedDate(
startOfMonth(new Date(value.year, value.month - 1)),
@ -106,6 +106,11 @@ export default function CreateQuestionEncounterForm({
)}
{step < 3 && (
<Button
disabled={
(step === 0 && selectedCompany === null) ||
(step === 1 && selectedLocation === null) ||
(step === 2 && selectedRole === null)
}
label="Next"
variant="primary"
onClick={() => {

@ -4,7 +4,7 @@ import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button, Collapsible, Select, TextArea } from '@tih/ui';
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 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">
<FullQuestionCard
{...question}
companies={aggregatedEncounters?.companyCounts ?? {}}
locations={aggregatedEncounters?.locationCounts ?? {}}
questionId={question.id}
receivedCount={question.receivedCount}
receivedCount={undefined}
roles={aggregatedEncounters?.roleCounts ?? {}}
timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',

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

@ -1,6 +1,6 @@
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 createSlug from '~/utils/questions/createSlug';
@ -14,13 +14,13 @@ export default function ListPage() {
{(questions ?? []).map((question) => (
<QuestionListCard
key={question.id}
company={question.company}
companies={{ [question.company]: 1 }}
content={question.content}
href={`/questions/${question.id}/${createSlug(question.content)}`}
location={question.location}
locations={{ [question.location]: 1 }}
questionId={question.id}
receivedCount={0}
role={question.role}
receivedCount={question.receivedCount}
roles={{ [question.role]: 1 }}
timestamp={question.seenAt.toLocaleDateString(undefined, {
month: 'short',
year: 'numeric',

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

Loading…
Cancel
Save