Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/app/core/components/Icons/CheckDouble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function CheckDoubleIcon() {
return (
<svg
className="fill-current w-full h-full"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15 6H17V8H15V6ZM13 10V8H15V10H13ZM11 12V10H13V12H11ZM9 14V12H11V14H9ZM7 16V14H9V16H7ZM5 16H7V18H5V16ZM3 14H5V16H3V14ZM3 14H1V12H3V14ZM11 16H13V18H11V16ZM15 14V16H13V14H15ZM17 12V14H15V12H17ZM19 10V12H17V10H19ZM21 8H19V10H21V8ZM21 8H23V6H21V8Z"
/>
</svg>
);
}

export default CheckDoubleIcon;

23 changes: 23 additions & 0 deletions src/app/core/components/MobileMenu/MobileNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ export function MobileNavigation({ handleNavToggle }: IProps) {
</svg>

<span>Vaults</span>
</Link>{" "}
<Link
href="/governance/boosters"
onClick={() => handleNavToggle()}
className="text-neutral-900 dark:text-breadgray-rye dark:hover:text-breadgray-light-grey flex gap-2 items-center justify-end px-2"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="fill-current"
>
<g opacity="0.5">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14 6H20H22V8V14H20V10H18V8H14V6ZM16 12V10H18V12H16ZM14 14V12H16V14H14ZM12 14H14V16H12V14ZM10 12H12V14H10V12ZM8 12V10H10V12H8ZM6 14V12H8V14H6ZM4 16V14H6V16H4ZM4 16V18H2V16H4Z"
/>
</g>
</svg>

<span>Boosters</span>
</Link>
</>
)}
Expand Down
54 changes: 54 additions & 0 deletions src/app/governance/boosters/BoosterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";
import { useEffect, useState } from "react";
import { useConnectedUser } from "@/app/core/hooks/useConnectedUser";
import { PageGrid } from "@/app/governance/components/PageGrid";
import { VotingPowerPanel } from "./components/VotingPowerPanel";
import { BoosterCard } from "./components/BoosterCard";
import { boostData, mapBoostToCardProps } from "./data/BoostData";
import Button from "@/app/core/components/Button";

export function BoosterPage() {
const { user } = useConnectedUser();

return (
<section className="grow w-full max-w-[44rem] lg:max-w-[67rem] m-auto pb-16 px-4 lg:px-8">
<PageGrid>
<div className="col-span-12 lg:col-span-8 row-start-1 row-span-1">
<TitleSection />
</div>
<VotingPowerPanel />
</PageGrid>
<div className="w-full pt-6">
<h2 className="font-bold text-xl">All boosters?</h2>
<div className="grid md:grid-cols-3 gap-4 items-start">
{BoosterList()}
</div>
</div>
</section>
);
}

function TitleSection() {
return (
<div className="flex flex-col gap-4">
<h1 className="font-bold text-3xl text-breadgray-grey100 dark:text-breadgray-ultra-white">
Voting Power Boosters
</h1>
<div className="max-w-xl text-lg text-breadgray-rye dark:text-breadgray-light-grey">
<p>
Active voter? Top baker? If this is you, the Breadchain cooperative
rewards you with voting power boosters for your engaging support of
post-capitalism. Be active and your voice will be amplified.{" "}
</p>
</div>
</div>
);
}

function BoosterList() {
return (
boostData.map((boost, index) => {
return (<BoosterCard key={index} boost={boost} {...mapBoostToCardProps(boost)} />);
})
);
}
191 changes: 191 additions & 0 deletions src/app/governance/boosters/components/BoosterCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { ReactElement } from "react";
import { CheckIcon } from "@/app/core/components/Icons/CheckIcon";
import Tooltip from "@/app/core/components/Tooltip";
import BoosterIcon, { IconName } from "@/app/governance/boosters/components/BoosterIcon";
import { Boost } from "@/app/governance/boosters/data/BoostData";
import { useModal } from "@/app/core/context/ModalContext";
import { DetailedBoosterCard } from "./DetailedBoosterCard";
import { mapBoostToDetailedCardProps } from "../data/BoostData"

export function BoosterCard({
boost,
iconName,
boosterName,
verified,
boostAmmount,
boostAmmountSubtitle,
description,
expiration,
expirationUrgent = false,
}:{
boost: Boost;
iconName: string;
boosterName: string;
verified: boolean;
boostAmmount: string;
boostAmmountSubtitle: string;
description: string;
expiration: number | undefined;
expirationUrgent: boolean;
}) {
return(
<div className="
w-full flex flex-col justify-center items-center justify-between
rounded-[15px] p-[20px]
border border-breadgray-light-grey dark:border-breadgray-burnt
bg-breadgray-ultra-white dark:bg-breadgray-grey200
text-breadgray-rye dark:text-breadgray-grey
">
{header(iconName, boosterName, verified)}
{boostPowerSection(boostAmmount, boostAmmountSubtitle)}
<p className="my-[24px]" >{description}</p>
<ViewButton verified={verified} boost={boost} />
{expiry(expiration, expirationUrgent, "Helpful information loading...")}
</div>
)
}

export function header(
iconName: string,
boosterName: string,
verified: boolean,
extraItem?: ReactElement | null
): ReactElement {
return(
<div className="w-full pb-6 flex items-center justify-between space-x-3">
<div className="flex items-center space-x-3">
{getIcon(iconName)}
<span className="
shrink-[5]
text-[20px] uppercase font-medium leading-none
text-breadgray-rye dark:text-breadgray-grey
">
{boosterName}
</span>
{extraItem && (verifiedBadge(verified))}
</div>
{extraItem ? (extraItem) : (verifiedBadge(verified))}
</div>
)
}

function verifiedBadge(verified: boolean): ReactElement {
const colorClass = verified ? "text-status-success bg-status-success/10" : "bg-[rgba(152,151,151,0.1)]"
return (
<div className={`
leading-none
py-[4px] px-[6px] rounded-full
text-[12px] font-semibold
${colorClass}
`}>
{verified ? "Verified" : "Unverified" }
</div>
)
}

export function boostPowerSection(ammount: string, subtitle: string): ReactElement {
return (
<div className="
h-[145px] w-full relative
flex flex-col justify-center items-center
rounded-lg
bg-white dark:bg-breadgray-pitchblack
p-4 text-center
">
<div className="
absolute inset-0
bg-[radial-gradient(50%_90%_at_50%_110%,rgba(232,115,211,0.3)_0%,rgba(64,38,56,0)_100%)]
">
{/* This element simply adds the gradient. It has opacity and layers ontop of the solid background */}
</div>
<p className="font-bold text-[30px] bread-pink-text-gradient z-0">{ammount}</p>
<div className="flex items-center gap-2 z-30">
<p className="block text-[16px] pb-[5px]">{subtitle}</p>
<Tooltip>Helpful information loading...</Tooltip>
</div>
</div>
)
}

function getIcon(iconName: string): ReactElement {
// To be updated once we have actual icons
const iconNames = Object.values(IconName);
const randomIconName = iconNames[Math.floor(Math.random() * iconNames.length)] as IconName
return <BoosterIcon name={randomIconName} className="flex-shrink-0 bg-breadgray-charcoal"></BoosterIcon>
}

// This is React function since it makes use of useModel, which requires special React magic
const ViewButton = (
{ verified, boost }:{verified: boolean, boost: Boost}
) => {
const { setModal } = useModal();

const openModal = () => {
setModal({
type: "GENERIC_MODAL",
showCloseButton: false,
includeContainerStyling: false,
children: (
<DetailedBoosterCard
close={() => setModal(null)}
{...mapBoostToDetailedCardProps(boost)}
/>
)
});
};

const buttonStyles = verified
? "bg-[rgba(152,151,151,0.1)] dark:bg-breadgray-charcoal text-status-success" // Verified
: "bg-[#FFCCF1] dark:bg-[#402639] text-breadviolet-violet dark:text-breadpink-shaded"; // Not verified

const verifiedButtonContent = (
<div className="flex items-center justify-center gap-2">
<div className="w-5 text-status-success"><CheckIcon /></div>
<span className="font-semibold text-xl">Verified</span>
<span className="font-normal text-base dark:text-breadpink-shaded">view</span>
</div>
);

return (boosterCardButton(
openModal,
buttonStyles,
verified ? verifiedButtonContent : "View"
));
};

export function boosterCardButton(
action: (()=>void),
styles: String,
content: String | ReactElement,
): ReactElement {
return (
<button
onClick={()=>(action())}
className={`
w-full h-[50px] mb-[10px] rounded-[10px]
flex items-center justify-center
${styles}
`}
>
{typeof content === 'string' ? (<span className="font-semibold text-[20px]">{content}</span>) : (content)}
</button>
);
}


export function expiry(
expiration: number | undefined,
expirationUrgent: boolean,
tooltipContent: string
): ReactElement | undefined {
if (!expiration) {
return undefined
}
const textColorClass = expirationUrgent ? "text-[#F2D54E]" : "text-breadgray-grey"
return (
<div className="flex flex-row items-center justify-center mb-[-6px]">
<span className={`${textColorClass} leading-none mb-[6px] mr-[6px]`}>{expiration} days until booster expires</span>
<Tooltip>{tooltipContent}</Tooltip>
</div>
)
}
42 changes: 42 additions & 0 deletions src/app/governance/boosters/components/BoosterIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { ReactElement } from "react";
import { Metamask } from "@/app/core/components/Icons/Metamask";
import { Coinbase } from "@/app/core/components/Icons/Coinbase";
import { BreadSVG } from "@/app/core/components/Icons/Bread";

// To be filled in once we have the appropriate icons
export enum IconName {
MetaMask = "metaMask",
Coinbase = "coinBase",
Network = "network"
}

interface SvgIconProps {
name: IconName;
className?: string;
}

const getIcon = (name: IconName): ReactElement => {
switch (name) {
case "metaMask":
return Metamask();
case "coinBase":
return Coinbase();
case "network":
return BreadSVG({});
default:
throw new Error("Invalid icon name");
}
};

function BoosterIcon({ name, className }: SvgIconProps) {
const wrapperClasses = `inline-flex items-center justify-center rounded-full overflow-hidden w-[45px] h-[45px] ${className || ""}`;
const IconComponent = getIcon(name);

return (
<div className={wrapperClasses}>
{IconComponent}
</div>
);
}

export default BoosterIcon;
Loading