Skip to content

Commit

Permalink
feat: Rewrite and improve Polkicon - @w3ux/react-polkicon (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
rossbulat authored Nov 2, 2024
1 parent 8266de6 commit b16c7e7
Show file tree
Hide file tree
Showing 7 changed files with 799 additions and 406 deletions.
4 changes: 2 additions & 2 deletions library/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"test": "vitest run",
"build": "tsup src/**/* --format esm,cjs --target es2022 --dts --no-splitting"
},
"dependencies": {
"blakejs": "^1.2.1"
"devDependencies": {
"tsup": "^8.3.5"
}
}
6 changes: 3 additions & 3 deletions library/react-polkicon/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "@w3ux/react-polkicon-source",
"license": "GPL-3.0-only",
"version": "1.5.0",
"version": "2.0.0-beta.1",
"type": "module",
"scripts": {
"clear": "rm -rf node_modules dist tsconfig.tsbuildinfo",
"build": "gulp --silent"
},
"dependencies": {
"@w3ux/utils": "^0.9.0",
"framer-motion": "^11.2.10"
"@polkadot-api/substrate-bindings": "^0.9.3",
"@w3ux/crypto": "1.0.0-beta.0"
},
"devDependencies": {
"@types/react": "^18",
Expand Down
45 changes: 45 additions & 0 deletions library/react-polkicon/src/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import { Scheme } from "./types";

// Size of the Polkicon in pixels.
export const PolkiconSize = 64;

// Center coordinate of the Polkicon.
export const PolkiconCenter = PolkiconSize / 2;

// Radius of a Polkicon circle.
export const CircleRadius = 5;

// Schemas for the Polkicon.
export const SCHEMA: Record<string, Scheme> = {
target: {
colors: [0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 1],
freq: 1,
},
cube: {
colors: [0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 5],
freq: 20,
},
quazar: {
colors: [1, 2, 3, 1, 2, 4, 5, 5, 4, 1, 2, 3, 1, 2, 4, 5, 5, 4, 0],
freq: 16,
},
flower: {
colors: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3],
freq: 32,
},
cyclic: {
colors: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6],
freq: 32,
},
vmirror: {
colors: [0, 1, 2, 3, 4, 5, 3, 4, 2, 0, 1, 6, 7, 8, 9, 7, 8, 6, 10],
freq: 128,
},
hmirror: {
colors: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 8, 6, 7, 5, 3, 4, 2, 11],
freq: 128,
},
};
276 changes: 55 additions & 221 deletions library/react-polkicon/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,253 +1,87 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import { useCallback, useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Circle,
getCircleXY,
outerCircle,
renderCircle,
Z,
getColors,
ChainName,
} from "./utils";
import { useEffect, useState } from "react";
import { generateCssTransform, getCircleCoordinates, getColors } from "./utils";
import { isValidAddress } from "@w3ux/utils";

interface PolkiconProps {
size?: number | string;
address: string;
copy?: boolean;
colors?: string[];
outerColor?: string;
copyTimeout?: number;
}

const copyPopup = {
initial: {
opacity: 0,
scale: 0.5,
},
animate: {
opacity: 1,
scale: 1,
transition: {
ease: "easeInOut",
duration: 0.1,
},
},
exit: {
opacity: 0,
},
};
import { CircleRadius, PolkiconCenter, PolkiconSize } from "./consts";
import { Circle, Coordinate, PolkiconProps } from "./types";

export const Polkicon = ({
size = "2rem",
address,
copy = false,
colors: initialColors,
outerColor,
copyTimeout = 500,
background,
inactive,
transform: propTransform,
}: PolkiconProps) => {
// The colors of the Polkicon and inner circles.
const [colors, setColors] = useState<string[]>([]);
const [xy, setXy] = useState<[number, number][] | undefined>();
const [copySuccess, setCopySuccess] = useState(false);
const [message, setMessage] = useState<string>("Copied!");

const [s, setS] = useState<string | number>();
const [f, setF] = useState<string>();
const [p, setP] = useState<string>();

useEffect(() => {
const InfoText = (type: string, value: string | number) =>
console.warn(
`Polkicon: 'Size' expressed in '${type}' cannot be less than ${value}. Will be resized to minimum size.`
);

if (
typeof size === "string" &&
!size.includes("px") &&
!size.includes("rem")
) {
throw new Error(
"Providing a string for 'size' in Polkicon should be expressed either in 'px', 'rem' or 'em'"
);
}
// The coordinates of the Polkicon circles.
const [coords, setCoords] = useState<Coordinate[]>();

let sizeNumb: number;
let fontType: string;
if (typeof size === "string") {
fontType = size.replace(/[0-9.]/g, "");
switch (fontType) {
case "px":
sizeNumb = parseFloat(size);
break;
case "rem":
sizeNumb = parseFloat(size) * 10;
break;
}
} else if (typeof size === "number") {
sizeNumb = size;
}
// Renders the outer circle of the Polkicon.
const renderOuterCircle = (fill: string): Circle => ({
cx: PolkiconCenter,
cy: PolkiconCenter,
fill,
r: PolkiconCenter,
});

setS(
fontType
? `${fontType === "px" ? sizeNumb + "px" : sizeNumb / 10 + "rem"}`
: sizeNumb
);
if (sizeNumb < 12) {
InfoText(
fontType || "number",
fontType === "px" ? "12px" : fontType === "rem" ? "1.2rem" : 12
);
}
// Renders a circle element of the Polkicon.
const renderCircle = ({ cx, cy, fill, r }: Circle, key: number) => (
<circle cx={cx} cy={cy} fill={fill} key={key} r={r} />
);

if (sizeNumb < 32) {
setP("0rem 0.5rem");
setF("0.5rem");
} else if (sizeNumb >= 32 && sizeNumb < 64) {
setP("1rem 0.5rem");
setF("1rem");
} else if (sizeNumb >= 64 && sizeNumb < 100) {
setP("2rem 1rem");
setF("1.5rem");
} else if (sizeNumb >= 100) {
setP("3rem 1rem");
setF("2rem");
}
}, [size]);
const transform = propTransform
? generateCssTransform(propTransform)
: undefined;

// Generate Polkicon coordinates and colors based on the address validity and inactivity status.
// Re-renders on `address` change.
useEffect(() => {
// TODO: look closer into this approach
let ch = "generic";
// Polkadot
if (address) {
if (address.startsWith("1")) {
ch = "polkadot";
} else if (
address.startsWith("E") ||
address.startsWith("D") ||
address.startsWith("G")
) {
ch = "kusama";
} else if (address.startsWith("5")) {
ch = "westend";
}
} else {
ch = "polkadot";
}
const circleXy = getCircleXY(ch as ChainName);
if (initialColors && initialColors?.length < circleXy.length) {
let initColIdx = 0;
for (let i = 0; i < circleXy.length; i++) {
if (!initialColors[i]) {
initialColors[i] = initialColors[initColIdx++];
}
if (initColIdx == initialColors.length) {
initColIdx = 0;
}
}
}
const defaultColors = new Array<string>(circleXy.length).fill("#ddd");
const deactiveColors = new Array<string>(circleXy.length).fill(
"var(--background-invert)"
);
// Generate Polkicon coordinates.
const circleXy = getCircleCoordinates();
// Get the amount of Polkicon circles.
const length = circleXy.length;
// Generate the colors of the Polkicon.
const colors =
isValidAddress(address) && !inactive
? getColors(address)
: Array.from({ length }, () => "var(--background-invert)");

setXy(circleXy);
setColors(
isValidAddress(address)
? initialColors || getColors(address) || defaultColors
: deactiveColors
);
setCoords(circleXy);
setColors(colors);
}, [address]);

const handleClick = useCallback(() => {
const copyText = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopySuccess(true);
setMessage("Copied!");
} catch (err) {
setCopySuccess(true);
setMessage("Failed!");
}
};
copy && copyText(address);
}, [copy, address]);

useEffect(() => {
if (copy && copySuccess) {
setTimeout(() => {
setCopySuccess(false);
}, copyTimeout);
}
}, [copy, copySuccess]);

return (
xy && (
coords && (
<div
onClick={copy ? handleClick : undefined}
style={
copy
? {
cursor: copySuccess ? "none" : "copy",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
}
: {
display: "flex",
justifyContent: "center",
alignItems: "center",
}
}
style={{
display: "inline-block",
height: "1em",
width: "auto",
verticalAlign: "-0.125em",
transform,
}}
>
<svg
viewBox="0 0 64 64"
viewBox={`0 0 ${PolkiconSize} ${PolkiconSize}`}
id={address}
name={address}
width={s}
height={s}
width="100%"
height="100%"
>
{[
outerColor
? outerCircle(outerColor)
: outerCircle("var(--background-default"),
]
{[renderOuterCircle(background || "var(--background-default)")]
.concat(
xy.map(
([cx, cy], index): Circle => ({
cx,
cy,
fill: colors[index],
r: Z,
})
)
coords.map(([cx, cy], index) => ({
cx,
cy,
fill: colors[index],
r: CircleRadius,
}))
)
.map(renderCircle)}
</svg>
{copy && (
<AnimatePresence>
{copySuccess && (
<motion.div
variants={copyPopup}
initial={"initial"}
animate={"animate"}
exit={"exit"}
style={{
position: "absolute",
padding: p,
borderRadius: "100%",
backgroundColor: "var(--background-default)",
fontWeight: "bold",
}}
>
<p style={{ fontSize: f, fontWeight: "bold" }}>{message}</p>
</motion.div>
)}
</AnimatePresence>
)}
</div>
)
);
Expand Down
Loading

0 comments on commit b16c7e7

Please sign in to comment.