diff --git a/package.json b/package.json index c91af3c9..661c3053 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@code4rena/components-library", - "version": "3.0.2", + "version": "3.1.0", "description": "Code4rena's official components library ", "types": "./dist/lib.d.ts", "exports": { diff --git a/src/lib/ContestTile/CompactTemplate.tsx b/src/lib/ContestTile/CompactTemplate.tsx index ea4ebd2a..f0772ae8 100644 --- a/src/lib/ContestTile/CompactTemplate.tsx +++ b/src/lib/ContestTile/CompactTemplate.tsx @@ -1,10 +1,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import clsx from "clsx"; import { BountyTileData, ContestSchedule, ContestTileData, ContestTileProps, ContestTileVariant } from "./ContestTile.types"; -import { Status } from '../types'; +import { Status, TagSize, TagVariant } from '../types'; import { ContestStatus } from '../ContestStatus'; import { Countdown } from './ContestTile'; import { getDates } from '../../utils/time'; +import { Tag } from '../Tag'; +import { Icon } from '../Icon'; +import wolfbotIcon from "../../../public/icons/wolfbot.svg"; export default function CompactTemplate({ variant, @@ -19,6 +22,7 @@ export default function CompactTemplate({ "compact--light": variant === ContestTileVariant.COMPACT_LIGHT, "compact--dark": variant === ContestTileVariant.COMPACT_DARK, }); + const isDarkTile = variant === ContestTileVariant.DARK || variant === ContestTileVariant.COMPACT_DARK; const tileClasses = clsx({ c4contesttile: true, @@ -29,11 +33,13 @@ export default function CompactTemplate({
{contestData && } {bountyData && { - const { startDate, endDate, amount, contestUrl, contestType } = contestData; + const { startDate, endDate, amount, contestUrl, contestType, ecosystem, languages } = contestData; const [contestTimelineObject, setContestTimelineObject] = useState(); + const [hasBotRace, setHasBotRace] = useState(contestData ? !!contestData.botFindingsRepo : false); + let ecosystemLogoName: string = ""; + if (ecosystem && (ecosystem === "Polkadot" || ecosystem === "Ethereum")) { + ecosystemLogoName = `logo-${ecosystem.toLowerCase()}` + } const updateContestTileStatus = useCallback(() => { if (contestData) { @@ -137,16 +149,48 @@ const IsContest = ({title, contestData, sponsorUrl, sponsorImage}: {

{amount}

+ {((hasBotRace && contestTimelineObject && (contestTimelineObject.botRaceStatus === Status.UPCOMING || + contestTimelineObject.botRaceStatus === Status.LIVE)) || ecosystem || languages?.length > 0) &&
+ {hasBotRace && contestTimelineObject && + (contestTimelineObject.botRaceStatus === Status.UPCOMING || + contestTimelineObject.botRaceStatus === Status.LIVE) && ( + + )} + {ecosystem && : undefined} + size={TagSize.NARROW} + />} + {languages + && languages.length > 0 + && languages.map((language) => + )} +
} )} -const IsBounty = ({title, bountyData, sponsorUrl, sponsorImage}: { +const IsBounty = ({title, isDarkTile = true, bountyData, sponsorUrl, sponsorImage}: { title: string; + isDarkTile: boolean; bountyData: BountyTileData; sponsorUrl: string | undefined; sponsorImage: string | undefined; }) => { - const { amount, bountyUrl } = bountyData; + const { amount, bountyUrl, ecosystem, languages } = bountyData; + let ecosystemLogoName: string = ""; + if (ecosystem && (ecosystem === "Polkadot" || ecosystem === "Ethereum")) { + ecosystemLogoName = `logo-${ecosystem.toLowerCase()}` + } return (
@@ -189,5 +233,20 @@ const IsBounty = ({title, bountyData, sponsorUrl, sponsorImage}: { Max Bounty

{amount}

+ {(ecosystem || languages?.length > 0) &&
+ {ecosystem && : undefined} + size={TagSize.NARROW} + />} + {languages + && languages.length > 0 + && languages.map((language) => + )} +
} )} \ No newline at end of file diff --git a/src/lib/ContestTile/ContestTile.scss b/src/lib/ContestTile/ContestTile.scss index 472aae7f..5fc10582 100644 --- a/src/lib/ContestTile/ContestTile.scss +++ b/src/lib/ContestTile/ContestTile.scss @@ -437,6 +437,39 @@ } } + .tags { + padding: 0.24rem 0.5rem 0.5rem; + display: flex; + flex-direction: row; + gap: 0.5rem; + flex-wrap: wrap; + + img { + width: 0.75rem !important; + height: 0.75rem !important; + } + + .icon { + svg { + min-width: 1.125rem; + min-height: 1.125rem; + width: 1.125rem; + height: 1.125rem; + } + } + + + .separator { + height: 26px; + } + + p { + padding-top: 5px; + padding-bottom: 5px; + font-size: 0.75rem !important; + } + } + .amount { margin: 0px; margin-left: auto; @@ -506,7 +539,16 @@ display: block; text-align: center; } - + + .tags { + margin-top: 0.5rem; + display: flex; + flex-direction: row; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: center; + } + .title { margin: 0rem; color: $color__white; @@ -514,16 +556,6 @@ font-size: $headline-font-size__xxs; } - .bot-race-status { - display: flex; - gap: 0.5rem; - margin-top: 0.25rem; - align-items: center; - justify-content: center; - color: $color__white; - font-weight: 700; - } - .description { margin: $spacing__s 0rem 0rem; } @@ -587,7 +619,7 @@ text-align: left; } - .bot-race-status { + .tags { justify-content: flex-start; } } diff --git a/src/lib/ContestTile/ContestTile.stories.tsx b/src/lib/ContestTile/ContestTile.stories.tsx index 6c994c0b..9ff53b13 100644 --- a/src/lib/ContestTile/ContestTile.stories.tsx +++ b/src/lib/ContestTile/ContestTile.stories.tsx @@ -1,7 +1,7 @@ import React, { Fragment } from "react"; import { ContestTile } from "./ContestTile"; import { Meta, StoryObj } from "@storybook/react"; -import { ContestTileVariant } from "./ContestTile.types"; +import { CodingLanguage, ContestEcosystem, ContestTileVariant } from "./ContestTile.types"; const meta: Meta = { component: ContestTile, @@ -39,9 +39,12 @@ const defaultArgs = { contestType: "Open Audit", isUserCertified: false, contestId: 321, + ecosystem: "Ethereum" as ContestEcosystem, + languages: ["Rust"] as CodingLanguage[], contestUrl: "https://code4rena.com/audits/2023-07-axelar-network#top", contestRepo: "https://github.com/code-423n4/2023-07-axelar", findingsRepo: "https://github.com/code-423n4/2023-07-axelar", + botFindingsRepo: "https://github.com/code-423n4/2023-07-axelar", amount: "$80,000 USDC", startDate: "2030-07-12T18:00:00Z", endDate: "2030-07-21T18:00:00.000Z", @@ -162,6 +165,8 @@ BountyTile.args = { startDate: "2023-07-12T18:00:00Z", repoUrl: "https://github.com/code-423n4/2023-07-axelar", bountyUrl: "https://code4rena.com/audits/2023-07-axelar-network#top", + ecosystem: "Polkadot" as ContestEcosystem, + languages: ["Rust"] as CodingLanguage[], }, variant: ContestTileVariant.LIGHT, sponsorImage: "/logos/apple-touch-icon.png", diff --git a/src/lib/ContestTile/ContestTile.types.ts b/src/lib/ContestTile/ContestTile.types.ts index a9299dd7..2e38e512 100644 --- a/src/lib/ContestTile/ContestTile.types.ts +++ b/src/lib/ContestTile/ContestTile.types.ts @@ -8,6 +8,10 @@ export enum ContestTileVariant { COMPACT_DARK = "COMPACT_DARK" } +export type ContestEcosystem = "Algorand" | "Aptos" | "Blast" | "Cosmos" | "Ethereum" | "EVM" | "NEAR" | "Polkadot" | "Scroll" | "Sei" | "Solana" | "StarkNet" | "Stellar" | "Sui" | "Other"; + +export type CodingLanguage = "Cairo" | "GO" | "HUFF" | "Ink" | "Move" | "Noir" | "Other" | "Rain" | "Rust" | "Rust evm" | "Solidity" | "Vyper" | "Yui"; + export interface ContestTileProps { /** An html `id` for the contest tile's wrapping div. */ htmlId?: string; @@ -38,6 +42,10 @@ export interface BountyTileData { bountyUrl: string; /** Absolute url to the bounty's source code. */ repoUrl: string; + /** Ecosystem being deployed to for the current contest. */ + ecosystem: ContestEcosystem; + /** Coding language for the current contest. */ + languages: CodingLanguage[]; /** Callback function to be triggered on bounty time/status changes. */ updateBountyStatus?: () => void; } @@ -57,6 +65,10 @@ export interface ContestTileData { findingsRepo: string; /** Absolute url to the contest's findings. */ botFindingsRepo?: string; + /** Ecosystem being deployed to for the current contest. */ + ecosystem: ContestEcosystem; + /** Coding language for the current contest. */ + languages: CodingLanguage[]; /** Reward pool for the current contest. */ amount: string; /** Callback function to be triggered on contest time/status changes. */ diff --git a/src/lib/ContestTile/DefaultTemplate.tsx b/src/lib/ContestTile/DefaultTemplate.tsx index 51e068ce..662d75b8 100644 --- a/src/lib/ContestTile/DefaultTemplate.tsx +++ b/src/lib/ContestTile/DefaultTemplate.tsx @@ -2,13 +2,14 @@ import React, { Fragment, useCallback, useEffect, useState } from "react"; import clsx from "clsx"; import wolfbotIcon from "../../../public/icons/wolfbot.svg"; import { BountyTileData, ContestSchedule, ContestTileData, ContestTileProps, ContestTileVariant } from "./ContestTile.types"; -import { DropdownLink, Status } from "../types"; +import { DropdownLink, Status, TagSize, TagVariant } from "../types"; import { ContestStatus } from "../ContestStatus"; import { Countdown } from "./ContestTile"; import { getDates } from "../../utils/time"; import { isBefore } from "date-fns"; import { Dropdown } from "../Dropdown"; import { Icon } from "../Icon"; +import { Tag } from "../Tag"; export default function DefaultTemplate({ @@ -26,6 +27,7 @@ export default function DefaultTemplate({ "tile--light": variant === ContestTileVariant.LIGHT, "tile--dark": variant === ContestTileVariant.DARK, }); + const isDarkTile = variant === ContestTileVariant.DARK || variant === ContestTileVariant.COMPACT_DARK const [hasBotRace, setHasBotRace] = useState(false); const [canViewContest, setCanViewContest] = useState(false); const [dropdownLinks, setDropdownLinks] = useState([]); @@ -148,6 +150,7 @@ export default function DefaultTemplate({
{contestData && } {bountyData && void; contestTimelineObject: ContestSchedule | undefined; }) { - const { contestUrl, amount, findingsRepo, startDate, endDate } = contestData; + const { contestUrl, amount, findingsRepo, startDate, endDate, ecosystem, languages } = contestData; + let ecosystemLogoName: string = ""; + if (ecosystem && (ecosystem === "Polkadot" || ecosystem === "Ethereum")) { + ecosystemLogoName = `logo-${ecosystem.toLowerCase()}` + } return ( @@ -296,23 +306,35 @@ function IsContest({ {/* Contest description */}

- {description}{" "} - {hasBotRace && contestTimelineObject && - (contestTimelineObject.botRaceStatus === Status.UPCOMING || - contestTimelineObject.botRaceStatus === Status.LIVE) && ( - - Wolf bot - {contestTimelineObject.botRaceStatus === - Status.UPCOMING && "1st hour: Bot Race"} - {contestTimelineObject.botRaceStatus === Status.LIVE && - "Bot Race live"} - - )} + {description}{" "} + {((hasBotRace && contestTimelineObject && (contestTimelineObject.botRaceStatus === Status.UPCOMING || + contestTimelineObject.botRaceStatus === Status.LIVE)) + || ecosystem + || languages?.length > 0) &&

+ {hasBotRace && contestTimelineObject && + (contestTimelineObject.botRaceStatus === Status.UPCOMING || + contestTimelineObject.botRaceStatus === Status.LIVE) && ( + + )} + {ecosystem && : undefined} + size={TagSize.NARROW} + />} + {languages + && languages.length > 0 + && languages.map((language) => + )} +
}

@@ -365,6 +387,7 @@ function IsBounty({ sponsorUrl, sponsorImage, bountyData, + isDarkTile = true, updateBountyTileStatus, bountyTimelineObject }: { @@ -373,94 +396,114 @@ function IsBounty({ sponsorUrl?: string; sponsorImage?: string; bountyData: BountyTileData; + isDarkTile: boolean; updateBountyTileStatus?: () => void; bountyTimelineObject?: ContestSchedule | undefined; }) { - const { bountyUrl, amount, startDate } = bountyData; + const { bountyUrl, amount, startDate, ecosystem, languages } = bountyData; const endDate = "2999-01-01T00:00:00Z" + let ecosystemLogoName: string = ""; + if (ecosystem && (ecosystem === "Polkadot" || ecosystem === "Ethereum")) { + ecosystemLogoName = `logo-${ecosystem.toLowerCase()}` + } - return ( - -
-
- {/* Sponsor Image */} - {sponsorUrl ? ( + return ( + +
+
+ {/* Sponsor Image */} + {sponsorUrl ? ( + e.stopPropagation()} + > + Sponsor logo + + ) : ( + Sponsor logo + )} +
+ {/* Contest title */} +

e.stopPropagation()} > - Sponsor logo + {title} - ) : ( - Sponsor logo + {/* Contest description */} +

+ {description} + {(ecosystem || languages?.length > 0) &&

+ {ecosystem && : undefined} + size={TagSize.NARROW} + />} + {languages + && languages.length > 0 + && languages.map((language) => + )} +
} +

+

+
+ {/* Reward pool amount */} +
+

{amount}

+

Max award

+
+
+ {/* Contest tile footer */} +
+
+ {bountyTimelineObject && } + {bountyData && bountyTimelineObject && bountyTimelineObject.contestStatus === Status.UPCOMING && ( +
+ +
)} -
- {/* Contest title */} -

- e.stopPropagation()} - > - {title} - -

- {/* Contest description */} -

- {description} -

-
-
- {/* Reward pool amount */} -
-

{amount}

-

Max award

-
- {/* Contest tile footer */} - -
- ) +
+ e.stopPropagation()} + > + View details + +
+ + + ) } \ No newline at end of file diff --git a/src/lib/Icon/iconList.tsx b/src/lib/Icon/iconList.tsx index 615b7ed0..a1ec991b 100644 --- a/src/lib/Icon/iconList.tsx +++ b/src/lib/Icon/iconList.tsx @@ -13,6 +13,10 @@ export const icons = (size: string, color: string, className?: string): Record , + "bell": + + + , "book": @@ -103,6 +107,17 @@ export const icons = (size: string, color: string, className?: string): Record , + "eye-closed": + + + + + , + "eye-open": + + + + , "findings": @@ -113,6 +128,14 @@ export const icons = (size: string, color: string, className?: string): Record , + "grab-vertical": + + + + + + + , "heart": @@ -186,6 +209,17 @@ export const icons = (size: string, color: string, className?: string): Record , + "logo-ethereum": + + , + "logo-polkadot": + + + + + + + , "menu": diff --git a/src/lib/Tag/Tag.scss b/src/lib/Tag/Tag.scss index 99b031bc..56072260 100644 --- a/src/lib/Tag/Tag.scss +++ b/src/lib/Tag/Tag.scss @@ -2,10 +2,10 @@ .c4tag, .tag--default { height: fit-content; - padding: $spacing__xs 0.75rem; + width: fit-content; font-family: $font__default; font-size: 1rem; - font-weight: bold !important; + font-weight: 500 !important; letter-spacing: 0.5px; display: flex; flex-direction: row; @@ -15,35 +15,106 @@ border-radius: 2.25rem; border-width: 1px; border-style: solid; - border-color: $color__n-80; + border-color: $color__n-40; background: $color__n-80; + .separator { + height: 30px; + width: 1px; + background: $color__n-40; + border: 0px; + } + + img { + margin: $spacing__xs 0 $spacing__xs 0.5rem; + } + + .icon { + padding: $spacing__xs 0 $spacing__xs 0.5rem; + + svg { + display: block; + min-height: 22px; + min-width: 22px; + height: 22px; + width: 22px; + } + } + + p { + margin: 0; + padding: $spacing__xs 0.75rem $spacing__xs 0; + line-height: normal; + + &:not(.has-icon) { + padding-left: 0.75rem; + } + } + &.wide { - padding: $spacing__s $spacing__m; + .separator { + height: 38px; + } + + p { + padding: $spacing__s $spacing__m $spacing__s 0; + + &:not(.has-icon) { + padding-left: $spacing__m; + } + } + + img { + margin: $spacing__s 0 $spacing__s $spacing__s; + } + + .icon { + padding: $spacing__s 0 $spacing__s $spacing__s; + } } &.tag--blurple { background: $color__blurple-60; border-color: $color__blurple-60; + + .separator { + background: $color__blurple-30; + } } &.tag--white { background: rgba(255, 255, 255, 0.13); - border-color: none; + border-color: $color__n-40; + + .separator { + background: $color__n-40; + } } &.tag--red { background: transparent; border-color: $color__red; + + .separator { + background: $color__red; + } } &.tag--yellow { background: transparent; border-color: $color__y-1; + + .separator { + background: $color__y-1; + } } &.tag--white-outline { background: transparent; border-color: $color__white; + + .separator { + background: $color__white; + } } } \ No newline at end of file diff --git a/src/lib/Tag/Tag.tsx b/src/lib/Tag/Tag.tsx index f4562af1..606210e5 100644 --- a/src/lib/Tag/Tag.tsx +++ b/src/lib/Tag/Tag.tsx @@ -17,7 +17,7 @@ import "./Tag.scss"; * @param variant - Style variant to be applied to rendered component. * @param label - Label to be attached to the tag. * @param size - Standard button size options. - * @param iconLeft - Relative path or absolute url to an icon/image. Renders icon to the left of label. + * @param iconLeft - Relative path/absolute url to an icon/image or a components-library Icon. Renders icon to the left of label. * @param className - String of custom classes to extend the default styling of the component. * @param id - HTML element identifier. */ @@ -42,8 +42,12 @@ export const Tag: React.FC = ({ return (
- {iconLeft && } - {label} + {iconLeft && typeof iconLeft === "string" && } + {iconLeft && typeof iconLeft !== "string" &&
+ {iconLeft} +
} + {iconLeft &&
} +

{label}

); }; diff --git a/src/lib/Tag/Tag.types.ts b/src/lib/Tag/Tag.types.ts index 43ba155b..fc26188f 100644 --- a/src/lib/Tag/Tag.types.ts +++ b/src/lib/Tag/Tag.types.ts @@ -1,3 +1,5 @@ +import { ReactNode } from "react"; + export enum TagVariant { WHITE = "WHITE", DEFAULT = "DEFAULT", @@ -18,7 +20,7 @@ export interface TagProps { /** Standard button size options */ size?: TagSize; /** Relative path or absolute url to an icon/image. Renders icon to the left of label */ - iconLeft?: string; + iconLeft?: string | ReactNode; /** Label to be attached to the tag */ label: string; /** String of custom classes to extend the default styling of the component. */