Skip to content

Commit

Permalink
feat(app): enabling dynamically defined dashboard customizations (#2182)
Browse files Browse the repository at this point in the history
* feat(app): enabling dynamically defined dashboard customizations

* feat: added finalized avax links
  • Loading branch information
lucianHymer authored Feb 22, 2024
1 parent 902e3a4 commit 989ddef
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 227 deletions.
61 changes: 61 additions & 0 deletions app/__tests__/hooks/useDashboardCustomization.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, waitFor, screen } from "@testing-library/react";

import { useDashboardCustomization } from "../../hooks/useDashboardCustomization";
import { DynamicCustomDashboardPanel } from "../../components/CustomDashboardPanel";

// Once we're using the API, we should mock the axios
// call (or however we do it) and return the data that
// is currently hardcoded in useDatastoreConnectionContext.tsx
// for testing

const TestingComponent = ({ customizationKey }: { customizationKey: string }) => {
const { customizationEnabled, customizationConfig } = useDashboardCustomization(customizationKey);
const { useCustomDashboardPanel, customizationTheme } = customizationConfig;

return (
<div>
<div data-testid="customizationEnabled">{customizationEnabled ? "Enabled" : "Disabled"}</div>
<div data-testid="useCustomDashboardPanel">{useCustomDashboardPanel ? "true" : "false"}</div>
<div data-testid="customizationThemeBackgroundColor1">{customizationTheme?.colors?.customizationBackground1}</div>
{useCustomDashboardPanel && <DynamicCustomDashboardPanel customizationKey={customizationKey} className="" />}
</div>
);
};

describe("useDashboardCustomization", () => {
it("should render panel for API-defined customization", async () => {
render(<TestingComponent customizationKey="avalanche" />);
await waitFor(() => {
expect(document.querySelector("[data-testid=customizationEnabled]")?.textContent).toBe("Enabled");
expect(document.querySelector("[data-testid=useCustomDashboardPanel]")?.textContent).toBe("true");
expect(document.querySelector("[data-testid=customizationThemeBackgroundColor1]")?.textContent).toBe("232 65 66");
expect(document.querySelector("html")).toHaveStyle("--color-customization-background-1: 232 65 66");

expect(
screen.getByText((str) => str.includes("The Avalanche Community Grant Rounds require"))
).toBeInTheDocument();
});
});

it("should render panel for hardcoded customization", async () => {
render(<TestingComponent customizationKey="testing" />);
await waitFor(() => {
expect(document.querySelector("[data-testid=customizationEnabled]")?.textContent).toBe("Enabled");
expect(document.querySelector("[data-testid=useCustomDashboardPanel]")?.textContent).toBe("true");
expect(document.querySelector("[data-testid=customizationThemeBackgroundColor1]")?.textContent).toBe(
"var(--color-focus)"
);
expect(document.querySelector("html")).toHaveStyle("--color-customization-background-1: var(--color-focus)");

expect(screen.getByText((str) => str.includes("Click below to enable test mode"))).toBeInTheDocument();
});
});

it("should cleanly ignore invalid customizationKey", async () => {
render(<TestingComponent customizationKey="invalid" />);
await waitFor(() => {
expect(document.querySelector("[data-testid=customizationEnabled]")?.textContent).toBe("Disabled");
expect(document.querySelector("[data-testid=useCustomDashboardPanel]")?.textContent).toBe("false");
});
});
});
103 changes: 103 additions & 0 deletions app/components/CustomDashboardPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useMemo } from "react";
import { VeraxPanel } from "../components/VeraxPanel";
import { TestingPanel } from "../components/TestingPanel";
import { Button } from "../components/Button";
import { CustomizationLogoBackground, isDynamicCustomization } from "../utils/customizationUtils";
import { useDashboardCustomization } from "../hooks/useDashboardCustomization";

type CustomDashboardPanelProps = {
logo: {
image: React.ReactNode;
caption?: React.ReactNode;
background?: CustomizationLogoBackground;
};
children: React.ReactNode;
className: string;
};

const DotsBackground = ({ viewBox, className }: { viewBox: string; className: string }) => (
<svg viewBox={viewBox} fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="10" cy="10" r="1.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="40" cy="8" r="0.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="20" cy="20" r="0.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="08" cy="30" r="0.8" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="46" cy="28" r="0.8" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="44" cy="64" r="1.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="02" cy="68" r="0.8" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="18" cy="80" r="1.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="35" cy="84" r="0.5" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="08" cy="92" r="0.8" fill="#D9D9D9" fillOpacity="0.5" />
<circle cx="46" cy="96" r="0.8" fill="#D9D9D9" fillOpacity="0.5" />
</svg>
);

// Used as base for both API-defined and static panels
export const CustomDashboardPanel = ({ logo, className, children }: CustomDashboardPanelProps) => {
const logoBackground = useMemo(() => {
if (logo.background === "dots") {
return (
<>
<DotsBackground viewBox="0 0 50 100" className="block md:hidden lg:block" />
{/* 1:1 aspect ratio works better on medium screen */}
<DotsBackground viewBox="0 15 50 65" className="hidden md:block lg:hidden" />
</>
);
}
}, [logo.background]);

return (
<div className={`${className} flex rounded border border-customization-background-1`}>
<div className="grid shrink border-r border-customization-background-1 bg-gradient-to-b from-transparent to-customization-background-1/[.4]">
<div className="flex col-start-1 row-start-1 flex-col items-center justify-center p-6 z-10">
{logo.image}
{logo.caption && <span className="mt-1 text-3xl leading-none">{logo.caption}</span>}
</div>
{logoBackground && <div className="col-start-1 flex row-start-1 z-0">{logoBackground}</div>}
</div>
<div className="relative flex flex-col justify-start gap-2 bg-gradient-to-b from-transparent to-customization-background-2/[.26] p-6 w-full">
{children}
</div>
</div>
);
};

export const DynamicCustomDashboardPanel = ({
className,
customizationKey,
}: {
className: string;
customizationKey?: string;
}) => {
const { customizationConfig } = useDashboardCustomization(customizationKey);

// First, check to see if the customization key is one of the built-in ones
switch (customizationKey) {
case "testing":
return <TestingPanel className={className} />;
case "verax":
return <VeraxPanel className={className} />;
default:
// If there is no customization key, return an empty div
if (!customizationKey || !isDynamicCustomization(customizationConfig)) {
return <div></div>;
}
}

// Otherwise, it's a dynamically defined panel

const { logo, body } = customizationConfig.dashboardPanel;

return (
<CustomDashboardPanel className={className} logo={logo}>
<div>{body.mainText}</div>
<div className="text-sm grow">{body.subText}</div>
<Button
variant="custom"
className={`rounded-s mr-2 mt-2 w-fit self-end bg-customization-background-1 text-customization-foreground-1 hover:bg-customization-background-1/75 enabled:hover:text-color-1 disabled:bg-customization-background-1 disabled:brightness-100`}
onClick={() => window.open(body.action.url, "_blank")}
>
{body.action.text}
</Button>
</CustomDashboardPanel>
);
};
27 changes: 16 additions & 11 deletions app/components/SIWEButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import React from "react";
import { Button, ButtonProps } from "./Button";

const SIWEButton = (props: ButtonProps) => {
const SIWEButton = (props: ButtonProps & { enableEthBranding: boolean }) => {
const { enableEthBranding, ...rest } = props;
return (
<Button {...props} className={(props.className || "") + " rounded-sm"}>
<svg className="my-1" width="19" height="30" viewBox="0 0 19 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.22009 22.4887V30.0001L18.4402 17.0493L9.22009 22.4887Z" fill="#2F3030" />
<path d="M9.22009 11.1099V20.751L18.4402 15.2972L9.22009 11.1099Z" fill="black" />
<path d="M9.22009 0V11.1098L18.4402 15.2971L9.22009 0Z" fill="#2F3030" />
<path d="M9.22009 22.4887V30.0001L0 17.0493L9.22009 22.4887Z" fill="#828384" />
<path d="M9.22009 11.1099V20.751L0 15.2972L9.22009 11.1099Z" fill="#343535" />
<path d="M9.22009 0V11.1098L0 15.2971L9.22009 0Z" fill="#828384" />
</svg>
<Button {...rest} className={(props.className || "") + " rounded-sm"}>
{enableEthBranding && (
<svg className="my-1" width="19" height="30" viewBox="0 0 19 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.22009 22.4887V30.0001L18.4402 17.0493L9.22009 22.4887Z" fill="#2F3030" />
<path d="M9.22009 11.1099V20.751L18.4402 15.2972L9.22009 11.1099Z" fill="black" />
<path d="M9.22009 0V11.1098L18.4402 15.2971L9.22009 0Z" fill="#2F3030" />
<path d="M9.22009 22.4887V30.0001L0 17.0493L9.22009 22.4887Z" fill="#828384" />
<path d="M9.22009 11.1099V20.751L0 15.2972L9.22009 11.1099Z" fill="#343535" />
<path d="M9.22009 0V11.1098L0 15.2971L9.22009 0Z" fill="#828384" />
</svg>
)}
<span className="hidden group-disabled:inline">Loading...</span>
<span className="inline group-disabled:hidden">Sign in with Ethereum</span>
<span className="inline group-disabled:hidden">
Sign in {enableEthBranding ? "with Ethereum" : "using signature"}
</span>
</Button>
);
};
Expand Down
16 changes: 8 additions & 8 deletions app/components/TestingPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ const TestingLogo = () => (
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
className="h-16 w-16"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z"
/>
</svg>
Expand All @@ -21,15 +21,15 @@ const TestingLogo = () => (
export const TestingPanel = ({ className }: { className: string }) => {
return (
<div
className={`${className} flex rounded border border-focus ${
TEST_MODE ? "" : "shadow-[0_0_15px_rgb(var(--color-focus)/.75)]"
className={`${className} flex rounded border border-customization-background-1 ${
TEST_MODE ? "" : "shadow-[0_0_15px_rgb(var(--color-customization-background-1)/.75)]"
}`}
>
<div className="flex shrink flex-col items-center justify-center border-r border-focus bg-gradient-to-b from-transparent to-focus/[.4] p-6">
<div className="flex shrink flex-col items-center justify-center border-r border-customization-background-1 bg-gradient-to-b from-transparent to-customization-background-1/[.4] p-6">
<TestingLogo />
<span className="mt-1 text-3xl leading-none">Testing</span>
</div>
<div className="relative flex w-full flex-col justify-center gap-2 bg-gradient-to-b from-transparent to-focus/[.26] p-6">
<div className="relative flex w-full flex-col justify-center gap-2 bg-gradient-to-b from-transparent to-customization-background-1/[.26] p-6">
<div className="text-center text-xl">
Test Mode:{" "}
<span className={`font-extrabold ${TEST_MODE ? "text-foreground-5" : "text-background-3"}`}>
Expand All @@ -49,7 +49,7 @@ export const TestingPanel = ({ className }: { className: string }) => {
<div className="grow" />
<Button
variant="custom"
className={`rounded-s mr-2 mt-2 w-fit self-end bg-focus hover:bg-focus/75 enabled:hover:text-color-1 disabled:bg-focus disabled:brightness-100`}
className={`rounded-s mr-2 mt-2 w-fit self-end bg-customization-background-1 hover:bg-customization-background-1/75 hover:text-color-1`}
onClick={toggleTestMode}
>
{TEST_MODE ? "Disable" : "Enable"} Test Mode
Expand Down
110 changes: 57 additions & 53 deletions app/components/VeraxPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve
import { Button } from "./Button";
import { ScorerContext } from "../context/scorerContext";
import { useContext, useState } from "react";
import { CustomDashboardPanel } from "./CustomDashboardPanel";

const VeraxLogo = () => (
<svg width="64" height="56" viewBox="0 0 64 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.1082 0H0L15.8409 37.0131L24.395 17.7418L17.1082 0Z" fill="rgb(var(--color-foreground-7))" />
<path d="M46.5404 0H63.0907L40.7172 55.5197H23.5539L46.5404 0Z" fill="rgb(var(--color-foreground-7))" />
<path
d="M17.1082 0H0L15.8409 37.0131L24.395 17.7418L17.1082 0Z"
fill="rgb(var(--color-customization-background-1))"
/>
<path
d="M46.5404 0H63.0907L40.7172 55.5197H23.5539L46.5404 0Z"
fill="rgb(var(--color-customization-background-1))"
/>
</svg>
);

Expand Down Expand Up @@ -50,62 +57,59 @@ export const VeraxPanel = ({ className }: { className: string }) => {

return (
<>
<div
className={`${className} flex rounded border border-foreground-7 ${
onChainStatus === OnChainStatus.MOVED_UP_TO_DATE ? "text-foreground-7 brightness-50 saturate-50" : ""
<CustomDashboardPanel
className={`${className} ${
onChainStatus === OnChainStatus.MOVED_UP_TO_DATE
? "text-customization-background-1 brightness-50 saturate-50"
: ""
} ${
onChainStatus === OnChainStatus.MOVED_OUT_OF_DATE
? "shadow-[0_0_15px_rgb(var(--color-foreground-7)/.75)]"
? "shadow-[0_0_15px_rgb(var(--color-customization-background-1)/.75)]"
: ""
}`}
logo={{ image: <VeraxLogo />, caption: "Verax" }}
>
<div className="flex shrink flex-col items-center justify-center border-r border-foreground-7 bg-gradient-to-b from-transparent to-foreground-7/[.4] p-6">
<VeraxLogo />
<span className="mt-1 text-3xl leading-none">Verax</span>
</div>
<div className="relative flex flex-col justify-start gap-2 bg-gradient-to-b from-transparent to-foreground-7/[.26] p-6">
<Tooltip
className={`absolute top-0 right-0 p-2 ${
needToSwitchChain && onChainStatus !== OnChainStatus.MOVED_UP_TO_DATE ? "block" : "hidden"
}`}
panelClassName="w-[200px] border-foreground-7"
iconClassName="text-foreground-7"
>
You will be prompted to switch to {chain?.label} and sign the transaction.
</Tooltip>
{onChainStatus === OnChainStatus.NOT_MOVED || onChainStatus === OnChainStatus.MOVED_OUT_OF_DATE ? (
<>
Verax is a community maintained public attestation registry on Linea. Push your Passport Stamps onto Verax
to gain rewards for early adopters in the Linea ecosystem.
<span className="text-xs text-foreground-7 brightness-[1.4]">
This action requires ETH bridged to Linea Mainnet to cover network fees, as well as a $2 mint fee which
goes to the Gitcoin treasury.
</span>
</>
) : (
<p>
Verax is a community maintained public attestation registry on Linea. Push your Passport Stamps onto Verax
to gain rewards for early adopters in the Linea ecosystem.
</p>
)}
<div className="grow" />
<LoadButton
{...buttonProps}
isLoading={syncingToChain || onChainStatus === OnChainStatus.LOADING}
variant="custom"
className={`${buttonProps.className} rounded-s mr-2 mt-2 w-fit self-end bg-foreground-7 text-color-4 hover:bg-foreground-7/75 enabled:hover:text-color-1 disabled:bg-foreground-7 disabled:brightness-100`}
onClick={() => {
if (score < 1) {
setConfirmModalOpen(true);
} else {
buttonProps.onClick();
}
}}
>
{text}
</LoadButton>
</div>
</div>
<Tooltip
className={`absolute top-0 right-0 p-2 ${
needToSwitchChain && onChainStatus !== OnChainStatus.MOVED_UP_TO_DATE ? "block" : "hidden"
}`}
panelClassName="w-[200px] border-customization-background-1"
iconClassName="text-customization-background-1"
>
You will be prompted to switch to {chain?.label} and sign the transaction.
</Tooltip>
{onChainStatus === OnChainStatus.NOT_MOVED || onChainStatus === OnChainStatus.MOVED_OUT_OF_DATE ? (
<>
Verax is a community maintained public attestation registry on Linea. Push your Passport Stamps onto Verax
to gain rewards for early adopters in the Linea ecosystem.
<span className="text-xs text-customization-background-1 brightness-[1.4]">
This action requires ETH bridged to Linea Mainnet to cover network fees, as well as a $2 mint fee which
goes to the Gitcoin treasury.
</span>
</>
) : (
<p>
Verax is a community maintained public attestation registry on Linea. Push your Passport Stamps onto Verax
to gain rewards for early adopters in the Linea ecosystem.
</p>
)}
<div className="grow" />
<LoadButton
{...buttonProps}
isLoading={syncingToChain || onChainStatus === OnChainStatus.LOADING}
variant="custom"
className={`${buttonProps.className} rounded-s mr-2 mt-2 w-fit self-end bg-customization-background-1 text-color-4 hover:bg-customization-background-1/75 enabled:hover:text-color-1 disabled:bg-customization-background-1 disabled:brightness-100`}
onClick={() => {
if (score < 1) {
setConfirmModalOpen(true);
} else {
buttonProps.onClick();
}
}}
>
{text}
</LoadButton>
</CustomDashboardPanel>
<VeraxPanelApprovalModal
isOpen={confirmModalOpen}
onReject={() => setConfirmModalOpen(false)}
Expand Down
Loading

0 comments on commit 989ddef

Please sign in to comment.