[questions][feat] tweak cards UI (#546)

* [questions][feat] tweak card UI

* [questions][feat] tweak comment and answer pages
This commit is contained in:
Yangshun Tay
2022-11-09 07:15:44 +08:00
committed by GitHub
parent 8a73925601
commit 8299a5463e
13 changed files with 212 additions and 180 deletions

View File

@@ -106,7 +106,7 @@ export default function AddToListDropdown({
const CustomMenuButton = ({ children }: PropsWithChildren<unknown>) => (
<button
className="focus:ring-primary-500 inline-flex w-full justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
className="focus:ring-primary-500 inline-flex w-full items-center justify-center rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-100"
type="button"
onClick={handleMenuButtonClick}>
{children}

View File

@@ -1,19 +1,16 @@
import type { ComponentProps } from 'react';
import { useMemo } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip';
import { Badge } from '@tih/ui';
import 'react-popper-tooltip/dist/styles.css';
type BadgeProps = ComponentProps<typeof Badge>;
export type QuestionAggregateBadgeProps = Omit<BadgeProps, 'label'> & {
export type QuestionAggregateBadgeProps = Readonly<{
icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
statistics: Record<string, number>;
};
}>;
export default function QuestionAggregateBadge({
icon: Icon,
statistics,
...badgeProps
}: QuestionAggregateBadgeProps) {
const { getTooltipProps, setTooltipRef, setTriggerRef, visible } =
usePopperTooltip({
@@ -56,8 +53,16 @@ export default function QuestionAggregateBadge({
return (
<>
<button ref={setTriggerRef} className="rounded-full" type="button">
<Badge label={label} {...badgeProps} />
<button
ref={setTriggerRef}
className="-my-1 flex items-center rounded-md px-2
py-1 text-xs font-medium text-slate-500 hover:bg-slate-100 hover:text-slate-600"
type="button">
<Icon
aria-hidden={true}
className="mr-1.5 h-5 w-5 flex-shrink-0 text-slate-400"
/>
{label}
</button>
{visible && (
<div ref={setTooltipRef} {...getTooltipProps()} className="z-10">

View File

@@ -36,7 +36,7 @@ export default function SortOptionsSelect({
return (
<div className="flex items-end justify-end gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center">
<Select
display="inline"
label="Sort by"
@@ -52,7 +52,7 @@ export default function SortOptionsSelect({
}}
/>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center">
<Select
display="inline"
label="Order by"

View File

@@ -1,8 +1,7 @@
import clsx from 'clsx';
import React from 'react';
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import type { Vote } from '@prisma/client';
import type { ButtonSize } from '@tih/ui';
import { Button } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
@@ -18,7 +17,7 @@ export type VotingButtonsCallbackProps = {
};
export type VotingButtonsProps = VotingButtonsCallbackProps & {
size?: ButtonSize;
size?: 'md' | 'sm';
upvoteCount: number;
};
@@ -29,11 +28,6 @@ export default function VotingButtons({
upvoteCount,
size = 'md',
}: VotingButtonsProps) {
const upvoteButtonVariant =
vote?.vote === 'UPVOTE' ? 'secondary' : 'tertiary';
const downvoteButtonVariant =
vote?.vote === 'DOWNVOTE' ? 'secondary' : 'tertiary';
const handleUpvoteClick = useProtectedCallback(() => {
onUpvote();
});
@@ -44,31 +38,45 @@ export default function VotingButtons({
return (
<div className="flex flex-col items-center">
<Button
icon={ChevronUpIcon}
isLabelHidden={true}
label="Upvote"
size={size}
variant={upvoteButtonVariant}
<button
aria-label="Upvote"
className="rounded-full p-1 hover:bg-slate-100"
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleUpvoteClick();
}}
/>
}}>
<ChevronUpIcon
className={clsx(
size === 'sm' && 'h-5 w-5',
size === 'md' && 'h-8 w-8',
vote?.vote === 'UPVOTE'
? 'text-primary-500'
: 'hover:text-primary-500 text-slate-400',
)}
/>
</button>
<p>{upvoteCount}</p>
<Button
icon={ChevronDownIcon}
isLabelHidden={true}
label="Downvote"
size={size}
variant={downvoteButtonVariant}
<button
aria-label="Downvote"
className="rounded-full p-1 hover:bg-slate-100"
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleDownvoteClick();
}}
/>
}}>
<ChevronDownIcon
className={clsx(
size === 'sm' && 'h-5 w-5',
size === 'md' && 'h-8 w-8',
vote?.vote === 'DOWNVOTE'
? 'text-danger-500'
: 'hover:text-danger-500 text-slate-400',
)}
/>
</button>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import { format } from 'date-fns';
import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import { ChatBubbleLeftRightIcon } from '@heroicons/react/24/outline';
import useAnswerVote from '~/utils/questions/vote/useAnswerVote';
@@ -30,11 +31,13 @@ export default function AnswerCard({
showHover,
}: AnswerCardProps) {
const { handleUpvote, handleDownvote, vote } = useAnswerVote(answerId);
const hoverClass = showHover ? 'hover:bg-slate-50' : '';
return (
<article
className={`flex gap-4 rounded-md border bg-white p-2 ${hoverClass}`}>
className={clsx(
'flex gap-4 rounded-md border border-slate-200 bg-white p-4 sm:rounded-lg',
showHover && 'hover:bg-slate-50',
)}>
<VotingButtons
size={votingButtonsSize}
upvoteCount={upvoteCount}
@@ -42,26 +45,37 @@ export default function AnswerCard({
onDownvote={handleDownvote}
onUpvote={handleUpvote}
/>
<div className="flex flex-col gap-2">
<div className="flex w-full flex-col gap-2">
<div className="flex items-center gap-2">
<img
alt={`${authorName} profile picture`}
className="h-8 w-8 rounded-full"
src={authorImageUrl}></img>
<h1 className="font-bold">{authorName}</h1>
<p className="text-xs font-extralight">
Posted on: {format(createdAt, 'h:mm a, MMMM dd, yyyy')}
alt={authorName}
className="h-7 w-7 rounded-full"
src={authorImageUrl}
/>
<p className="text-sm font-medium text-slate-900">{authorName}</p>
<span className="font-medium text-slate-500">·</span>
<p className="text-xs text-slate-500">
{formatDistanceToNow(createdAt, {
addSuffix: true,
})}
</p>
</div>
<p className="whitespace-pre-line">{content}</p>
{commentCount !== undefined && (
<div className="flex items-center gap-2 text-slate-500">
<ChatBubbleLeftRightIcon className="h-6 w-6" />
<p className="text-sm font-medium">
<p className="whitespace-pre-wrap text-xs sm:text-sm">{content}</p>
<div className="-ml-2">
{commentCount !== undefined && (
<button
className="-my-1 flex items-center rounded-md px-2
py-1 text-xs font-medium
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
type="button">
<ChatBubbleLeftRightIcon
aria-hidden={true}
className="mr-2 h-5 w-5"
/>
{commentCount} {commentCount === 1 ? 'comment' : 'comments'}
</p>
</div>
)}
</button>
)}
</div>
</div>
</article>
);

View File

@@ -1,5 +1,10 @@
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import {
BuildingOfficeIcon,
MapPinIcon,
UserCircleIcon,
} from '@heroicons/react/20/solid';
import {
ChatBubbleBottomCenterTextIcon,
CheckIcon,
@@ -198,21 +203,24 @@ export default function BaseQuestionCard({
</div>
</>
)}
<div className="flex flex-1 flex-col items-start gap-2">
<div className="flex items-baseline justify-between self-stretch">
<div className="flex flex-wrap items-center gap-2 text-slate-500">
<div className="flex flex-1 flex-col items-start gap-4">
<div className="flex items-center justify-between self-stretch">
<div className="flex flex-wrap items-center gap-3 text-slate-500">
{showAggregateStatistics && (
<>
<QuestionTypeBadge type={type} />
<QuestionAggregateBadge
icon={BuildingOfficeIcon}
statistics={companies}
variant="primary"
/>
<QuestionAggregateBadge
icon={MapPinIcon}
statistics={locations!}
variant="success"
/>
<QuestionAggregateBadge statistics={roles} variant="danger" />
<QuestionAggregateBadge
icon={UserCircleIcon}
statistics={roles}
/>
</>
)}
{timestamp !== null && (
@@ -240,59 +248,43 @@ export default function BaseQuestionCard({
</div>
<p
className={clsx(
'whitespace-pre-line font-semibold',
'whitespace-pre-line text-base font-medium leading-6 text-slate-900 md:text-lg',
truncateContent && 'line-clamp-2 text-ellipsis',
)}>
{content}
</p>
{!showReceivedForm &&
(showAnswerStatistics ||
showReceivedStatistics ||
showCreateEncounterButton) && (
<div className="flex gap-2">
<div className="flex w-full gap-4">
{showAnswerStatistics && (
<>
<div className="sm:hidden">
<Button
addonPosition="start"
icon={ChatBubbleBottomCenterTextIcon}
label={`${answerCount}`}
size="sm"
variant="tertiary"
<div>
<button
className="-my-1 flex items-center rounded-md px-2
py-1 text-xs font-medium
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
type="button">
<ChatBubbleBottomCenterTextIcon
aria-hidden={true}
className="mr-2 h-5 w-5"
/>
</div>
<div className="hidden sm:block">
<Button
addonPosition="start"
icon={ChatBubbleBottomCenterTextIcon}
label={`${answerCount} answers`}
size="sm"
variant="tertiary"
/>
</div>
</>
{answerCount} {answerCount === 1 ? 'answer' : 'answers'}
</button>
</div>
)}
{showReceivedStatistics && (
<>
<div className="sm:hidden">
<Button
addonPosition="start"
icon={EyeIcon}
label={`${receivedCount}`}
size="sm"
variant="tertiary"
/>
</div>
<div className="hidden sm:block">
<Button
addonPosition="start"
icon={EyeIcon}
label={`${receivedCount} received this`}
size="sm"
variant="tertiary"
/>
</div>
</>
<div>
<button
className="-my-1 flex items-center rounded-md px-2
py-1 text-xs font-medium
text-slate-500 hover:bg-slate-100 hover:text-slate-600"
type="button">
<EyeIcon aria-hidden={true} className="mr-2 h-5 w-5" />
{receivedCount} received this
</button>
</div>
)}
{showCreateEncounterButton && (
<Button
@@ -323,9 +315,9 @@ export default function BaseQuestionCard({
return (
<article
className={clsx(
'group flex gap-4 border-slate-300',
showHover && 'transition hover:bg-slate-50',
!hideCard && 'rounded-md border bg-white p-4',
'group flex gap-4 border-slate-200',
showHover && 'hover:border-primary-500 transition',
!hideCard && 'rounded-md border bg-white p-4 sm:rounded-lg sm:p-6',
)}>
{cardContent}
{showDeleteButton && (

View File

@@ -1,4 +1,4 @@
import { format } from 'date-fns';
import { formatDistanceToNow } from 'date-fns';
import type { BackendVote } from '../VotingButtons';
import VotingButtons from '../VotingButtons';
@@ -25,7 +25,7 @@ export default function CommentListItem({
onUpvote,
}: CommentListItemProps) {
return (
<div className="flex gap-4 rounded-md border bg-white p-2">
<div className="flex gap-4 rounded-md border border-slate-200 bg-white p-2">
<VotingButtons
size="sm"
upvoteCount={upvoteCount}
@@ -36,15 +36,19 @@ export default function CommentListItem({
<div className="mt-1 flex flex-col gap-1">
<div className="flex items-center gap-2">
<img
alt={`${authorName} profile picture`}
className="h-8 w-8 rounded-full"
src={authorImageUrl}></img>
<h1 className="font-bold">{authorName}</h1>
<p className="pt-1 text-xs font-extralight">
Posted on: {format(createdAt, 'h:mm a, MMMM dd, yyyy')}
alt={authorName}
className="h-7 w-7 rounded-full"
src={authorImageUrl}
/>
<p className="text-sm font-medium text-slate-900">{authorName}</p>
<span className="font-medium text-slate-500">·</span>
<p className="text-xs text-slate-500">
{formatDistanceToNow(createdAt, {
addSuffix: true,
})}
</p>
</div>
<p className="whitespace-pre-line">{content}</p>
<p className="whitespace-pre-wrap text-xs sm:text-sm">{content}</p>
</div>
</div>
);

View File

@@ -142,6 +142,7 @@ export default function CreateQuestionEncounterForm({
(step === 2 && selectedRole === null)
}
label="Next"
size="sm"
variant="primary"
onClick={() => {
setStep(step + 1);
@@ -152,6 +153,7 @@ export default function CreateQuestionEncounterForm({
<Button
isLoading={loading}
label="Submit"
size="sm"
variant="primary"
onClick={async () => {
if (
@@ -181,6 +183,7 @@ export default function CreateQuestionEncounterForm({
)}
<Button
label="Cancel"
size="sm"
variant="tertiary"
onClick={(event) => {
event.preventDefault();

View File

@@ -2,6 +2,8 @@ import type { PropsWithChildren } from 'react';
import { ArrowSmallLeftIcon } from '@heroicons/react/24/outline';
import { Button } from '@tih/ui';
import Container from '~/components/shared/Container';
export type BackButtonLayoutProps = PropsWithChildren<{
href: string;
}>;
@@ -11,7 +13,7 @@ export default function BackButtonLayout({
children,
}: BackButtonLayoutProps) {
return (
<div className="flex w-full flex-1 flex-col items-stretch gap-4 p-4 lg:flex-row">
<Container className="flex flex-col gap-4 pt-4 pb-12" variant="sm">
<div>
<Button
addonPosition="start"
@@ -19,12 +21,13 @@ export default function BackButtonLayout({
href={href}
icon={ArrowSmallLeftIcon}
label="Back"
size="sm"
variant="secondary"
/>
</div>
<div className="flex w-full justify-center overflow-y-auto">
{children}
</div>
</div>
</Container>
);
}

View File

@@ -44,11 +44,12 @@ export default function ExpandedTypeahead({
);
return (
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap items-center gap-2">
{suggestions.map((suggestion) => (
<div key={suggestion.id} className="hidden lg:block">
<Button
label={suggestion.label}
size="sm"
variant="tertiary"
onClick={() => {
onSuggestionClick?.(suggestion);