Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add collection vote table #525

Merged
merged 23 commits into from
Dec 5, 2023
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 lang/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,5 @@
'top' => 'Top',
'all_chains' => 'All chains',
'votes' => 'Votes',
'vol' => 'Vol',
];
1 change: 1 addition & 0 deletions lang/en/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
'vote' => 'Vote',
'time_left' => 'Time Left',
'or_nominate_collection' => 'Or nominate a collection',
'vote_to_reveal' => 'Vote to reveal data',
],
'sorting' => [
'token_number' => 'Token Number',
Expand Down
16 changes: 16 additions & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,19 @@
background-image: linear-gradient(90deg, rgb(var(--theme-color-dark-800)) 55.67%, transparent 151.85%);
}
}

.hidden-votes-svg-line {
@apply fill-theme-primary-600;
}

.dark .hidden-votes-svg-line {
@apply fill-theme-primary-400;
}

.hidden-votes-svg-shape {
@apply fill-none stroke-[#C2C2FF];
}

.dark .hidden-votes-svg-shape {
@apply fill-none stroke-[#47509F];
}
1 change: 1 addition & 0 deletions resources/icons/hidden-vote.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion resources/js/I18n/Locales/en.json

Large diffs are not rendered by default.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { within } from "@testing-library/react";
import { expect } from "vitest";
import {
VoteCollection,
type VoteCollectionProperties,
VoteCollections,
VoteCount,
} from "@/Pages/Collections/Components/CollectionVoting/VoteCollections";
import { render, screen } from "@/Tests/testing-library";

const demoCollection: VoteCollectionProperties = {
index: 1,
name: "AlphaDogs",
image: "https://i.seadn.io/gcs/files/4ef4a60496c335d66eba069423c0af90.png?w=500&auto=format",
volume: "256.000000000000000000",
volumeCurrency: "ETH",
volumeDecimals: 18,
votes: 15,
};

const collections = Array.from({ length: 8 }).fill(demoCollection) as VoteCollectionProperties[];

describe("VoteCollections", () => {
it("should render collections in two block, 4 collection in each", () => {
render(<VoteCollections collections={collections} />);

const leftBlock = screen.getByTestId("VoteCollections_Left");
const rightBlock = screen.getByTestId("VoteCollections_Right");

expect(within(leftBlock).getAllByText("AlphaDogs").length).toBe(4);
expect(within(rightBlock).getAllByText("AlphaDogs").length).toBe(4);
});
});
describe("VoteCollection", () => {
it("should render the component", () => {
render(<VoteCollection collection={demoCollection} />);

expect(screen.getByText("AlphaDogs")).toBeInTheDocument();
});

it("should render volume of the collection", () => {
render(<VoteCollection collection={demoCollection} />);

expect(screen.getByText(/Vol: 256 ETH/)).toBeInTheDocument();
});
});

describe("VoteCount", () => {
it("should render without vote count", () => {
render(<VoteCount />);

expect(screen.getByTestId("icon-HiddenVote")).toBeInTheDocument();
});

it("should render with vote count", () => {
render(<VoteCount voteCount={15} />);

expect(screen.getByText("15")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { twMerge } from "tailwind-merge";
import { VoteCountdown } from "./VoteCountdown";
import { Heading } from "@/Components/Heading";
import { Icon } from "@/Components/Icon";
import { Img } from "@/Components/Image";
import { LinkButton } from "@/Components/Link";
import { Tooltip } from "@/Components/Tooltip";
import { FormatCrypto } from "@/Utils/Currency";

export interface VoteCollectionProperties {
name: string;
image: string;
volume?: string;
volumeCurrency?: string;
volumeDecimals?: number;
votes?: number;
index: number;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dummy interface, should be linked with backend data


export const VoteCollections = ({ collections }: { collections: VoteCollectionProperties[] }): JSX.Element => {
const { t } = useTranslation();

return (
<div className="flex w-full min-w-0 flex-col gap-4 rounded-xl border-theme-secondary-300 p-0 dark:border-theme-dark-700 lg:gap-6 lg:border lg:p-8">
<Heading level={2}>{t("pages.collections.vote.vote_for_top_collection")}</Heading>

<div className="grid grid-cols-1 sm:grid-cols-2 sm:gap-x-2.5">
<div
className="max-w-full flex-1 space-y-2"
data-testid="VoteCollections_Left"
>
{collections.slice(0, 4).map((collection, index) => (
<VoteCollection
key={index}
collection={collection}
/>
))}
</div>
<div
className="hidden flex-1 space-y-2 sm:block"
data-testid="VoteCollections_Right"
>
{collections.slice(4, 8).map((collection, index) => (
<VoteCollection
key={index}
collection={collection}
/>
))}
</div>
</div>

<div className="flex w-full flex-col items-center gap-4 sm:flex-row sm:justify-between">
<VoteCountdown />

<LinkButton
onClick={(): void => {
console.log("TODO: Implement or nominate collection");
}}
variant="link"
className="font-medium leading-6 dark:hover:decoration-theme-primary-400"
fontSize="!text-base"
textColor="!text-theme-primary-600 dark:!text-theme-primary-400"
>
{t("pages.collections.vote.or_nominate_collection")}
</LinkButton>
</div>
</div>
);
};

export const VoteCollection = ({ collection }: { collection: VoteCollectionProperties }): JSX.Element => {
const { t } = useTranslation();

return (
<div
tabIndex={0}
className="cursor-pointer rounded-lg border border-theme-secondary-300 px-4 py-4 hover:outline hover:outline-theme-hint-100 focus:outline-none focus:ring focus:ring-theme-hint-100 dark:border-theme-dark-700 dark:border-theme-dark-700 dark:hover:outline-theme-dark-500 dark:hover:outline-theme-dark-500 dark:focus:ring-theme-dark-500 md:py-3"
>
<div className="flex items-center justify-between">
<div className="flex min-w-0 flex-1 items-center space-x-3">
<div className="flex">
<div className="relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-theme-secondary-100 dark:bg-theme-vote-background xs:h-12 xs:w-12">
<span className="font-medium text-theme-secondary-700 dark:text-theme-dark-200">
{collection.index}
</span>
</div>
<div className="relative -ml-2 h-8 w-8 shrink-0 xs:h-12 xs:w-12">
<Img
wrapperClassName="aspect-square"
className="h-full w-full rounded-full rounded-full bg-white object-cover ring-4 ring-white dark:bg-theme-dark-700 dark:ring-theme-dark-900"
isCircle
src={collection.image}
/>
</div>
</div>

<div className="break-word-legacy min-w-0 space-y-0.5 ">
<p
data-testid="CollectionName__name"
className="truncate text-base font-medium text-theme-secondary-900 dark:text-theme-dark-50 md-lg:text-base"
>
{collection.name}
</p>
<p className="hidden text-sm font-medium leading-5.5 text-theme-secondary-700 dark:text-theme-dark-200 md-lg:block">
{t("common.vol")}:{" "}
<FormatCrypto
value={collection.volume ?? "0"}
token={{
symbol: collection.volumeCurrency ?? "ETH",
name: collection.volumeCurrency ?? "ETH",
decimals: collection.volumeDecimals ?? 18,
}}
/>
</p>
<div className="mt-0.5 md-lg:hidden">
<VoteCount
iconClass="h-6 w-8"
textClass="text-sm md:text-sm"
voteCount={collection.votes}
/>
</div>
</div>
</div>

<div className="ml-2 hidden md-lg:block">
<VoteCount voteCount={collection.votes} />
</div>
</div>
</div>
);
};

export const VoteCount = ({
iconClass,
textClass,
voteCount,
}: {
iconClass?: string;
textClass?: string;
voteCount?: number;
}): JSX.Element => {
const { t } = useTranslation();
return (
<div className="flex items-center space-x-2">
<p
className={twMerge(
"text-sm font-medium leading-5.5 text-theme-secondary-700 dark:text-theme-dark-200 md:text-base md:leading-6",
textClass,
)}
>
Votes
</p>
{voteCount !== undefined ? (
<p className={twMerge("font-medium text-theme-secondary-900 dark:text-theme-dark-50", textClass)}>
{voteCount}
</p>
) : (
<Tooltip content={t("pages.collections.vote.vote_to_reveal")}>
<div>
<Icon
className={twMerge("h-7 w-9", iconClass)}
name="HiddenVote"
size="2xl"
/>
</div>
</Tooltip>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./VoteCollections";
export * from "./VoteCountdown";
23 changes: 20 additions & 3 deletions resources/js/Pages/Collections/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { FeaturedCollectionsCarousel } from "./Components/FeaturedCollections";
import { PopularCollectionsFilterPopover } from "./Components/PopularCollectionsFilterPopover";
import { type PopularCollectionsSortBy, PopularCollectionsSorting } from "./Components/PopularCollectionsSorting";
import { ButtonLink } from "@/Components/Buttons/ButtonLink";
import { CollectionOfTheMonthWinners } from "@/Components/Collections/CollectionOfTheMonthWinners";
import { PopularCollectionsTable } from "@/Components/Collections/PopularCollectionsTable";
import { Heading } from "@/Components/Heading";
import { type PaginationData } from "@/Components/Pagination/Pagination.contracts";
import { useIsFirstRender } from "@/Hooks/useIsFirstRender";
import { DefaultLayout } from "@/Layouts/DefaultLayout";
import { CollectionOfTheMonth } from "@/Pages/Collections/Components/CollectionOfTheMonth";
import { type VoteCollectionProperties, VoteCollections } from "@/Pages/Collections/Components/CollectionVoting";
import { type ChainFilter, ChainFilters } from "@/Pages/Collections/Components/PopularCollectionsFilters";

interface Filters extends Record<string, FormDataConvertible> {
Expand All @@ -29,6 +30,15 @@ interface CollectionsIndexProperties extends PageProps {
filters: Filters;
}

const demoCollection: VoteCollectionProperties = {
index: 1,
name: "AlphaDogs",
image: "https://i.seadn.io/gcs/files/4ef4a60496c335d66eba069423c0af90.png?w=500&auto=format",
volume: "256.000000000000000000",
volumeCurrency: "ETH",
volumeDecimals: 18,
};

const CollectionsIndex = ({
title,
featuredCollections,
Expand Down Expand Up @@ -133,10 +143,17 @@ const CollectionsIndex = ({
<ViewAllButton />
</div>
</div>
<div className="mt-12 flex w-full flex-col gap-4 xl:flex-row">
<VoteCollections
collections={Array.from({ length: 8 }).fill(demoCollection) as VoteCollectionProperties[]}
/>
<CollectionOfTheMonthWinners
winners={topCollections}
className="hidden xl:flex"
/>
</div>
</div>

<CollectionOfTheMonth winners={topCollections} />

<CollectionsCallToAction />
</DefaultLayout>
);
Expand Down
2 changes: 2 additions & 0 deletions resources/js/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { ReactComponent as GridWithPencil } from "@icons/grid-with-pencil.svg";
import { ReactComponent as Grid } from "@icons/grid.svg";
import { ReactComponent as Heart } from "@icons/heart.svg";
import { ReactComponent as HeartbeatInCircle } from "@icons/heartbeat-in-circle.svg";
import { ReactComponent as HiddenVote } from "@icons/hidden-vote.svg";
import { ReactComponent as Image } from "@icons/image.svg";
import { ReactComponent as InfoInCircle } from "@icons/info-in-circle.svg";
import { ReactComponent as KeyboardDelete } from "@icons/keyboard-delete.svg";
Expand Down Expand Up @@ -234,4 +235,5 @@ export const SvgCollection = {
AudioPlay,
Polygon,
Ethereum,
HiddenVote,
};
Loading