@coinmeca/ui
Version:
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
454 lines • 25.4 kB
JSX
"use client";
// import { Vault } from "components/treasury";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Controls, Elements, Layouts } from "../../../../components";
import { Modals } from "../../../../containers";
import { useMobile, usePortal, useTheme, useWindowSize } from "../../../../hooks";
import { Root } from "../../../../lib/style";
import { format, HexToColor, parseNumber } from "../../../../lib/utils";
export default function Listing(props) {
const { windowWidth } = useWindowSize();
const { isMobile } = useMobile();
const { theme } = useTheme();
const [asset, setAsset] = useState();
const [pair, setPair] = useState([]);
const [address, setAddress] = useState("");
const [currentValue, setCurrentValue] = useState(0);
const [validate, setValidate] = useState({ state: false });
const [loading, setLoading] = useState(false);
const [process, setProcess] = useState(null);
const [fetching, setFetching] = useState(false);
const keyTokens = useMemo(() => props?.keyTokens?.filter(({ address: k }) => !pair?.find(({ address: p }) => k?.toLowerCase() === p?.toLowerCase())) || [], [props?.keyTokens, pair]);
const tokens = useMemo(() => pair?.map((p) => ({
...props?.tokens?.find(({ address }) => address?.toLowerCase() === p?.address?.toLowerCase()),
...p,
})), [props?.tokens, pair]);
const max = (pair &&
pair?.length > 0 &&
asset?.amount &&
Math.max(...[
...pair,
{
...asset,
amount: parseNumber(asset?.amount || 0) / pair?.length,
},
]?.map((p) => parseNumber(p?.amount || 0, "number")))) ||
0;
const estimate = useMemo(() => {
const target = {
...asset,
...(props?.tokens &&
props?.tokens?.length > 0 &&
props?.tokens?.find(({ address }) => address?.toLowerCase() === asset?.address?.toLowerCase())),
};
return pair?.reduce((a, b) => {
const token = tokens?.find(({ address }) => address === b?.address);
const quote = {
amount: parseNumber(token?.amount),
weight: parseNumber(token?.weight),
locked: parseNumber(token?.locked),
};
const base = {
amount: parseNumber(target?.amount),
weight: parseNumber(target?.weight),
locked: parseNumber(target?.locked),
};
return (a +
(quote?.weight > 0
? (quote?.amount * quote?.weight) / quote?.locked
: base?.weight > 0
? (base?.amount * base?.weight) / base?.locked
: 0));
}, 0);
}, [props?.tokens, tokens, asset, pair]);
const handleTokenValidate = (address) => {
if (address === asset?.address && validate?.state)
return;
let check = { state: false };
if (!!address && address !== "" && address !== "0" && address !== "0x") {
const pattern = /^[a-zA-Z0-9]+$/;
if (!address?.startsWith("0x"))
check = { state: true, message: "The typed address form of a Token Contract is Invalid." };
else if (!pattern.test(address))
check = { state: true, message: "The unacceptable charater is used in address form." };
else if (address?.length < 42)
check = { state: true, message: "The address is too short." };
else if (address?.length > 42)
check = { state: true, message: "The address is too long." };
}
setValidate(check);
if (address !== asset?.address)
setAsset(() => ({ address }));
};
const handleAddPair = (v) => {
setPair((state) => [...(state && state), { ...v, amount: 0 }]);
};
const handleRemovePair = (target) => {
setPair((state) => state?.filter(({ address }) => address !== target));
};
const maxValue = useCallback(() => {
const pairs = pair?.map((p) => {
const value = parseNumber(p?.value);
const balance = parseNumber(p?.balance?.amount);
return {
address: p?.address?.toLowerCase(),
total: balance * value,
value,
};
});
const max_value = Math.max(...pairs?.map(({ total }) => total));
const min_value = Math.min(...pairs?.map(({ total }) => total));
return min_value > 0 && min_value < max_value ? min_value : max_value;
}, [pair]);
const maxAmount = useCallback((address) => {
const target = pair?.find((p) => p?.address?.toLowerCase() === address?.toLowerCase());
const max = maxValue();
return max && !isNaN(max) ? max / parseNumber(target?.value) : undefined;
}, [pair, maxValue]);
const handleChangeAmount = (address, amount) => {
const max_amount = maxAmount(address);
const a = parseNumber(amount);
const v = parseNumber(pair?.find((p) => p?.address?.toLowerCase() === address?.toLowerCase())?.value);
const c = a * v;
if (!(max_amount && a > max_amount))
setCurrentValue(c);
setPair((state) => state?.map((s) => ({
...s,
amount: s?.address?.toLowerCase() === address?.toLowerCase()
? max_amount && a > max_amount
? max_amount
: amount
: max_amount && a > max_amount
? maxAmount(s?.address)
: c / parseNumber(s?.value),
})));
};
const validating = () => {
if (!asset?.amount)
return false;
const quantity = parseNumber(asset?.amount);
if (isNaN(quantity))
return false;
if (quantity <= 0)
return false;
let validate = true;
let pairs = pair;
pair?.map((p) => {
const amount = parseNumber(p?.amount);
if (p?.address?.toLowerCase() === asset?.address?.toLowerCase() || !p?.amount || isNaN(amount) || amount <= 0) {
validate = false;
pairs = pairs?.map((s) => s?.address?.toLowerCase() === p?.address?.toLowerCase()
? { ...s, amount: parseNumber(amount), error: true, message: "Invalid amount." }
: s);
}
});
if (!validate) {
setPair(pairs);
return false;
}
return true;
};
const handleListing = async (e) => {
if (!validating())
return;
setLoading(true);
try {
if (typeof props?.onProcess === "function")
await props?.onProcess(e);
}
catch {
setProcess(false);
}
setLoading(false);
};
const handleBack = (e) => {
if (typeof props?.onBack === "function")
props?.onBack(e);
setLoading(false);
setProcess(null);
};
const handleClose = (e) => {
if (typeof props?.onClose === "function")
props?.onClose(e);
setLoading(false);
setProcess(null);
};
useEffect(() => {
if (!!address && address !== "" && address !== "0" && address !== "0x" && address.length === 42)
(async () => {
let error;
try {
setFetching(true);
if (typeof props?.onAsset === "function")
await props?.onAsset();
}
catch (e) {
setValidate({
state: true,
message: error || "This token cannot used to be listing.",
});
}
finally {
setFetching(false);
}
})();
}, [asset?.address]);
const [handleAmountPad, closeAmountPad, resetAmountPad] = usePortal(({ title, asset, onChange }) => (<></>
// <Vault.BottomSheets.OrderPad
// placeholder={"0"}
// label={title}
// value={asset?.amount}
// unit={asset?.symbol}
// max={asset?.balance?.amount}
// button={{
// children: "OK",
// onClick: () => closeAmountPad(),
// }}
// onChange={(e: any, v: any) => typeof onChange === "function" && onChange(e, v)}
// onClose={() => closeAmountPad()}
// zIndex={200}
// />
));
return (<Modals.Process width={process === null && !loading && asset?.validate && pair?.length > 0
? windowWidth > Root.Device.Tablet
? 96
: 64
: 64} title={"Listing"} process={process} content={<Layouts.Col gap={2} fill>
<Elements.Text height={2} opacity={0.6} align={"center"}>
{asset?.validate ? "Please input amount to be list" : "Please enter the token address to be list."}
</Elements.Text>
<Layouts.Contents.InnerContent scroll>
<Layouts.Row responsive={"tablet"} fix>
<Layouts.Col gap={2} style={{
...(asset?.validate &&
pair?.length > 0 &&
windowWidth <= Root.Device.Tablet && { flex: "initial" }),
}}>
<Layouts.Col gap={1}>
<Elements.Text type={"desc"} align={"left"}>
Token Address
</Elements.Text>
<Controls.Input placeholder={"0xA1z2b3Y4C5x6d7E8..."} onChange={(e, v) => handleTokenValidate(v)} value={asset?.address} error={validate?.state} message={{
color: "red",
children: validate?.message,
}} left={fetching || asset?.validate
? {
children: (<Elements.Icon icon={asset?.validate
? "check-bold"
: validate?.state
? "x"
: "loading"} color={asset?.validate && "green"} style={{
...(!asset?.validate &&
!validate?.state && { opacity: 0.45 }),
}}/>),
}
: {
style: { maxWidth: 0, opacity: 0 },
}} right={asset?.address &&
asset?.address?.length > 0 && {
style: { pointerEvents: "initial" },
children: (<Controls.Button icon={"x"} onClick={() => {
setAsset({});
setPair([]);
}}/>),
}} autoFocus lock={fetching || asset?.validate}/>
</Layouts.Col>
{asset?.validate && asset?.balance?.amount > 0 && (<>
<Layouts.Col gap={1}>
<Layouts.Row gap={1} fix>
<Elements.Text type={"desc"} fit>
Balance:{" "}
</Elements.Text>
<Elements.Text type={"desc"} align={"right"} opacity={1} fix>
{format(asset?.balance?.amount || 0, "currency", {
unit: 9,
limit: 12,
})}
</Elements.Text>
</Layouts.Row>
<Controls.Input type={"currency"} align={"right"} placeholder={"0"} value={asset?.amount} max={asset?.balance?.amount} onChange={(e, v) => setAsset((state) => ({ ...state, amount: v }))}
// onFocus={() => isMobile &&
// handleAmountPad({
// asset,
// title: 'Quantity',
// onChange: (e: any, v: any) => setAsset((state: any) => ({ ...state, amount: v }))
// })
// }
// inputMode={isMobile ? 'none' : undefined}
left={{
children: <span>Quantity</span>,
}} right={{
width: 10,
children: (<Elements.Text style={{ justifyContent: "flex-start" }}>
{asset?.symbol || "???"}
</Elements.Text>),
}} autoFocus/>
{asset?.amount && asset?.amount !== "" && (<Layouts.Row gap={1} fix>
<Elements.Text type={"desc"} fit>
Value:{" "}
</Elements.Text>
<Elements.Text type={"desc"} align={"right"} opacity={1} fix>
${" "}
{format(asset?.value
? parseNumber(asset?.amount) * parseNumber(asset?.value)
: currentValue > 0
? currentValue * (pair?.length || 1)
: 0, "currency", {
unit: 9,
limit: 12,
})}
</Elements.Text>
</Layouts.Row>)}
</Layouts.Col>
{pair?.length > 0 && (<>
<Layouts.Divider gap={1}>
<Elements.Icon icon={"plus-small"} scale={0.75}/>
</Layouts.Divider>
{pair?.map((p, i) => (<Layouts.Col key={p?.address || i} gap={1}>
<Layouts.Row gap={1} fix>
<Elements.Text type={"desc"} fit>
Balance:{" "}
</Elements.Text>
<Elements.Text type={"desc"} align={"right"} opacity={1} fix>
{format(p?.balance?.amount || 0, "currency", {
unit: 9,
limit: 12,
})}
</Elements.Text>
</Layouts.Row>
<Controls.Input type={"currency"} align={"right"} placeholder={"0"} value={p?.amount} max={maxAmount(p?.address)} onChange={(e, v) => {
address?.toLowerCase() === p?.address?.toLowerCase() &&
handleChangeAmount(p?.address, v);
}} onFocus={() => setAddress(p?.address)} left={{
style: { paddingLeft: ".5em" },
children: <Elements.Avatar size={2.5} img={p?.logo}/>,
}} right={{
width: 10,
children: (<Layouts.Row gap={0} align={"center"} fix>
<Elements.Text align={"left"} opacity={0.6} style={{ paddingLeft: ".5em" }} fix>
{p?.symbol}
</Elements.Text>
<Controls.Button icon={"x"} onClick={() => handleRemovePair(p?.address)} fit/>
</Layouts.Row>),
}} error={p?.error} message={{
color: "red",
children: p?.message,
}} autoFocus/>
{/* {(p?.amount && p?.amount !== '') && (
<Layouts.Row gap={1} fix>
<Elements.Text type={'desc'} fit>
Value:{' '}
</Elements.Text>
<Elements.Text type={'desc'} align={'right'} opacity={1} fix>
$ {format(parseNumber(p?.amount) * parseNumber(p?.value), 'currency', {
unit: 9,
limit: 12,
})}
</Elements.Text>
</Layouts.Row>
)} */}
</Layouts.Col>))}
</>)}
{keyTokens?.length > 0 && (<>
<Layouts.Divider />
<Controls.Dropdown keyName={"symbol"} imgName={"logo"} option={{ icon: "plus-small-bold", symbol: "Select Key Token to Pair" }} options={keyTokens} onClickItem={(e, v, k) => handleAddPair(v)} theme={theme === "light" ? "dark" : "light"}/>
</>)}
</>)}
</Layouts.Col>
{asset?.validate && pair?.length > 0 && (<Layouts.Box padding={1} style={{
background: "rgba(var(--white),var(--o0045))",
height: "0",
maxHeight: "max-content",
...(windowWidth > Root.Device.Tablet && { maxWidth: "calc(50% - 4em)" }),
...(asset?.validate &&
pair?.length > 0 &&
windowWidth <= Root.Device.Tablet && { minHeight: "initial" }),
}}>
<Layouts.Col gap={1} fill>
<Layouts.Contents.InnerContent>
<Layouts.Col gap={1} fill>
<Elements.Text type={"desc"} fit>
Pairs
</Elements.Text>
<Layouts.Contents.InnerContent scroll>
<Layouts.Col gap={1} fill>
{pair?.map((p, i) => (<Controls.Card key={p?.address || i} padding={1} style={{ background: "rgba(var(--white),var(--o0045))" }}>
<Layouts.Col gap={1}>
<Layouts.Col gap={0.5}>
<Layouts.Row gap={1}>
<Elements.Text type={"desc"} fit>
{asset?.symbol}
</Elements.Text>
<Elements.Text type={"desc"} align={"right"}>
{parseNumber(asset?.amount || 0) / pair?.length}
</Elements.Text>
</Layouts.Row>
<div style={{
height: "1em",
width: `${Math.max(1, ((parseNumber(asset?.amount || 0) /
pair?.length) *
100) /
max)}%`,
backgroundColor: `#${HexToColor(asset?.address)}`,
transition: ".3s ease",
}}/>
</Layouts.Col>
<Layouts.Col gap={0.5}>
<Layouts.Row gap={1}>
<Elements.Text type={"desc"} fit>
{p?.symbol || "???"}
</Elements.Text>
<Elements.Text type={"desc"} align={"right"}>
{p?.amount || 0}
</Elements.Text>
</Layouts.Row>
<div style={{
height: "1em",
width: `${Math.max(1, parseNumber(p?.amount || 0) * 100) / max}%`,
backgroundColor: `#${HexToColor(p?.address)}`,
transition: ".3s ease",
}}/>
</Layouts.Col>
</Layouts.Col>
</Controls.Card>))}
</Layouts.Col>
</Layouts.Contents.InnerContent>
</Layouts.Col>
</Layouts.Contents.InnerContent>
<Layouts.Divider />
<Layouts.Col gap={1}>
<Elements.Text type={"desc"} align={"left"}>
Estimate earning reward:
</Elements.Text>
<Layouts.Row gap={1} align={"center"} style={{ padding: ".5em" }} fix>
<Elements.Avatar img={props?.standard?.logo} name="MECA" character={1} hideName size={2.5}/>
<Layouts.Row gap={2} fix>
<Elements.Text align={"right"} fix>
{estimate ? format(estimate, "currency") : "Cannot Estimate"}
</Elements.Text>
<Elements.Text align={"left"} opacity={0.3} fit>
MECA
</Elements.Text>
</Layouts.Row>
</Layouts.Row>
</Layouts.Col>
</Layouts.Col>
</Layouts.Box>)}
</Layouts.Row>
</Layouts.Contents.InnerContent>
<Layouts.Row gap={windowWidth <= Root.Device.Tablet ? 2 : undefined} fix>
<Controls.Button onClick={(e) => handleClose(e)}>Close</Controls.Button>
{asset?.validate && pair?.length > 0 && (<Controls.Button onClick={(e) => handleListing(e)}>Listing</Controls.Button>)}
</Layouts.Row>
</Layouts.Col>} failure={{
message: "Processing has failed.",
children: <Controls.Button onClick={(e) => handleBack(e)}>Go Back</Controls.Button>,
}} loading={{
active: loading,
message: "Please wait until the processing is complete.",
}} success={{
message: "Processing succeeded.",
children: <Controls.Button onClick={(e) => handleClose(e)}>OK</Controls.Button>,
}} onClose={(e) => handleClose(e)} close/>);
}
//# sourceMappingURL=Listing.jsx.map