diff --git a/client/src/components/Button/Button.tsx b/client/src/components/Button/Button.tsx index 5ad5d9b7..b8c9baea 100644 --- a/client/src/components/Button/Button.tsx +++ b/client/src/components/Button/Button.tsx @@ -16,13 +16,15 @@ interface ButtonProps { kind?: ButtonKind; size?: ButtonSize; fullWidth?: boolean; + bold?: boolean; } const getButtonStyles = ( theme: DefaultTheme, kind?: ButtonKind, size?: ButtonSize, - fullWidth?: boolean + fullWidth?: boolean, + bold?: boolean ) => { let color = "inherit"; let bgColor = "transparent"; @@ -131,6 +133,12 @@ const getButtonStyles = ( width: 100%; `); + // Bold + if (bold) + defaultCss = defaultCss.concat(css` + font-weight: bold; + `); + if (kind === "icon") defaultCss = defaultCss.concat(css` display: flex; @@ -153,8 +161,8 @@ const getButtonStyles = ( }; const Button = styled.button` - ${({ theme, kind, size, fullWidth }) => - getButtonStyles(theme, kind, size, fullWidth)} + ${({ theme, kind, size, fullWidth, bold }) => + getButtonStyles(theme, kind, size, fullWidth, bold)} `; export default Button; diff --git a/client/src/components/DownloadButton/DownloadButton.tsx b/client/src/components/DownloadButton/DownloadButton.tsx index 574f44de..e5234660 100644 --- a/client/src/components/DownloadButton/DownloadButton.tsx +++ b/client/src/components/DownloadButton/DownloadButton.tsx @@ -5,16 +5,22 @@ interface DownloadButtonProps { href: string; download: string; buttonKind?: ButtonKind; + noButton?: boolean; } const DownloadButton: FC = ({ href, download, buttonKind = "outline", + noButton = false, children, }) => ( - + {noButton ? ( +
{children}
+ ) : ( + + )}
); diff --git a/client/src/components/IconButton/IconButton.tsx b/client/src/components/IconButton/IconButton.tsx index 40e8a977..0883f57c 100644 --- a/client/src/components/IconButton/IconButton.tsx +++ b/client/src/components/IconButton/IconButton.tsx @@ -39,7 +39,7 @@ const IconWrapper = styled.div` align-items: center; &.active { - background-color: ${({ theme }) => theme.colors?.iconButton?.selectedBg!}; + background-color: ${({ theme }) => theme.colors?.iconButton?.selectedBg}; border-left: 2px solid ${({ theme }) => theme.colors?.iconButton?.selectedBorderColor ?? diff --git a/client/src/components/Icons/Icons.tsx b/client/src/components/Icons/Icons.tsx index fb2df8eb..e5cc808c 100644 --- a/client/src/components/Icons/Icons.tsx +++ b/client/src/components/Icons/Icons.tsx @@ -106,3 +106,48 @@ export const Copy = ({ fullSize }: IconProps) => { ); }; + +export const Sad = ({ fullSize }: IconProps) => { + return ( + + + + + + + ); +}; + +export const Refresh = ({ fullSize }: IconProps) => { + return ( + + + + + ); +}; + +export const Error = ({ fullSize }: IconProps) => { + return ( + + + + + ); +}; + +export const Clock = ({ fullSize }: IconProps) => { + return ( + + + + ); +}; + +export const ThreeDots = ({ fullSize }: IconProps) => { + return ( + + + + ); +}; diff --git a/client/src/components/Panels/Bottom/Bottom.tsx b/client/src/components/Panels/Bottom/Bottom.tsx index d987d474..8d5eabec 100644 --- a/client/src/components/Panels/Bottom/Bottom.tsx +++ b/client/src/components/Panels/Bottom/Bottom.tsx @@ -3,12 +3,12 @@ import { useConnection } from "@solana/wallet-adapter-react"; import styled, { css } from "styled-components"; import Button from "../../Button"; -import { ConnState } from "../Side/Right/Wallet/connection-states"; +import { ConnState } from "../Wallet/connection-states"; import Link from "../../Link"; -import useCurrentWallet from "../Side/Right/Wallet/useCurrentWallet"; -import useConnect from "../Side/Right/Wallet/useConnect"; +import useCurrentWallet from "../Wallet/useCurrentWallet"; +import useConnect from "../Wallet/useConnect"; import { EXPLORER_URL, Id, NETWORKS } from "../../../constants"; -import useAirdropAmount from "../Side/Right/Wallet/useAirdropAmount"; +import useAirdropAmount from "../Wallet/useAirdropAmount"; import { PgCommon } from "../../../utils/pg/common"; const Bottom = () => { diff --git a/client/src/components/Panels/Main/Editor/Editor.tsx b/client/src/components/Panels/Main/Editor/Editor.tsx index 6942080a..eb73d416 100644 --- a/client/src/components/Panels/Main/Editor/Editor.tsx +++ b/client/src/components/Panels/Main/Editor/Editor.tsx @@ -281,6 +281,8 @@ const Editor = () => { return ; }; +export const EDITOR_SCROLLBAR_WIDTH = "0.75rem"; + const Wrapper = styled.div` ${({ theme }) => css` flex: 1; @@ -291,7 +293,7 @@ const Wrapper = styled.div` /* Scrollbar */ /* Chromium */ & ::-webkit-scrollbar { - width: 0.75rem; + width: ${EDITOR_SCROLLBAR_WIDTH}; } & ::-webkit-scrollbar-track { diff --git a/client/src/components/Panels/Main/Editor/index.ts b/client/src/components/Panels/Main/Editor/index.ts index 100d0292..7b59755c 100644 --- a/client/src/components/Panels/Main/Editor/index.ts +++ b/client/src/components/Panels/Main/Editor/index.ts @@ -1 +1,2 @@ export { default } from "./Editor"; +export * from "./Editor"; diff --git a/client/src/components/Panels/Main/Tabs/Tabs.tsx b/client/src/components/Panels/Main/Tabs/Tabs.tsx index dcbe4b18..fac92f09 100644 --- a/client/src/components/Panels/Main/Tabs/Tabs.tsx +++ b/client/src/components/Panels/Main/Tabs/Tabs.tsx @@ -1,12 +1,16 @@ import { useAtom } from "jotai"; +import { useCallback } from "react"; import styled, { css } from "styled-components"; -import { explorerAtom, refreshExplorerAtom } from "../../../../state"; +import { + explorerAtom, + refreshExplorerAtom, + showWalletAtom, +} from "../../../../state"; +import Button from "../../../Button"; +import useCurrentWallet from "../../Wallet/useCurrentWallet"; import Tab from "./Tab"; -// Same height with Side-Right Title -export const TAB_HEIGHT = "2rem"; - const Tabs = () => { const [explorer] = useAtom(explorerAtom); useAtom(refreshExplorerAtom); @@ -16,18 +20,44 @@ const Tabs = () => { return ( - {tabs?.map((t, i) => ( - - ))} + + {tabs?.map((t, i) => ( + + ))} + + ); }; +const Wallet = () => { + const [, setShowWallet] = useAtom(showWalletAtom); + + const { walletPkStr } = useCurrentWallet(); + + const toggleWallet = useCallback(() => { + setShowWallet((s) => !s); + }, [setShowWallet]); + + if (!walletPkStr) return null; + + return ( + + + + ); +}; + +// Same height with Side-Right Title +export const TAB_HEIGHT = "2rem"; + const Wrapper = styled.div` ${({ theme }) => css` display: flex; - overflow-x: auto; - overflow-y: hidden; + justify-content: space-between; min-height: ${TAB_HEIGHT}; user-select: none; background-color: ${theme.colors.right?.bg}; @@ -36,4 +66,35 @@ const Wrapper = styled.div` `} `; +const TabsWrapper = styled.div` + display: flex; + width: 100%; + overflow-x: auto; + overflow-y: hidden; +`; + +const WalletWrapper = styled.div` + ${({ theme }) => css` + display: flex; + align-items: center; + + & > button { + background-color: ${theme.colors.default.bg}; + border-top-left-radius: ${theme.borderRadius}; + border-bottom-left-radius: ${theme.borderRadius}; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + & img { + filter: invert(0.5); + margin-right: 0.375rem; + } + + &:hover img { + filter: invert(1); + } + } + `} +`; + export default Tabs; diff --git a/client/src/components/Panels/Main/Tabs/index.ts b/client/src/components/Panels/Main/Tabs/index.ts index 9a31da7f..2bda533d 100644 --- a/client/src/components/Panels/Main/Tabs/index.ts +++ b/client/src/components/Panels/Main/Tabs/index.ts @@ -1 +1,2 @@ export { default } from "./Tabs"; +export * from "./Tabs"; diff --git a/client/src/components/Panels/Panels.tsx b/client/src/components/Panels/Panels.tsx index d73f5803..848772f6 100644 --- a/client/src/components/Panels/Panels.tsx +++ b/client/src/components/Panels/Panels.tsx @@ -8,6 +8,7 @@ import Toast from "../Toast"; const Main = lazy(() => import("./Main")); const Bottom = lazy(() => import("./Bottom")); +const Wallet = lazy(() => import("./Wallet")); const Panels = () => ( @@ -17,6 +18,7 @@ const Panels = () => ( }>
+ diff --git a/client/src/components/Panels/Side/Left/Left.tsx b/client/src/components/Panels/Side/Left/Left.tsx index bfd9ba7b..1b4ae749 100644 --- a/client/src/components/Panels/Side/Left/Left.tsx +++ b/client/src/components/Panels/Side/Left/Left.tsx @@ -6,7 +6,6 @@ import IconButton from "../../../IconButton"; import Link from "../../../Link"; import PopButton from "../../../PopButton"; import Settings from "../Right/Settings"; -import Wallet from "../Right/Wallet"; import { Sidebar } from "../sidebar-values"; import { sidebarData } from "./sidebar-data"; import useActiveTab from "./useActiveTab"; @@ -53,7 +52,7 @@ const Left: FC = ({ return ( ); diff --git a/client/src/components/Panels/Side/Left/sidebar-data.ts b/client/src/components/Panels/Side/Left/sidebar-data.ts index 1e57c135..44b43cb4 100644 --- a/client/src/components/Panels/Side/Left/sidebar-data.ts +++ b/client/src/components/Panels/Side/Left/sidebar-data.ts @@ -31,11 +31,6 @@ export const sidebarData = { src: rootDir + "github.png", value: Sidebar.GITHUB, }, - { - title: Sidebar.WALLET, - src: rootDir + "wallet.png", - value: Sidebar.WALLET, - }, { title: Sidebar.SETTINGS, src: rootDir + "settings.webp", diff --git a/client/src/components/Panels/Side/Right/BuildDeploy/Deploy.tsx b/client/src/components/Panels/Side/Right/BuildDeploy/Deploy.tsx index 14bb4eab..d259f9f2 100644 --- a/client/src/components/Panels/Side/Right/BuildDeploy/Deploy.tsx +++ b/client/src/components/Panels/Side/Right/BuildDeploy/Deploy.tsx @@ -17,7 +17,7 @@ import { programAtom, } from "../../../../../state"; import useIsDeployed from "./useIsDeployed"; -import useConnect from "../Wallet/useConnect"; +import useConnect from "../../../Wallet/useConnect"; import Loading from "../../../../Loading"; import useInitialLoading from "../../useInitialLoading"; import { ConnectionErrorText } from "../../Common"; diff --git a/client/src/components/Panels/Side/Right/BuildDeploy/Extras/IDL.tsx b/client/src/components/Panels/Side/Right/BuildDeploy/Extras/IDL.tsx index 969bffc8..22a29012 100644 --- a/client/src/components/Panels/Side/Right/BuildDeploy/Extras/IDL.tsx +++ b/client/src/components/Panels/Side/Right/BuildDeploy/Extras/IDL.tsx @@ -1,8 +1,8 @@ -import { useAtom } from "jotai"; import { ChangeEvent } from "react"; +import { useAtom } from "jotai"; import styled from "styled-components"; -import { buildCountAtom } from "../../../../../../state"; +import { buildCountAtom } from "../../../../../../state"; import { PgCommon } from "../../../../../../utils/pg/common"; import { PgProgramInfo } from "../../../../../../utils/pg/program-info"; import DownloadButton from "../../../../../DownloadButton"; @@ -51,10 +51,7 @@ const Export = () => { return ( Export diff --git a/client/src/components/Panels/Side/Right/BuildDeploy/Extras/ProgramCredentials.tsx b/client/src/components/Panels/Side/Right/BuildDeploy/Extras/ProgramCredentials.tsx index df543665..82ea45a9 100644 --- a/client/src/components/Panels/Side/Right/BuildDeploy/Extras/ProgramCredentials.tsx +++ b/client/src/components/Panels/Side/Right/BuildDeploy/Extras/ProgramCredentials.tsx @@ -70,10 +70,7 @@ const Export = () => { return ( Export diff --git a/client/src/components/Panels/Side/Right/Right.tsx b/client/src/components/Panels/Side/Right/Right.tsx index 7a6e3008..9d242b0b 100644 --- a/client/src/components/Panels/Side/Right/Right.tsx +++ b/client/src/components/Panels/Side/Right/Right.tsx @@ -6,7 +6,7 @@ import { Resizable } from "re-resizable"; import Loading from "../../../Loading"; import { ClassName, Id } from "../../../../constants"; -import { TAB_HEIGHT } from "../../Main/Tabs/Tabs"; +import { TAB_HEIGHT } from "../../Main/Tabs"; import { Sidebar } from "../sidebar-values"; import { PgShare } from "../../../../utils/pg/share"; import { PgExplorer } from "../../../../utils/pg/explorer"; diff --git a/client/src/components/Panels/Side/Right/Test/Account.tsx b/client/src/components/Panels/Side/Right/Test/Account.tsx index 9b19e6c0..3254a71f 100644 --- a/client/src/components/Panels/Side/Right/Test/Account.tsx +++ b/client/src/components/Panels/Side/Right/Test/Account.tsx @@ -29,7 +29,7 @@ import Input, { defaultInputProps } from "../../../../Input"; import Button from "../../../../Button"; import InputLabel from "./InputLabel"; import useUpdateTxVals, { Identifiers } from "./useUpdateTxVals"; -import useCurrentWallet from "../Wallet/useCurrentWallet"; +import useCurrentWallet from "../../../Wallet/useCurrentWallet"; interface AccountProps { account: IdlAccount; diff --git a/client/src/components/Panels/Side/Right/Test/Function.tsx b/client/src/components/Panels/Side/Right/Test/Function.tsx index 8f7cf4ab..807f43d2 100644 --- a/client/src/components/Panels/Side/Right/Test/Function.tsx +++ b/client/src/components/Panels/Side/Right/Test/Function.tsx @@ -14,7 +14,7 @@ import { getFullType } from "./types"; import { updateTxValsProps } from "./useUpdateTxVals"; import { ClassName } from "../../../../../constants"; import { terminalAtom, txHashAtom } from "../../../../../state"; -import useCurrentWallet from "../Wallet/useCurrentWallet"; +import useCurrentWallet from "../../../Wallet/useCurrentWallet"; import { PgTest } from "../../../../../utils/pg/test"; import { PgTx } from "../../../../../utils/pg/tx"; import { PgTerminal } from "../../../../../utils/pg/terminal"; diff --git a/client/src/components/Panels/Side/Right/Wallet/Wallet.tsx b/client/src/components/Panels/Side/Right/Wallet/Wallet.tsx deleted file mode 100644 index 013a9a29..00000000 --- a/client/src/components/Panels/Side/Right/Wallet/Wallet.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useCallback, useState } from "react"; -import { useAtom } from "jotai"; -import { useConnection } from "@solana/wallet-adapter-react"; -import { PublicKey } from "@solana/web3.js"; -import styled, { css } from "styled-components"; - -import { terminalAtom, txHashAtom } from "../../../../../state"; -import { PgTx } from "../../../../../utils/pg/tx"; -import Button from "../../../../Button"; -import useConnect from "./useConnect"; -import useCurrentWallet from "./useCurrentWallet"; -import useAirdropAmount from "./useAirdropAmount"; -import { PgTerminal } from "../../../../../utils/pg/terminal"; -import { PgCommon } from "../../../../../utils/pg/common"; - -const Wallet = () => { - const [, setTerminal] = useAtom(terminalAtom); - const [, setTxHash] = useAtom(txHashAtom); - - const { - pgButtonStatus, - handleConnectPg, - solButtonStatus, - connecting, - disconnecting, - handleConnect, - } = useConnect(); - const { connection: conn } = useConnection(); - const { pgWalletPk, solWalletPk } = useCurrentWallet(); - - // State - const [loading, setLoading] = useState(false); - - // Get cap amount for airdrop based on network - const amount = useAirdropAmount(); - - const airdrop = useCallback( - async (walletPk: PublicKey) => { - if (!amount) return; - - setLoading(true); - - let msg = ""; - - try { - msg = PgTerminal.info("Sending an airdrop request..."); - setTerminal(msg); - - const txHash = await conn.requestAirdrop( - walletPk, - PgCommon.SolToLamports(amount) - ); - - setTxHash(txHash); - - const txResult = await PgTx.confirm(txHash, conn); - - if (txResult?.err) - msg = `${PgTerminal.CROSS} ${PgTerminal.error( - "Error" - )} receiving airdrop.`; - else - msg = `${PgTerminal.CHECKMARK} ${PgTerminal.success( - "Success" - )}. Received ${amount} SOL.`; - } catch (e: any) { - msg = e.message; - } finally { - setTerminal(msg + "\n"); - setLoading(false); - } - }, - [conn, amount, setLoading, setTerminal, setTxHash] - ); - - const airdropPg = useCallback(async () => { - if (pgWalletPk) await airdrop(pgWalletPk); - }, [pgWalletPk, airdrop]); - - const airdropSol = useCallback(async () => { - if (solWalletPk) await airdrop(solWalletPk); - }, [solWalletPk, airdrop]); - - const pgCond = conn && pgWalletPk && amount; - const solCond = conn && solWalletPk && amount; - - return ( - - - {pgCond && ( - - )} - or - - {solCond && ( - - )} - - ); -}; - -const Wrapper = styled.div` - ${({ theme }) => css` - padding: 1rem; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: ${theme.colors.tooltip?.bg}; - border: 1px solid ${theme.colors.default.borderColor}; - border-radius: ${theme.borderRadius}; - - & button.airdrop { - margin-top: 1rem; - } - `} -`; - -const Or = styled.span` - margin: 1rem; -`; - -export default Wallet; diff --git a/client/src/components/Panels/Side/Right/Wallet/Approve.tsx b/client/src/components/Panels/Wallet/Approve.tsx similarity index 81% rename from client/src/components/Panels/Side/Right/Wallet/Approve.tsx rename to client/src/components/Panels/Wallet/Approve.tsx index 418cecbb..e876bf3c 100644 --- a/client/src/components/Panels/Side/Right/Wallet/Approve.tsx +++ b/client/src/components/Panels/Wallet/Approve.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import styled from "styled-components"; -import ModalInside from "../../../../Modal/ModalInside"; -import useModal from "../../../../Modal/useModal"; +import ModalInside from "../../Modal/ModalInside"; +import useModal from "../../Modal/useModal"; interface ApproveProps {} diff --git a/client/src/components/Panels/Side/Right/Wallet/Setup.tsx b/client/src/components/Panels/Wallet/Setup.tsx similarity index 84% rename from client/src/components/Panels/Side/Right/Wallet/Setup.tsx rename to client/src/components/Panels/Wallet/Setup.tsx index 87c48194..719a4b36 100644 --- a/client/src/components/Panels/Side/Right/Wallet/Setup.tsx +++ b/client/src/components/Panels/Wallet/Setup.tsx @@ -4,15 +4,15 @@ import { Buffer } from "buffer"; import { Keypair } from "@solana/web3.js"; import styled, { css } from "styled-components"; -import { PgCommon } from "../../../../../utils/pg/common"; -import { PgWallet } from "../../../../../utils/pg/wallet"; -import DownloadButton from "../../../../DownloadButton"; -import { Warning } from "../../../../Icons"; -import ModalInside from "../../../../Modal/ModalInside"; -import useModal from "../../../../Modal/useModal"; -import Text from "../../../../Text"; -import UploadButton from "../../../../UploadButton"; -import { pgWalletAtom } from "../../../../../state"; +import { PgCommon } from "../../../utils/pg/common"; +import { PgWallet } from "../../../utils/pg/wallet"; +import DownloadButton from "../../DownloadButton"; +import { Warning } from "../../Icons"; +import ModalInside from "../../Modal/ModalInside"; +import useModal from "../../Modal/useModal"; +import Text from "../../Text"; +import UploadButton from "../../UploadButton"; +import { pgWalletAtom } from "../../../state"; interface SetupProps { onSubmit: () => void; @@ -88,12 +88,9 @@ const Setup: FC = ({ onSubmit }) => { diff --git a/client/src/components/Panels/Wallet/Wallet.tsx b/client/src/components/Panels/Wallet/Wallet.tsx new file mode 100644 index 00000000..2219e2a3 --- /dev/null +++ b/client/src/components/Panels/Wallet/Wallet.tsx @@ -0,0 +1,557 @@ +import { + ChangeEvent, + FC, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { useAtom } from "jotai"; +import { Buffer } from "buffer"; +import { useConnection } from "@solana/wallet-adapter-react"; +import { ConfirmedSignatureInfo, Keypair, PublicKey } from "@solana/web3.js"; +import useCopyClipboard from "react-use-clipboard"; +import styled, { css } from "styled-components"; + +import { + endpointAtom, + pgWalletAtom, + showWalletAtom, + terminalAtom, + txHashAtom, +} from "../../../state"; +import { PgTx } from "../../../utils/pg/tx"; +import { PgTerminal } from "../../../utils/pg/terminal"; +import { PgCommon } from "../../../utils/pg/common"; +import useConnect from "./useConnect"; +import useCurrentWallet from "./useCurrentWallet"; +import useAirdropAmount from "./useAirdropAmount"; +import { TAB_HEIGHT } from "../Main/Tabs"; +import { EDITOR_SCROLLBAR_WIDTH } from "../Main/Editor"; +import { + Clock, + Error as ErrorIcon, + Refresh, + Sad, + ThreeDots, +} from "../../Icons"; +import Button from "../../Button"; +import DownloadButton from "../../DownloadButton"; +import { PgWallet } from "../../../utils/pg/wallet"; +import UploadButton from "../../UploadButton"; +import Link from "../../Link"; + +const Wallet = () => { + const [showWallet] = useAtom(showWalletAtom); + + const { walletPkStr } = useCurrentWallet(); + + if (!showWallet || !walletPkStr) return null; + + return ( + + +
+ +
+
+ ); +}; + +const WalletTitle = () => { + const { walletPkStr } = useCurrentWallet(); + + const [, setCopied] = useCopyClipboard(walletPkStr); + + return ( + + + {PgCommon.shortenPk(walletPkStr)} + + + + ); +}; + +const Txs = () => { + const { connection: conn } = useConnection(); + const { currentWallet } = useCurrentWallet(); + + // State + const [signatures, setSignatures] = useState(); + const [refreshCount, setRefreshCount] = useState(0); + + useEffect(() => { + if (!currentWallet) return; + + const getTxs = async () => { + try { + const _signatures = await conn.getSignaturesForAddress( + currentWallet.publicKey, + { limit: 10 } + ); + setSignatures(_signatures); + } catch (e: any) { + console.log(e.message); + } + }; + + getTxs(); + }, [conn, currentWallet, refreshCount, setSignatures]); + + const refresh = useCallback(() => { + setRefreshCount((rc) => rc + 1); + }, [setRefreshCount]); + + return ( + + + Transactions + + + + + Signature + Slot + + + {signatures?.length ? ( + signatures.map((info, i) => ) + ) : ( + + + No transaction found. + + )} + + + ); +}; + +const WalletSettings = () => { + const [show, setShow] = useState(false); + + const toggle = useCallback(() => { + setShow((s) => !s); + }, [setShow]); + + const close = useCallback(() => { + setShow(false); + }, [setShow]); + + const settingsRef = useRef(null); + + useEffect(() => { + if (!show || !settingsRef.current) return; + + const handleClick = (e: globalThis.MouseEvent) => { + if (!settingsRef.current?.contains(e.target as Node)) setShow(false); + }; + + document.addEventListener("mousedown", handleClick); + + return () => document.removeEventListener("mousedown", handleClick); + }, [show]); + + return ( + + + {show && ( + + + + + + + )} + + ); +}; + +interface SettingsItemProps { + close: () => void; +} + +const Airdrop: FC = ({ close }) => { + const [, setTerminal] = useAtom(terminalAtom); + const [, setTxHash] = useAtom(txHashAtom); + + // State + const [loading, setLoading] = useState(false); + + // Get cap amount for airdrop based on network + const { connection: conn } = useConnection(); + const amount = useAirdropAmount(); + const { pgWalletPk, solWalletPk } = useCurrentWallet(); + + const airdrop = useCallback( + async (walletPk: PublicKey) => { + if (!amount || loading) return; + + setLoading(true); + close(); + + let msg = ""; + + try { + msg = PgTerminal.info("Sending an airdrop request..."); + setTerminal(msg); + + const txHash = await conn.requestAirdrop( + walletPk, + PgCommon.SolToLamports(amount) + ); + + setTxHash(txHash); + + const txResult = await PgTx.confirm(txHash, conn); + + if (txResult?.err) + msg = `${PgTerminal.CROSS} ${PgTerminal.error( + "Error" + )} receiving airdrop.`; + else + msg = `${PgTerminal.CHECKMARK} ${PgTerminal.success( + "Success" + )}. Received ${amount} SOL.`; + } catch (e: any) { + msg = `${PgTerminal.CROSS} ${PgTerminal.error( + "Error" + )} receiving airdrop: ${e.message}`; + } finally { + setTerminal(msg + "\n"); + setLoading(false); + } + }, + [conn, amount, loading, setLoading, setTerminal, setTxHash, close] + ); + + const airdropPg = useCallback(async () => { + if (pgWalletPk) await airdrop(pgWalletPk); + }, [pgWalletPk, airdrop]); + + const airdropSol = useCallback(async () => { + if (solWalletPk) await airdrop(solWalletPk); + }, [solWalletPk, airdrop]); + + const pgCond = conn && pgWalletPk && amount; + const solCond = conn && solWalletPk && amount; + + return ( + <> + {pgCond && Airdrop} + {solCond && ( + Airdrop Phantom + )} + + ); +}; + +const ImportKeypair: FC = ({ close }) => { + const [, setPgWallet] = useAtom(pgWalletAtom); + + const handleUpload = async (e: ChangeEvent) => { + const files = e.target.files; + if (!files?.length) return; + + try { + const file = files[0]; + const arrayBuffer = await file.arrayBuffer(); + const decodedString = PgCommon.decodeArrayBuffer(arrayBuffer); + const buffer = Buffer.from(JSON.parse(decodedString)); + if (buffer.length !== 64) throw new Error("Invalid keypair"); + + // Check if the keypair is valid + Keypair.fromSecretKey(new Uint8Array(buffer)); + + // Update localstorage + PgWallet.update({ + sk: Array.from(buffer), + }); + + // Update global wallet state + setPgWallet(new PgWallet()); + close(); + } catch (err: any) { + console.log(err.message); + } + }; + + return ( + + Import keypair + + ); +}; + +const ExportKeypair = () => { + const walletKp = PgWallet.getKp(); + + return ( + + + Export keypair + + + ); +}; + +const Connect: FC = ({ close }) => { + const { solButtonStatus, connecting, disconnecting, handleConnect } = + useConnect(); + + const handleClick = () => { + if (connecting || disconnecting) return; + + handleConnect(); + close(); + }; + + return {solButtonStatus}; +}; + +const Tx: FC = ({ + signature, + slot, + err, + blockTime, +}) => { + const [endpoint] = useAtom(endpointAtom); + + const [hover, setHover] = useState(false); + + const enter = useCallback(() => setHover(true), [setHover]); + const leave = useCallback(() => setHover(false), [setHover]); + + const now = new Date().getTime() / 1000; + const timePassed = PgCommon.secondsToTime(now - (blockTime ?? 0)); + + const [explorer, solscan] = PgCommon.getExplorerUrls(signature, endpoint); + + return ( + + {hover ? ( + + Solana Explorer + {solscan && Solscan} + + ) : ( + <> + + {err && } + {signature.substring(0, 5)}... + + {slot} + {blockTime && } + + )} + + ); +}; + +const Wrapper = styled.div` + ${({ theme }) => css` + position: absolute; + right: ${EDITOR_SCROLLBAR_WIDTH}; + top: ${TAB_HEIGHT}; + width: 20rem; + min-height: 12rem; + overflow: hidden; + background-color: ${theme.colors.right?.bg ?? theme.colors.default.bg}; + border-left: 1px solid ${theme.colors.default.borderColor}; + border-bottom: 1px solid ${theme.colors.default.borderColor}; + border-radius: ${theme.borderRadius}; + `} +`; + +const TitleWrapper = styled.div` + ${({ theme }) => css` + display: flex; + justify-content: center; + align-items: center; + padding: 0.5rem; + color: ${theme.colors.default.textSecondary}; + position: relative; + height: 2rem; + `} +`; + +const Title = styled.span` + &:hover { + cursor: pointer; + color ${({ theme }) => theme.colors.default.textPrimary}; + } +`; + +const SettingsWrapper = styled.div` + position: absolute; + right: 1rem; +`; + +const SettingsList = styled.div` + ${({ theme }) => css` + position: absolute; + right: 0; + top: 1.75rem; + background-color: ${theme.colors.right?.bg ?? theme.colors.default.bg}; + font-size: ${theme.font?.size.small}; + border: 1px solid ${theme.colors.default.borderColor}; + border-radius: ${theme.borderRadius}; + min-width: 8.5rem; + + & > :not(:last-child), + & .export-wallet-keypair { + border-bottom: 1px solid ${theme.colors.default.borderColor}; + } + `} +`; + +const SettingsItem = styled.div` + ${({ theme }) => css` + display: flex; + padding: 0.5rem 0.75rem; + + &:hover { + background-color: ${theme.colors.state.hover.bg}; + color: ${theme.colors.default.textPrimary}; + cursor: pointer; + } + `} +`; + +const Main = styled.div` + ${({ theme }) => css` + background: linear-gradient( + 0deg, + ${theme.colors.right?.bg ?? theme.colors.default.bg} 75%, + ${theme.colors.default.primary + theme.transparency?.low} 100% + ); + padding: 1rem; + `} +`; + +const TxsWrapper = styled.div``; + +const TxTitleWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + & > button { + margin-right: 0.5rem; + } +`; + +const TxTitle = styled.span` + font-weight: bold; +`; + +const TxsListWrapper = styled.div` + ${({ theme }) => css` + border: 1px solid ${theme.colors.default.borderColor}; + border-radius: ${theme.borderRadius}; + background-color: ${theme.colors.right?.otherBg}; + margin-top: 0.5rem; + `} +`; + +const TxsTop = styled.div` + ${({ theme }) => css` + padding: 0.5rem 1rem; + display: flex; + font-size: ${theme.font?.size.small}; + color: ${theme.colors.default.textSecondary}; + background-color: ${theme.colors.right?.bg ?? theme.colors.default.bg}; + font-weight: bold; + + &:not(:last-child) { + border-bottom: 1px solid ${theme.colors.default.borderColor}; + } + `} +`; + +const TxWrapper = styled.div` + ${({ theme }) => css` + padding: 0.5rem 1rem; + display: flex; + font-size: ${theme.font?.size.small}; + color: ${theme.colors.default.textSecondary}; + + &:not(:last-child) { + border-bottom: 1px solid ${theme.colors.default.borderColor}; + } + + &:hover { + color: ${theme.colors.default.textPrimary}; + background: linear-gradient( + 0deg, + ${theme.colors.right?.bg ?? theme.colors.default.bg} 75%, + ${theme.colors.default.primary + theme.transparency?.low} 100% + ); + } + `} +`; + +const HoverWrapper = styled.div` + display: flex; + justify-content: space-around; + width: 100%; + + & > a:hover { + text-decoration: underline; + cursor: pointer; + } +`; + +const Signature = styled.div` + width: 40%; + display: flex; + align-items: center; + + & > svg { + margin-right: 0.25rem; + color: ${({ theme }) => theme.colors.state.error.color}; + } +`; + +const Slot = styled.div` + width: 40%; +`; + +const Time = styled.div` + width: 20%; + display: flex; + justify-content: flex-end; + align-items: center; + + & > svg { + margin-left: 0.25rem; + } +`; + +const NoTransaction = styled.div` + padding: 2rem; + display: flex; + align-items: center; + justify-content: center; + color: ${({ theme }) => theme.colors.default.textSecondary}; + + & > svg { + margin-right: 0.5rem; + width: 1.5rem; + height: 1.5rem; + } +`; + +export default Wallet; diff --git a/client/src/components/Panels/Side/Right/Wallet/connection-states.ts b/client/src/components/Panels/Wallet/connection-states.ts similarity index 100% rename from client/src/components/Panels/Side/Right/Wallet/connection-states.ts rename to client/src/components/Panels/Wallet/connection-states.ts diff --git a/client/src/components/Panels/Side/Right/Wallet/index.ts b/client/src/components/Panels/Wallet/index.ts similarity index 100% rename from client/src/components/Panels/Side/Right/Wallet/index.ts rename to client/src/components/Panels/Wallet/index.ts diff --git a/client/src/components/Panels/Side/Right/Wallet/useAirdropAmount.tsx b/client/src/components/Panels/Wallet/useAirdropAmount.tsx similarity index 80% rename from client/src/components/Panels/Side/Right/Wallet/useAirdropAmount.tsx rename to client/src/components/Panels/Wallet/useAirdropAmount.tsx index f0afc761..ba469570 100644 --- a/client/src/components/Panels/Side/Right/Wallet/useAirdropAmount.tsx +++ b/client/src/components/Panels/Wallet/useAirdropAmount.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useAtom } from "jotai"; -import { Endpoint } from "../../../../../constants"; -import { endpointAtom } from "../../../../../state"; +import { Endpoint } from "../../../constants"; +import { endpointAtom } from "../../../state"; const useAirdropAmount = () => { const [endpoint] = useAtom(endpointAtom); diff --git a/client/src/components/Panels/Side/Right/Wallet/useConnect.tsx b/client/src/components/Panels/Wallet/useConnect.tsx similarity index 94% rename from client/src/components/Panels/Side/Right/Wallet/useConnect.tsx rename to client/src/components/Panels/Wallet/useConnect.tsx index 7015ead6..bfc663f1 100644 --- a/client/src/components/Panels/Side/Right/Wallet/useConnect.tsx +++ b/client/src/components/Panels/Wallet/useConnect.tsx @@ -3,12 +3,8 @@ import { useAtom } from "jotai"; import { useWallet } from "@solana/wallet-adapter-react"; import { ConnState } from "./connection-states"; -import { - modalAtom, - pgWalletAtom, - refreshPgWalletAtom, -} from "../../../../../state"; -import { PgWallet } from "../../../../../utils/pg/wallet"; +import { modalAtom, pgWalletAtom, refreshPgWalletAtom } from "../../../state"; +import { PgWallet } from "../../../utils/pg/wallet"; import Setup from "./Setup"; const useConnect = () => { diff --git a/client/src/components/Panels/Side/Right/Wallet/useCurrentWallet.tsx b/client/src/components/Panels/Wallet/useCurrentWallet.tsx similarity index 87% rename from client/src/components/Panels/Side/Right/Wallet/useCurrentWallet.tsx rename to client/src/components/Panels/Wallet/useCurrentWallet.tsx index 098d2189..0776fdff 100644 --- a/client/src/components/Panels/Side/Right/Wallet/useCurrentWallet.tsx +++ b/client/src/components/Panels/Wallet/useCurrentWallet.tsx @@ -2,8 +2,8 @@ import { useMemo } from "react"; import { useAtom } from "jotai"; import { AnchorWallet, useAnchorWallet } from "@solana/wallet-adapter-react"; -import { pgWalletAtom, refreshPgWalletAtom } from "../../../../../state"; -import { PgWallet } from "../../../../../utils/pg/wallet"; +import { pgWalletAtom, refreshPgWalletAtom } from "../../../state"; +import { PgWallet } from "../../../utils/pg/wallet"; const useCurrentWallet = () => { const [pgWallet] = useAtom(pgWalletAtom); diff --git a/client/src/components/Toast/ExplorerLink.tsx b/client/src/components/Toast/ExplorerLink.tsx index 336be81c..2809b595 100644 --- a/client/src/components/Toast/ExplorerLink.tsx +++ b/client/src/components/Toast/ExplorerLink.tsx @@ -1,15 +1,15 @@ import { useAtom } from "jotai"; import styled from "styled-components"; -import { Endpoint, EXPLORER_URL, SOLSCAN_URL } from "../../constants"; import { endpointAtom, txHashAtom } from "../../state"; +import { PgCommon } from "../../utils/pg/common"; import Link from "../Link"; export const ExplorerLink = () => { const [txHash] = useAtom(txHashAtom); const [endpoint] = useAtom(endpointAtom); - const [explorer, solscan] = getUrls(txHash, endpoint); + const [explorer, solscan] = PgCommon.getExplorerUrls(txHash, endpoint); return ( @@ -19,24 +19,6 @@ export const ExplorerLink = () => { ); }; -const getUrls = (txHash: string, endpoint: Endpoint) => { - const explorer = - EXPLORER_URL + "/tx/" + txHash + "?cluster=custom&customUrl=" + endpoint; - - let cluster = ""; - if (endpoint === Endpoint.LOCALHOST) return [explorer]; - else if ( - endpoint === Endpoint.DEVNET || - endpoint === Endpoint.DEVNET_GENESYSGO - ) - cluster = "?cluster=devnet"; - else if (endpoint === Endpoint.TESTNET) cluster = "?cluster=testnet"; - - const solscan = SOLSCAN_URL + "/tx/" + txHash + cluster; - - return [explorer, solscan]; -}; - const Wrapper = styled.div` display: flex; justify-content: space-around; diff --git a/client/src/components/UploadButton/UploadButton.tsx b/client/src/components/UploadButton/UploadButton.tsx index b9bd6715..aed05595 100644 --- a/client/src/components/UploadButton/UploadButton.tsx +++ b/client/src/components/UploadButton/UploadButton.tsx @@ -8,6 +8,7 @@ interface UploadButtonProps { onUpload: (e: ChangeEvent) => Promise; showUploadText?: boolean; buttonKind?: ButtonKind; + noButton?: boolean; } const UploadButton: FC = ({ @@ -15,6 +16,7 @@ const UploadButton: FC = ({ onUpload, buttonKind = "outline", showUploadText = false, + noButton = false, children, }) => { const inputRef = useRef(null); @@ -40,9 +42,13 @@ const UploadButton: FC = ({ onChange={handleChange} accept={accept} /> - + {noButton ? ( +
{children}
+ ) : ( + + )} {showUploadText && uploadText && {uploadText}}
); @@ -55,6 +61,10 @@ const Wrapper = styled.div` & input[type="file"] { display: none; } + + & > div { + width: 100%; + } `; const UploadInfo = styled.span` diff --git a/client/src/state/solana.tsx b/client/src/state/solana.tsx index 965328cb..f3c8a2b7 100644 --- a/client/src/state/solana.tsx +++ b/client/src/state/solana.tsx @@ -5,6 +5,7 @@ import { PgWallet } from "../utils/pg/wallet"; // Wallet export const pgWalletAtom = atom(new PgWallet()); +export const showWalletAtom = atom(false); // To trigger re-render const _countAtom = atom(0); diff --git a/client/src/theme/default.ts b/client/src/theme/default.ts index 7ca1fdfb..8ac21e39 100644 --- a/client/src/theme/default.ts +++ b/client/src/theme/default.ts @@ -21,11 +21,17 @@ export const PG_SCROLLBAR = { color: "#ffffff64", hoverColor: "#ffffff32", }, + width: { + editor: "0.75rem", + }, }, light: { thumb: { color: "#00000032", hoverColor: "#00000064", }, + width: { + editor: "0.75rem", + }, }, }; diff --git a/client/src/utils/pg/common.ts b/client/src/utils/pg/common.ts index d8de5e61..802bfc1c 100644 --- a/client/src/utils/pg/common.ts +++ b/client/src/utils/pg/common.ts @@ -1,4 +1,5 @@ import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { Endpoint, EXPLORER_URL, SOLSCAN_URL } from "../../constants"; export class PgCommon { static async sleep(ms: number) { @@ -20,8 +21,8 @@ export class PgCommon { return { arrayBuffer }; } - static shortenPk(pk: PublicKey, chars: number = 5) { - const pkStr = pk.toBase58(); + static shortenPk(pk: PublicKey | string, chars: number = 5) { + const pkStr = typeof pk === "object" ? pk.toBase58() : pk; return `${pkStr.slice(0, chars)}...${pkStr.slice(-chars)}`; } @@ -32,4 +33,41 @@ export class PgCommon { static SolToLamports(sol: number) { return sol * LAMPORTS_PER_SOL; } + + static secondsToTime(secs: number) { + const h = Math.floor(secs / 3600), + m = Math.floor((secs % 3600) / 60), + s = Math.floor(secs % 60); + + if (!(h + m)) return `${s}s`; + else if (!h) return `${m}m`; + else if (!m) return `${h}h`; + + return ""; + } + + static getUtf8EncodedString(object: object) { + return ( + "data:text/json;charset=utf-8," + + encodeURIComponent(JSON.stringify(object)) + ); + } + + static getExplorerUrls(txHash: string, endpoint: Endpoint) { + const explorer = + EXPLORER_URL + "/tx/" + txHash + "?cluster=custom&customUrl=" + endpoint; + + let cluster = ""; + if (endpoint === Endpoint.LOCALHOST) return [explorer]; + else if ( + endpoint === Endpoint.DEVNET || + endpoint === Endpoint.DEVNET_GENESYSGO + ) + cluster = "?cluster=devnet"; + else if (endpoint === Endpoint.TESTNET) cluster = "?cluster=testnet"; + + const solscan = SOLSCAN_URL + "/tx/" + txHash + cluster; + + return [explorer, solscan]; + } }