Skip to content

Commit

Permalink
PAR-606: ReviewDropdown (gitcoinco#41)
Browse files Browse the repository at this point in the history
* ReviewSummary

* wip

* deisgn updates

* clean code

* questionmark

* fix padding

* refactor classes

* add list

* introduce isOpen

* minor fix

* fix breaking build

---------

Co-authored-by: Aditya Anand M C <aditya.anandmc@gmail.com>
  • Loading branch information
0xKurt and thelostone-mc authored Nov 21, 2024
1 parent 6d0d989 commit ed0eb7b
Show file tree
Hide file tree
Showing 19 changed files with 582 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ export { default as ExclamationCircleIcon } from "./exclamationCircle.svg?react"
export { default as XIcon } from "./x.svg?react";
// Solid X Icon
export { default as XSolidIcon } from "./solid/x.svg?react";
export { default as SolidQuestionMarkCircleIcon } from "./solid/questionMarkCircle.svg?react";

// Time Icons
export { default as ClockIcon } from "./clock.svg?react";
export { default as CalendarIcon } from "./calendar.svg?react";

// Miscellaneous Icons
export { default as SparklesIcon } from "./sparkles.svg?react";
export { default as ShineIcon } from "./shine.svg?react";
export { default as UserIcon } from "./user.svg?react";
export { default as VerifiedBadgeIcon } from "./verifiedBadge.svg?react";
export { default as GlobeIcon } from "./globe.svg?react";

Expand Down
4 changes: 4 additions & 0 deletions src/assets/icons/shine.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/solid/questionMarkCircle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/features/checker/components/ReviewDropdown/ReviewDropdown.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meta, Stories, Controls, Story } from "@storybook/blocks";
import * as ReviewDropdownStories from './ReviewDropdown.stories';
import ReviewDropdown from './ReviewDropdown';

<Meta of={ReviewDropdownStories}/>

# Review Summary

The `ReviewDropdown` component is a simple UI element designed to provide a title or summary for a review section.

## Example

<Story name="Default" of={ReviewDropdownStories.Default} />

## Controls

<Controls />

## Other Variations

<Stories />
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from "@storybook/react";

import ReviewDropdown from "./ReviewDropdown";
import { mockData } from "./mockData";

const meta: Meta<typeof ReviewDropdown> = {
component: ReviewDropdown,
title: "Features/Checker/Components/ReviewDropdown", // Adjust the path as per your Storybook organization
} satisfies Meta<typeof ReviewDropdown>;

export default meta;

type Story = StoryObj<typeof ReviewDropdown>;

export const Default: Story = {
args: { evaluation: { ...mockData[0] }, index: 1 },
};

export const Rejected: Story = {
args: { evaluation: { ...mockData[1] }, index: 2 },
};

export const LlmGpt3: Story = {
args: { evaluation: { ...mockData[2] }, index: 3 },
};
195 changes: 195 additions & 0 deletions src/features/checker/components/ReviewDropdown/ReviewDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import * as React from "react";

import { tv, type VariantProps } from "tailwind-variants";

import { cn, formatLocalDate } from "@/lib/utils";
import { Accordion } from "@/primitives/Accordion";
import { Badge } from "@/primitives/Badge/Badge";
import { Icon, IconType } from "@/primitives/Icon";

import { EvaluationSummaryProps } from "./types";

const ReviewDropdownVariants = tv({
slots: {
header: "flex w-full items-center justify-between gap-4 py-8 pr-2",
headerLeft: "flex flex-1 items-center gap-4",
headerRight: "flex items-center justify-end gap-4",
content: "flex w-full flex-col gap-6 p-8",
textRow: "space-x-1 text-left",
status: "text-gray-600",
reviewTitle: "font-sans text-xl text-black",
evaluatorTitle: "absolute my-1 font-sans text-sm font-normal text-gray-900",
reviewDate: "text-base font-normal text-black",
},
defaultVariants: {},
});

const evaluationSummaryVariants = tv({
base: "flex items-center gap-2 self-stretch rounded-lg p-4",
variants: {
background: {
default: "bg-gray-50",
light: "bg-white",
},
},
defaultVariants: {
background: "default",
},
});

export type ReviewDropdownVariants = VariantProps<typeof ReviewDropdownVariants>;
export type EvaluationSummaryVariants = VariantProps<typeof evaluationSummaryVariants>;

interface ReviewDropdownContentProps {
evaluation: EvaluationSummaryProps;
index?: number;
isOpen?: boolean;
}

// Main Component
const ReviewDropdown: React.FC<ReviewDropdownContentProps> = ({
evaluation,
index,
isOpen = true,
}) => {
const accordionVariant = evaluation.evaluatorType === "human" ? "light" : "blue";
return (
<Accordion
border="md"
padding="md"
variant={accordionVariant}
header={<ReviewDropdownHeader evaluation={evaluation} index={index} />}
content={<ReviewDropdownContent evaluation={evaluation} />}
isOpen={isOpen}
/>
);
};

// Header Component
const ReviewDropdownHeader: React.FC<ReviewDropdownContentProps> = ({ evaluation, index }) => {
let reviewTitle = "";
let evaluatorTitle = "";
let evaluatorIconType;

if (evaluation.evaluatorType === "human") {
reviewTitle = `Review ${index ?? ""}`;
evaluatorIconType = IconType.USER;
evaluatorTitle = `by ${evaluation.evaluator.slice(0, 4)}...${evaluation.evaluator.slice(-4)}`;
} else {
reviewTitle = "AI Powered";
evaluatorIconType = IconType.SHINE;
evaluatorTitle = "BETA";
}

const rating = evaluation.evaluation.filter((answer) => answer.answer === "YES").length;

const reviewStatusBadgeVariant =
evaluation.evaluationStatus === "approved"
? "success-strong"
: evaluation.evaluationStatus === "rejected"
? "error-strong"
: "info-strong";

const {
header,
headerLeft,
headerRight,
textRow,
status,
reviewTitle: reviewTitleClass,
evaluatorTitle: evaluatorTitleClass,
reviewDate,
} = ReviewDropdownVariants({
variant: "default",
});

return (
<div className={cn(header())}>
<div className={cn(headerLeft())}>
<Icon type={evaluatorIconType} />
<div>
<p className={cn(textRow())}>
<span className={cn(reviewTitleClass())}>{reviewTitle}</span>
<span className={cn(evaluatorTitleClass())}> {evaluatorTitle}</span>
</p>
<p className={cn(textRow(), reviewDate())}>
Reviewed on {formatLocalDate(evaluation.lastUpdatedAt)}
</p>
</div>
</div>
<div className={cn(headerRight())}>
<div className="flex gap-2">
{getIcon(evaluation.evaluationStatus)}
<p className={cn(status())}>
{rating}/{evaluation.evaluation.length}
</p>
</div>
<Badge variant={reviewStatusBadgeVariant}>
{evaluation.evaluationStatus.charAt(0).toUpperCase() +
evaluation.evaluationStatus.slice(1)}
</Badge>
</div>
</div>
);
};

// Content Component
const ReviewDropdownContent: React.FC<ReviewDropdownContentProps> = ({ evaluation }) => {
const { content } = ReviewDropdownVariants({ variant: "default" });

return (
<div className={cn(content())}>
<EvaluationSummary evaluation={evaluation} />
<EvaluationAnswers evaluation={evaluation} />
</div>
);
};

// Evaluation Summary Component
const EvaluationSummary: React.FC<ReviewDropdownContentProps> = ({ evaluation }) => {
const backgroundClass = evaluationSummaryVariants({
background: evaluation.evaluatorType === "human" ? "default" : "light",
});

return (
<div className={cn(backgroundClass)}>
<p>{evaluation.summary}</p>
</div>
);
};

// Evaluation Answers Component
const EvaluationAnswers: React.FC<ReviewDropdownContentProps> = ({ evaluation }) => {
return (
<div className="flex flex-col gap-6">
{evaluation.evaluation.map((evaluation, index) => (
<div
key={index}
className="flex items-start gap-2 font-sans text-base font-normal leading-7 text-black"
>
<span className="mt-1 shrink-0">{getIcon(evaluation.answer)}</span>
<p className="grow">{evaluation.question}</p>
</div>
))}
</div>
);
};

export default ReviewDropdown;

const getIcon = (value: string) => {
const iconMap = {
approved: IconType.SOLID_CHECK,
rejected: IconType.SOLID_X,
uncertain: IconType.SOLID_QUESTION_MARK_CIRCLE,
YES: IconType.SOLID_CHECK,
NO: IconType.SOLID_X,
UNCERTAIN: IconType.SOLID_QUESTION_MARK_CIRCLE,
};

const iconType = Object.keys(iconMap).includes(value)
? iconMap[value as keyof typeof iconMap]
: IconType.SOLID_QUESTION_MARK_CIRCLE;

return <Icon className="my-0.5" type={iconType} />;
};
73 changes: 73 additions & 0 deletions src/features/checker/components/ReviewDropdown/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { EvaluationSummaryProps } from "./types";

export const mockData: EvaluationSummaryProps[] = [
{
evaluator: "0x1234567890123456789012345678901234567890",
evaluationStatus: "approved",
evaluatorType: "human",
evaluation: [
{
question: "How would you rate the quality of the content?",
answer: "YES",
},
{
question: "Was the content easy to understand?",
answer: "NO",
},
{
question:
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. ",
answer: "UNCERTAIN",
},
],
summary:
"The content was well-researched, engaging, and easy to understand. The overall experience was very positive.",
lastUpdatedAt: "2024-11-20T13:04:36.042449",
},
{
evaluator: "0x1234567890123456789012345678901234567890",
evaluationStatus: "rejected",
evaluatorType: "human",
evaluation: [
{
question: "How would you rate the quality of the content?",
answer: "YES",
},
{
question: "Was the content easy to understand?",
answer: "YES",
},
{
question:
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. ",
answer: "UNCERTAIN",
},
],
summary:
"The content was well-researched, engaging, and easy to understand. The overall experience was very positive.",
lastUpdatedAt: "2024-11-20T13:04:36.042449",
},
{
evaluator: "0x1234567890123456789012345678901234567890",
evaluationStatus: "uncertain",
evaluatorType: "llm_gpt3",
evaluation: [
{
question: "How would you rate the quality of the content?",
answer: "YES",
},
{
question: "Was the content easy to understand?",
answer: "NO",
},
{
question:
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. ",
answer: "UNCERTAIN",
},
],
summary:
"The content was well-researched, engaging, and easy to understand. The overall experience was very positive.",
lastUpdatedAt: "2024-11-20T13:04:36.042449",
},
];
13 changes: 13 additions & 0 deletions src/features/checker/components/ReviewDropdown/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface EvaluationQuestionProps {
question: string;
answer: "YES" | "NO" | "UNCERTAIN";
}

export interface EvaluationSummaryProps {
evaluator: string;
evaluationStatus: "approved" | "rejected" | "pending" | "uncertain";
evaluatorType: "human" | "llm_gpt3";
evaluation: EvaluationQuestionProps[];
summary: string;
lastUpdatedAt: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meta, Stories, Controls, Story } from "@storybook/blocks";
import * as ReviewDropdownListStories from './ReviewDropdownList.stories';
import ReviewDropdownList from './ReviewDropdownList';

<Meta of={ReviewDropdownListStories}/>

# Review Summary

The `ReviewDropdownList` component renders a list of ReviewDropdown.

## Example

<Story name="Default" of={ReviewDropdownListStories.Default} />

## Controls

<Controls />

## Other Variations

<Stories />
Loading

0 comments on commit ed0eb7b

Please sign in to comment.