Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"embla-carousel-react": "^8.3.0",
"graphql-request": "^7.1.0",
"input-otp": "^1.2.4",
"moment": "^2.30.1",
"next-themes": "^0.3.0",
"react-day-picker": "8.10.1",
"react-hook-form": "^7.53.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added public/images/grey-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ProjectReviewList } from "./ProjectReviewList";
import { mockPendingReview0, mockReadyToSubmit0 } from "./mocks";

const meta = {
title: "Features/Checker/ProjectReviewList",
component: ProjectReviewList,
args: {
reviewer: "0x1234567890123456789012345678901234567890",
},
} satisfies Meta;

export default meta;

type Story = StoryObj<typeof ProjectReviewList>;

export const ReadyToSubmit: Story = { args: { projects: mockReadyToSubmit0 } };
export const PendingReview: Story = { args: { projects: mockPendingReview0 } };
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { IconLabel } from "@/components/IconLabel";
import { Button } from "@/primitives/Button";
import { CircleStat } from "@/primitives/Indicators";
import { ListGrid, ListGridColumn } from "@/primitives/ListGrid";

import { getReviewsCount } from "../../utils/getReviewsCount";
import { ProjectReview } from "./types";

export interface ProjectReviewListProps {
reviewer: `0x${string}`;
projects: ProjectReview[];
}

export const ProjectReviewList = ({ reviewer, projects }: ProjectReviewListProps) => {
const columns: ListGridColumn<ProjectReview>[] = [
{
header: "Project",
key: "project",
width: "2fr",
render: (item) => (
<div className="flex items-center gap-4">
<img
src={item.avatarUrl}
alt={item.name}
className="aspect-square size-12 rounded-sm"
onError={(event: React.SyntheticEvent<HTMLImageElement, Event>) => {
event.currentTarget.src = "/images/grey-image.png";
}}
/>
<span>{item.name}</span>
</div>
),
},
{
header: "Date Submitted",
key: "date",
width: "1fr",
render: (item) => <IconLabel type="date" date={item.date} />,
},
{
header: "Reviews",
key: "reviews",
width: "1fr",
render: (item) => {
const { nApproved, nRejected } = getReviewsCount(item.reviews);
return <IconLabel type="reviews" posReviews={nApproved} negReviews={nRejected} />;
},
},
{
header: "AI Suggestion",
key: "aiSuggestion",
width: "1fr",
render: (item) => <IconLabel type="ai-evaluation" percent={item.aiSuggestion} />,
},
{
header: "Score Average",
key: "scoreAverage",
width: "1fr",
position: "center",
render: (item) => (
<div className="flex items-center justify-center">
<CircleStat value={item.scoreAverage} />
</div>
),
},
{
header: "Action",
key: "action",
width: "1fr",
position: "center",
render: (item) => {
const isReviewed = item.reviews.some((review) => review.reviewer === reviewer);
return (
<div className="flex items-center justify-center">
<Button variant="secondary" value="Evaluate project" disabled={isReviewed} />
</div>
);
},
},
];
return (
<ListGrid
data={projects}
columns={columns}
rowClassName="h-[72px]"
getRowKey={(item: ProjectReview) => item.id.toString()}
/>
);
};
71 changes: 71 additions & 0 deletions src/features/checker/components/ProjectReviewList/mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ProjectReview } from "./types";

export const mockPendingReview0: ProjectReview[] = [
{
id: 1,
name: "project title",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [],
aiSuggestion: 60,
scoreAverage: 60,
},
{
id: 2,
name: "project title",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [],
aiSuggestion: 23,
scoreAverage: 23,
},
{
id: 3,
name: "project title",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [],
aiSuggestion: 54,
scoreAverage: 54,
},
];

export const mockReadyToSubmit0: ProjectReview[] = [
{
id: 1,
name: "cool project",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [
{ approved: true, reviewer: "0xJohnDoe" },
{ approved: false, reviewer: "0xJaneDoe" },
{ approved: true, reviewer: "0xJoneDoe" },
],
aiSuggestion: 72,
scoreAverage: 88,
},
{
id: 2,
name: "project title",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [
{ approved: true, reviewer: "0xJohnDoe" },
{ approved: true, reviewer: "0xJoneDoe" },
],
aiSuggestion: 80,
scoreAverage: 92,
},
{
id: 3,
name: "project title",
date: new Date(2024, 5, 3, 15, 0, 0),
avatarUrl: "",
reviews: [
{ approved: true, reviewer: "0xJohnDoe" },
{ approved: true, reviewer: "0xJoneDoe" },
],
aiSuggestion: 80,
scoreAverage: 92,
},
];
14 changes: 14 additions & 0 deletions src/features/checker/components/ProjectReviewList/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface Review {
reviewer: `0x${string}`;
approved: boolean;
}

export interface ProjectReview {
id: number;
name: string;
date: Date;
avatarUrl: string;
reviews: Review[];
aiSuggestion: number;
scoreAverage: number;
}
13 changes: 13 additions & 0 deletions src/features/checker/utils/getReviewsCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Review } from "../components/ProjectReviewList/types";

export const getReviewsCount = (reviews: Review[]) => {
const { nApproved, nRejected } = reviews.reduce(
(acc, review) => {
acc.nApproved += review.approved ? 1 : 0;
acc.nRejected += review.approved ? 0 : 1;
return acc;
},
{ nApproved: 0, nRejected: 0 },
);
return { nApproved, nRejected };
};
22 changes: 22 additions & 0 deletions src/primitives/ListGrid/ListGrid.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react";

import { ListGrid } from "./ListGrid";
import { mockColumns0, mockGetRowKey0, TMockData0 } from "./mocks";
import { mockData0 } from "./mocks";

const meta = {
title: "Primitives/ListGrid",
component: ListGrid,
} satisfies Meta;

export default meta;

type Story<T> = StoryObj<typeof ListGrid<T>>;

export const Default: Story<TMockData0> = {
args: {
data: mockData0,
columns: mockColumns0,
getRowKey: mockGetRowKey0,
},
};
83 changes: 83 additions & 0 deletions src/primitives/ListGrid/ListGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from "react";

import { tv } from "tailwind-variants";

const listGridVariants = tv({
slots: {
root: "mx-auto flex flex-col gap-4 overflow-x-auto",
header: "grid gap-4 px-4 py-2",
headerElement: "flex items-center",
row: "grid items-center gap-4 px-4 py-2",
},
variants: {
variant: {
default: {
header: "font-sans text-base font-bold text-black",
row: "font-sans text-base font-normal text-black",
},
},
position: {
center: { headerElement: "justify-center" },
left: { headerElement: "justify-start" },
right: { headerElement: "justify-end" },
},
},
defaultVariants: {
variant: "default",
position: "left",
},
});

export interface ListGridColumn<T> {
header: React.ReactNode;
key: keyof T | string;
position?: "center" | "left" | "right";
width?: string;
render: (item: T) => React.ReactNode;
}

export interface ListGridProps<T> {
data: T[];
columns: ListGridColumn<T>[];
getRowKey: (item: T) => string | number;
className?: string;
rowClassName?: string;
}

export const ListGrid = <T,>({
data,
columns,
getRowKey,
className,
rowClassName,
}: ListGridProps<T>) => {
const { root, header, headerElement, row } = listGridVariants();

// Generate grid-template-columns style based on column widths
const gridTemplateColumns = columns
.map((column) => column.width || "1fr") // Default to '1fr' if no width is specified
.join(" ");

return (
<div className={root({ className })}>
<div className={header()} style={{ gridTemplateColumns }}>
{columns.map((column, index) => (
<div key={index} className={headerElement({ position: column.position })}>
{column.header}
</div>
))}
</div>
{data.map((item, index) => (
<div
key={getRowKey ? getRowKey(item) : index}
className={row({ className: rowClassName })}
style={{ gridTemplateColumns }}
>
{columns.map((column) => (
<React.Fragment key={column.key as string}>{column.render(item)}</React.Fragment>
))}
</div>
))}
</div>
);
};
1 change: 1 addition & 0 deletions src/primitives/ListGrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ListGrid";
28 changes: 28 additions & 0 deletions src/primitives/ListGrid/mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ListGridColumn } from "./ListGrid";

export interface TMockData0 {
id: number;
name: string;
description: string;
}

export const mockData0: TMockData0[] = [
{ id: 1, name: "Item 1", description: "Description 1" },
{ id: 2, name: "Item 2", description: "Description 2" },
{ id: 3, name: "Item 3", description: "Description 3" },
];

export const mockColumns0: ListGridColumn<TMockData0>[] = [
{
header: "Name",
key: "name",
render: (item: TMockData0) => <span>{item.name}</span>,
},
{
header: "Description",
key: "description",
render: (item: TMockData0) => <span>{item.description}</span>,
},
];

export const mockGetRowKey0 = (item: TMockData0) => item.id;