diff --git a/packages/chess-app/index.html b/packages/chess-app/index.html index e4b78eae1..e0852dfe2 100644 --- a/packages/chess-app/index.html +++ b/packages/chess-app/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Chess
diff --git a/packages/chess-app/src/App.tsx b/packages/chess-app/src/App.tsx index c4fb66801..8b71a8e97 100644 --- a/packages/chess-app/src/App.tsx +++ b/packages/chess-app/src/App.tsx @@ -5,10 +5,7 @@ import { initFlowbite } from 'flowbite' import { Auth, UtilsContext, Wallet, ComputerContext } from '@bitcoin-computer/components' import { ChessBoard } from './components/ChessBoard' -import { Navbar } from "./components/Navbar" -import { MyGames } from "./components/Assets" -import CreateGame from "./components/CreateGame" -import StartGame from "./components/StartGame" +import { Navbar } from './components/Navbar' export default function App() { const [computer] = useState(Auth.getComputer()) @@ -25,11 +22,9 @@ export default function App() { -
+
- } /> - } /> - } /> + } /> } /> } /> diff --git a/packages/chess-app/src/components/Assets.tsx b/packages/chess-app/src/components/Assets.tsx deleted file mode 100644 index a37c8c126..000000000 --- a/packages/chess-app/src/components/Assets.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { VITE_CHESS_GAME_MOD_SPEC } from '../constants/modSpecs' -import { WithPagination } from './Gallery' - -export function MyGames() { - return ( - <> -

All Games

- - - ) -} diff --git a/packages/chess-app/src/components/ChessBoard.tsx b/packages/chess-app/src/components/ChessBoard.tsx index 48bcb7be0..09d2d9274 100644 --- a/packages/chess-app/src/components/ChessBoard.tsx +++ b/packages/chess-app/src/components/ChessBoard.tsx @@ -1,23 +1,33 @@ -import { ComputerContext, Modal, UtilsContext } from "@bitcoin-computer/components" -import { ChessContract, ChessContractHelper, Chess as ChessLib, Square } from "@bitcoin-computer/chess-contracts" -import { useCallback, useContext, useEffect, useState } from "react" -import { useParams, Link } from "react-router-dom" -import { Chessboard } from "react-chessboard" -import { getGameState } from "./utils" -import { VITE_CHESS_GAME_MOD_SPEC } from "../constants/modSpecs" +import { Auth, ComputerContext, Modal, UtilsContext } from '@bitcoin-computer/components' +import { + ChessContract, + ChessContractHelper, + Chess as ChessLib, + Square, +} from '@bitcoin-computer/chess-contracts' +import { useCallback, useContext, useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { Chessboard } from 'react-chessboard' +import { VITE_CHESS_GAME_MOD_SPEC } from '../constants/modSpecs' +import { StartGameModal } from './StartGame' +import { signInModal } from './Navbar' +import { getGameState } from './utils' +import { NewGameModal, newGameModal } from './NewGame' +import { InfiniteScroll } from './GamesList' + +const winnerModal = 'winner-modal' function currentPlayer(fen: string) { - const parts = fen.split(" ") + const parts = fen.split(' ') const activeColor = parts[1] - if (activeColor === "w") return "White" - if (activeColor === "b") return "Black" - throw new Error("Invalid FEN: Unknown active color") + if (activeColor === 'w') return 'White' + if (activeColor === 'b') return 'Black' + throw new Error('Invalid FEN: Unknown active color') } function getWinnerPubKey(chessLibrary: ChessLib, { publicKeyW, publicKeyB }: ChessContract) { - if (chessLibrary.isCheckmate()) - return chessLibrary.turn() === 'b' ? publicKeyW : publicKeyB + if (chessLibrary.isCheckmate()) return chessLibrary.turn() === 'b' ? publicKeyW : publicKeyB return null } @@ -51,30 +61,19 @@ function ListLayout(props: { listOfMoves: string[] }) { return
{rows}
} -function WinnerModal(data: { winnerPubKey: string, userPubKey: string }) { +function WinnerModal(data: { winnerPubKey: string; userPubKey: string }) { return ( <> -
-
+
+

{data.winnerPubKey === data.userPubKey ? `Congratiolations! You have won the game. ` : `Sorry! You have lost the game. `} - Click{' '} - { - Modal.hideModal('winner-modal') - }} - > - here - {' '} - to start a new game. -

+

-
+
+
+
- )} +
{/* Chessboard Column */} -
- {game && ( +
+ {game ? ( <>
- { orientation === 'white' ? chessContract!.nameB : chessContract!.nameW } + {orientation === 'white' ? chessContract!.nameB : chessContract!.nameW}
@@ -218,31 +252,91 @@ export function ChessBoard() {
- { orientation === 'white' ? chessContract!.nameW : chessContract!.nameB } + {orientation === 'white' ? chessContract!.nameW : chessContract!.nameB}
+ ) : ( +
+ +
)}
{/* Moves List Column */} - {chessContract && ( -
+
+ {chessContract ? ( + <> + {game && ( +
+
+
+
+
+ Current Player +
+
{currentPlayer(game.fen())}
+
+
+
+ State +
+
{getGameState(game)}
+
+
+
+ Balance +
+
+ {balance / 1e8} {computer.getChain()} +
+
+
+
+
+ )} +
+
+

+ Move History +

+
+ + +
+ + ) : (
-

Move History

- +
+

+ Play Chess on {computer.getChain()} +

+
+
+ +
-
- )} + )} +
+ + + +
) } diff --git a/packages/chess-app/src/components/Gallery.tsx b/packages/chess-app/src/components/Gallery.tsx deleted file mode 100644 index 278348dcb..000000000 --- a/packages/chess-app/src/components/Gallery.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { Computer } from "@bitcoin-computer/lib" -import { useContext, useEffect, useMemo, useState } from "react" -import { Link, useLocation, useNavigate } from "react-router-dom" -import { initFlowbite } from "flowbite" -import { Auth, ComputerContext, UtilsContext } from "@bitcoin-computer/components" -import { BiGitCompare } from "react-icons/bi" -import { ChessContract } from "../../../chess-contracts/" -import { Chess as ChessLib } from "../../../chess-contracts/" -import { getGameState } from "./utils" - -export type Class = new (...args: unknown[]) => unknown - -export type UserQuery = Partial<{ - mod: string - publicKey: string - limit: number - offset: number - order: 'ASC' | 'DESC' - ids: string[] - contract: { - class: T - args?: ConstructorParameters - } -}> - -function GameCard({ chessContract }: { chessContract: ChessContract }) { - const chessLib = new ChessLib(chessContract.fen) - const publicKey = Auth.getComputer().getPublicKey() - return ( - <> -
-
-

- {getGameState(chessLib)} -

-

- {chessContract.nameW} -

-
- -
-

- {chessContract.nameB} -

-
-
- - ) -} - -function HomePageCard({ content }: { content: () => JSX.Element }) { - return ( -
-
-        {typeof content === 'function' ? content() : ''}
-      
-
- ) -} - -function ValueComponent({ rev, computer }: { rev: string; computer: Computer }) { - const [value, setValue] = useState({ _id: '' } as ChessContract) - const [errorMsg, setMsgError] = useState("") - const [loading, setLoading] = useState(true) - - useEffect(() => { - const fetch = async () => { - try { - const synced: ChessContract = (await computer.sync(rev)) as ChessContract - setValue(synced) - } catch (err) { - if (err instanceof Error) setMsgError(`Error: ${err.message}`) - } - setLoading(false) - } - fetch() - }, [computer, rev]) - - const loadingContent = () => ( - <> - -  Loading... - - ) - - if (loading) { - return - } - - if (errorMsg) { - return <>errorMsg} /> - } - - return ( - - - - ) -} - -function FromRevs({ revs, computer }: { revs: string[]; computer: Computer }) { - const cols: string[][] = [[], [], [], []] - - revs.forEach((rev, index) => { - const colNumber = index % 4 - cols[colNumber].push(rev) - }) - return ( - <> -
-
- {cols[0].map((rev) => ( -
- -
- ))} -
-
- {cols[1].map((rev) => ( -
- -
- ))} -
-
- {cols[2].map((rev) => ( -
- -
- ))} -
-
- {cols[3].map((rev) => ( -
- -
- ))} -
-
- - ) -} - -type PaginationType = { - isPrevAvailable: boolean, - isNextAvailable: boolean, - handlePrev: React.MouseEventHandler, - handleNext: React.MouseEventHandler -} -function Pagination({ isPrevAvailable, handlePrev, isNextAvailable, handleNext }: PaginationType) { - return ( - - ) -} - -export function WithPagination(q: UserQuery) { - const navigate = useNavigate() - const { showLoader } = UtilsContext.useUtilsComponents() - const contractsPerPage = 12 - const computer = useContext(ComputerContext) - const [pageNum, setPageNum] = useState(0) - const [isNextAvailable, setIsNextAvailable] = useState(true) - const [isPrevAvailable, setIsPrevAvailable] = useState(pageNum > 0) - const [showNoAsset, setShowNoAsset] = useState(false) - const [revs, setRevs] = useState([]) - const location = useLocation() - - const params = useMemo( - () => Object.fromEntries(new URLSearchParams(location.search)), - [location.search] - ) - - useEffect(() => { - initFlowbite() - }, []) - - useEffect(() => { - const fetch = async () => { - showLoader(true) - const query = { ...q, ...params } - query.offset = contractsPerPage * pageNum - query.limit = contractsPerPage + 1 - query.order = 'DESC' - const result = await computer.query(query) - setIsNextAvailable(result.length > contractsPerPage) - setRevs(result.slice(0, contractsPerPage)) - if (pageNum === 0 && result?.length === 0) { - setShowNoAsset(true) - } - showLoader(false) - } - fetch() - }, [computer, pageNum, q, params]) - - const handleNext = async () => { - setIsPrevAvailable(true) - setPageNum(pageNum + 1) - } - - const handlePrev = async () => { - setIsNextAvailable(true) - if (pageNum - 1 === 0) setIsPrevAvailable(false) - setPageNum(pageNum - 1) - } - - return ( -
- - {!(pageNum === 0 && revs && revs.length === 0) && ( - - )} - {pageNum === 0 && revs && revs.length === 0 && showNoAsset && ( -
- -
- )} -
- ) -} diff --git a/packages/chess-app/src/components/GamesList.tsx b/packages/chess-app/src/components/GamesList.tsx new file mode 100644 index 000000000..5377c8695 --- /dev/null +++ b/packages/chess-app/src/components/GamesList.tsx @@ -0,0 +1,113 @@ +import { ComputerContext } from '@bitcoin-computer/components' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { VITE_CHESS_GAME_MOD_SPEC } from '../constants/modSpecs' + +type Class = new (...args: unknown[]) => unknown + +type UserQuery = Partial<{ + mod: string + publicKey: string + limit: number + offset: number + order: 'ASC' | 'DESC' + ids: string[] + contract: { + class: T + args?: ConstructorParameters + } +}> + +export const InfiniteScroll = ({ + setGameId, +}: { + setGameId: React.Dispatch> +}) => { + const [items, setItems] = useState([]) + const [hasMore, setHasMore] = useState(true) + const [loading, setLoading] = useState(false) + const scrollContainerRef = useRef(null) + const computer = useContext(ComputerContext) + const contractsPerPage = 12 + const navigate = useNavigate() + + const fetchMoreItems = useCallback( + async (q: UserQuery): Promise => { + const query = { ...q } + query.limit = contractsPerPage * 2 // For a chess game, we have two contracts: Payment and ChessContract + query.order = 'DESC' + const result = (await computer.query(query)) as string[] + + const filteredRevs = (result || []).filter((rev) => rev.split(':')[1] === '0') + return filteredRevs + }, + [contractsPerPage, computer], + ) + + const loadMoreItems = useCallback(async () => { + if (loading || !hasMore) return + + setLoading(true) + const newItems = await fetchMoreItems({ mod: VITE_CHESS_GAME_MOD_SPEC, offset: items.length }) + + setItems((prev) => [...prev, ...newItems]) + if (newItems.length < contractsPerPage) setHasMore(false) // Stop fetching when no more items are available + setLoading(false) + }, [loading, hasMore, items, fetchMoreItems]) + + const handleScroll = () => { + const container = scrollContainerRef.current + if (container) { + const bottomReached = container.scrollTop + container.clientHeight >= container.scrollHeight + if (bottomReached) { + loadMoreItems() + } + } + } + + useEffect(() => { + // Initial fetch without relying on scroll + const initialFetch = async () => { + setLoading(true) + const initialItems = await fetchMoreItems({ mod: VITE_CHESS_GAME_MOD_SPEC, offset: 0 }) + setItems(initialItems) + if (initialItems.length < contractsPerPage) setHasMore(false) + setLoading(false) + } + + initialFetch() + }, [fetchMoreItems]) + + return ( +
+
+

All Games

+
+
+
    + {items.map((item, index) => ( +
  • { + navigate(`/game/${item}`) + setGameId(item) + }} + > + {item} +
  • + ))} +
+ {loading && ( +
Loading...
+ )} +
+
+ ) +} diff --git a/packages/chess-app/src/components/Navbar.tsx b/packages/chess-app/src/components/Navbar.tsx index f05941ad0..da14d5b61 100644 --- a/packages/chess-app/src/components/Navbar.tsx +++ b/packages/chess-app/src/components/Navbar.tsx @@ -6,6 +6,7 @@ import { Chain, Network } from '../types/common' const modalTitle = 'Connect to Node' const modalId = 'unsupported-config-modal' +export const signInModal = 'sign-in-modal' function formatChainAndNetwork(chain: Chain, network: Network) { const map = { @@ -85,7 +86,7 @@ function SignInItem() { return (
  • ) @@ -107,7 +108,7 @@ export function NotLoggedMenu() { localStorage.setItem('CHAIN', chain) localStorage.setItem('NETWORK', network) setDropDownLabel(formatChainAndNetwork(chain, network)) - window.location.href = "/" + window.location.href = '/' } catch (error) { if (error instanceof Error) { showSnackBar(`Error setting chain and network: ${error.message}`, false) @@ -195,25 +196,9 @@ function WalletItem() { ) } -const capitalizeFirstLetter = (s: string) => s.charAt(0).toUpperCase() + s.slice(1) - -function Item({ dest }: { dest: string }) { - return ( - - - {capitalizeFirstLetter(dest)} - - - ) -} - export function LoggedInMenu() { return (
      -
    ) @@ -248,12 +233,21 @@ function NavbarDropdownButton() { ) } -export function Logo({ name = 'TBC Chess' }) { +export function Logo() { + const { chain } = Auth.defaultConfiguration() + const AppName = + chain === 'LTC' + ? 'Lite Chess' + : chain === 'DOGE' + ? 'Doge Chess' + : chain === 'PEPE' + ? 'Pepe Chess' + : 'Bit Chess' return ( Bitcoin Computer Logo - {name} + {AppName} ) diff --git a/packages/chess-app/src/components/NewGame.tsx b/packages/chess-app/src/components/NewGame.tsx new file mode 100644 index 000000000..681d2ea90 --- /dev/null +++ b/packages/chess-app/src/components/NewGame.tsx @@ -0,0 +1,197 @@ +import { ComputerContext, Modal, UtilsContext } from '@bitcoin-computer/components' +import { ChessContractHelper } from '@bitcoin-computer/chess-contracts' +import { useContext, useState } from 'react' +import { getHash } from '../services/secret.service' +import { VITE_CHESS_GAME_MOD_SPEC } from '../constants/modSpecs' + +export const newGameModal = 'new-game-modal' + +function NewGameModalContent({ + nameW, + nameB, + publicKeyB, + setSecondPlayerPublicKey, + amount, + setAmount, + copied, + setCopied, + serializedTx, + setSerializedTx, +}: { + nameW: string + setName: React.Dispatch> + nameB: string + setNameB: React.Dispatch> + publicKeyB: string + setSecondPlayerPublicKey: React.Dispatch> + amount: string + setAmount: React.Dispatch> + copied: boolean + setCopied: React.Dispatch> + serializedTx: string + setSerializedTx: React.Dispatch> +}) { + const computerW = useContext(ComputerContext) + + const { showLoader, showSnackBar } = UtilsContext.useUtilsComponents() + + const handleCopy = () => { + navigator.clipboard + .writeText(serializedTx) + .then(() => setCopied(true)) + .catch(() => setCopied(false)) + + setTimeout(() => setCopied(false), 2000) + } + + const onSubmit = async (e: React.SyntheticEvent) => { + e.preventDefault() + try { + showLoader(true) + const secretHashW = await getHash() + const secretHashB = await getHash() + + if (!secretHashW || !secretHashB) throw new Error('Could not obtain hash from server') + + const publicKeyW = computerW.getPublicKey() + const chessContractHelper = new ChessContractHelper({ + computer: computerW, + amount: parseFloat(amount) * 1e8, + nameW, + nameB, + publicKeyW, + publicKeyB, + secretHashW, + secretHashB, + mod: VITE_CHESS_GAME_MOD_SPEC, + }) + const tx = await chessContractHelper.makeTx() + setSerializedTx(`http://localhost:1032?start-game=${tx.serialize()}`) + + showLoader(false) + } catch (err) { + if (err instanceof Error) { + showSnackBar(err.message, false) + } else { + showSnackBar('Error occurred!', false) + } + showLoader(false) + } + } + + return ( + <> + {serializedTx ? ( +
    +
    +

    + Share this link with your opponent to start the game. +

    +

    + {`${serializedTx.slice(0, 50)}...`} +

    +
    +
    + +
    +
    + ) : ( + <> +
    +
    +
    + + setAmount(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
    +
    + + setSecondPlayerPublicKey(e.target.value)} + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-md focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + /> +
    +
    +
    + +
    +
    + + )} + + ) +} +export function NewGameModal() { + const [nameW, setName] = useState('White') + const [nameB, setNameB] = useState('Black') + const [publicKeyB, setSecondPlayerPublicKey] = useState('') + const [amount, setAmount] = useState(`0.1`) + const [copied, setCopied] = useState(false) + const [serializedTx, setSerializedTx] = useState('') + + const handleClear = () => { + setName('White') + setNameB('Black') + setSecondPlayerPublicKey('') + setAmount(`0.1`) + setCopied(false) + setSerializedTx('') + } + return ( + + ) +} diff --git a/packages/chess-app/src/components/StartGame.tsx b/packages/chess-app/src/components/StartGame.tsx index e22a4cdc5..dfff6b83e 100644 --- a/packages/chess-app/src/components/StartGame.tsx +++ b/packages/chess-app/src/components/StartGame.tsx @@ -1,118 +1,192 @@ -import { useContext, useState } from "react" -import { ComputerContext, Modal, UtilsContext } from "@bitcoin-computer/components" -import { Computer, Transaction } from "@bitcoin-computer/lib" -import { useParams } from "react-router-dom" -import { ChessContract, ChessContractHelper } from "../../../chess-contracts/" -import { VITE_CHESS_GAME_MOD_SPEC } from "../constants/modSpecs" +import { useContext, useEffect, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { ComputerContext, Modal, UtilsContext } from '@bitcoin-computer/components' +import { Computer, Transaction } from '@bitcoin-computer/lib' +import { ChessContract, ChessContractHelper } from '../../../chess-contracts/' +import { VITE_CHESS_GAME_MOD_SPEC } from '../constants/modSpecs' -function ErrorContent(msg: string) { - return ( - <> -
    - Something went wrong.
    {msg} -
    -
    - -
    - - ) -} +const startGameModal = 'start-game-modal' -function StartForm(props: { +export function StartGameModalContent({ + serialized, + game, + computer, + copied, + setCopied, + link, + setLink, +}: { + serialized: string + game: ChessContract computer: Computer - setErrorMsg: React.Dispatch> + copied: boolean + setCopied: React.Dispatch> + link: string + setLink: React.Dispatch> }) { - const { computer, setErrorMsg } = props - const { serialized } = useParams() - const [link, setLink] = useState('') - const [copied, setCopied] = useState(false) + const { showLoader, showSnackBar } = UtilsContext.useUtilsComponents() + + const handleCopy = () => { + navigator.clipboard + .writeText(link) + .then(() => setCopied(true)) + .catch(() => setCopied(false)) - const { showLoader } = UtilsContext.useUtilsComponents() + setTimeout(() => setCopied(false), 2000) + } const onSubmit = async (e: React.SyntheticEvent) => { e.preventDefault() try { showLoader(true) - if (!serialized) throw new Error('Invalid link') - const tx = Transaction.deserialize(serialized) - const { effect } = await computer.encode(tx.onChainMetaData as never) - const { res } = effect - const game = res as unknown as ChessContract - const chessContractHelper = ChessContractHelper.fromContract(computer, game, VITE_CHESS_GAME_MOD_SPEC) + const chessContractHelper = ChessContractHelper.fromContract( + computer, + game, + VITE_CHESS_GAME_MOD_SPEC, + ) const txId = await chessContractHelper.completeTx(tx) setLink(`http://localhost:1032/game/${txId}:0`) showLoader(false) } catch (err) { - console.log('Err', err) - showLoader(false) if (err instanceof Error) { - if(err.message.startsWith('Failed to load module')) setErrorMsg("Run 'npm run deploy' to deploy the smart contracts.") - else setErrorMsg(err.message) - Modal.showModal("error-modal") + showSnackBar(err.message, false) + } else { + showSnackBar('Error occurred!', false) } + showLoader(false) } } - const handleCopy = (e: React.SyntheticEvent) => { - e.preventDefault() - navigator.clipboard.writeText(link) - .then(() => setCopied(true)) - .catch(() => setCopied(false)) - - setTimeout(() => setCopied(false), 2000) - } - return ( <> -
    - - -
    -

    - {link} -

    - -
    + {link ? ( +
    +
    +

    + You can find your game URL at the link below. Please share this URL with the white + player. +

    + + {`${link.slice(0, 40)}...`} + +
    - +
    + +
    +
    + ) : ( + <> + {!!game && ( +
    +
    +
    +
    + + Amount + + + {game.amount / 1e8} {computer.getChain()} + +
    +
    +
    +
    + + Opponent + +
    + + {game.publicKeyW} + +
    +
    +
    +
    +
    + +
    +
    + )} + + )} ) } -export default function StartGame() { +export function StartGameModal() { + const location = useLocation() + const queryParams = new URLSearchParams(location.search) // Parse the query string + const serialized = queryParams.get('start-game') const computer = useContext(ComputerContext) - const [errorMsg, setErrorMsg] = useState("") + const [game, setGame] = useState(null) + const [copied, setCopied] = useState(false) + const [link, setLink] = useState('') + + const { showLoader, showSnackBar } = UtilsContext.useUtilsComponents() + + useEffect(() => { + const fetch = async () => { + try { + showLoader(true) + if (serialized) { + const tx = Transaction.deserialize(serialized) + const { effect } = await computer.encode(tx.onChainMetaData as never) + const { res } = effect + const game = res as unknown as ChessContract + setGame(game) + Modal.showModal(startGameModal) + showLoader(false) + } + } catch (error) { + showLoader(false) + if (error instanceof Error) { + showSnackBar(error.message, false) + } else { + showSnackBar('Error occurred', false) + } + } finally { + showLoader(false) + } + } + fetch() + }, [serialized, computer, showLoader, showSnackBar]) return ( - <> - - - + ) } diff --git a/packages/chess-app/src/index.css b/packages/chess-app/src/index.css index a47e51dd6..eebebf109 100644 --- a/packages/chess-app/src/index.css +++ b/packages/chess-app/src/index.css @@ -2,6 +2,7 @@ @tailwind components; @tailwind utilities; body { + @apply bg-white dark:bg-gray-900; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; diff --git a/packages/chess-app/tailwind.config.js b/packages/chess-app/tailwind.config.js index c53aed771..f65149acc 100644 --- a/packages/chess-app/tailwind.config.js +++ b/packages/chess-app/tailwind.config.js @@ -1,18 +1,18 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - "./src/**/*.{js,jsx,ts,tsx}", - "./node_modules/@bitcoin-computer/components/built/**/*.{js,jsx,ts,tsx}", - "../components/built/**/*.{js,jsx,ts,tsx}" + './src/**/*.{js,jsx,ts,tsx}', + './node_modules/@bitcoin-computer/components/built/**/*.{js,jsx,ts,tsx}', + '../components/built/**/*.{js,jsx,ts,tsx}', ], - darkMode: "media", + darkMode: 'media', theme: { extend: { colors: { - "blue-1": "#000F38", - "blue-2": "#002A99", - "blue-3": "#0046FF", - "blue-4": "#A7BFFF", + 'blue-1': '#000F38', + 'blue-2': '#002A99', + 'blue-3': '#0046FF', + 'blue-4': '#A7BFFF', }, }, }, diff --git a/packages/chess-contracts/.env.example b/packages/chess-contracts/.env.example index 8f6db2530..7cff0b340 100644 --- a/packages/chess-contracts/.env.example +++ b/packages/chess-contracts/.env.example @@ -1,12 +1 @@ -# Application configuration -VITE_CHAIN=LTC -VITE_NETWORK=regtest -VITE_URL=http://127.0.0.1:1031 VITE_API_BASE_URL=http://127.0.0.1:4000 - -# Application Port -VITE_PORT=1032 - -# Smart Contract Locations -# Run 'npm run deploy' to generate module specifiers -VITE_CHESS_GAME_MOD_SPEC= diff --git a/packages/chess-server/README.md b/packages/chess-server/README.md index 17378964e..551a63ec8 100644 --- a/packages/chess-server/README.md +++ b/packages/chess-server/README.md @@ -8,11 +8,12 @@ Start a Bitcoin Computer Node in the package `node`. Then copy the `.env.example cp .env.example .env ``` -Setup the environment variables for the chess-app `chess/app/.env` file. +Setup the environment variables for the chess-app `chess/app/.env` file. ``` cp ../chess-app/.env.example ../chess-app/.env ``` + ### Deploy Smart Contract Deploy the smart contract. The script will prompt you to update the `.env` files in this package as well as in the packages `chess-contracts` and `chess-app`. @@ -48,4 +49,4 @@ npm run up ``` npm test -``` \ No newline at end of file +``` diff --git a/packages/chess-server/scripts/deploy.ts b/packages/chess-server/scripts/deploy.ts index 6464e618a..852121271 100644 --- a/packages/chess-server/scripts/deploy.ts +++ b/packages/chess-server/scripts/deploy.ts @@ -54,7 +54,7 @@ Update the following rows in your .env file. VITE_CHESS_GAME_MOD_SPEC\x1b[2m=${mod}\x1b[0m `) } else { - const files = ['../chess-app/.env', '../chess-server/.env', '../chess-contracts/.env'] + const files = ['../chess-app/.env', '../chess-server/.env'] for (const file of files) { // Update module specifiers in the .env file diff --git a/packages/components/built/Loader.js b/packages/components/built/Loader.js index a7d7be82e..5aef60d10 100644 --- a/packages/components/built/Loader.js +++ b/packages/components/built/Loader.js @@ -1,4 +1,4 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; export function Loader() { - return (_jsx("div", { className: "grid place-items-center h-screen w-full top-0 left-0 fixed", children: _jsxs("svg", { "aria-hidden": "true", className: "mr-2 w-12 h-12 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600", viewBox: "0 0 100 101", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", fill: "currentColor" }), _jsx("path", { d: "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", fill: "currentFill" })] }) })); + return (_jsx("div", { className: "grid place-items-center h-screen w-full top-0 left-0 fixed z-50", children: _jsxs("svg", { "aria-hidden": "true", className: "mr-2 w-12 h-12 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600", viewBox: "0 0 100 101", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", fill: "currentColor" }), _jsx("path", { d: "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", fill: "currentFill" })] }) })); } diff --git a/packages/components/built/Modal.d.ts b/packages/components/built/Modal.d.ts index dcad1ac4a..af859b8b8 100644 --- a/packages/components/built/Modal.d.ts +++ b/packages/components/built/Modal.d.ts @@ -2,10 +2,16 @@ import { Modal as ModalClass } from 'flowbite'; export declare const Modal: { get: (id: string) => ModalClass; showModal: (id: string) => void; - hideModal: (id: string) => void; + hideModal: (id: string, onClickClose?: () => void) => void; toggleModal: (id: string) => void; ShowButton: ({ id, text }: any) => import("react/jsx-runtime").JSX.Element; HideButton: ({ id, text }: any) => import("react/jsx-runtime").JSX.Element; ToggleButton: ({ id, text }: any) => import("react/jsx-runtime").JSX.Element; - Component: ({ title, content, contentData, id }: any) => import("react/jsx-runtime").JSX.Element; + Component: ({ title, content, contentData, id, onClickClose, }: { + title: string; + content: any; + id: string; + contentData?: any; + onClickClose?: () => void; + }) => import("react/jsx-runtime").JSX.Element; }; diff --git a/packages/components/built/Modal.js b/packages/components/built/Modal.js index 1a97bdce7..1936f3b4f 100644 --- a/packages/components/built/Modal.js +++ b/packages/components/built/Modal.js @@ -9,8 +9,11 @@ var get = function (id) { var showModal = function (id) { get(id).show(); }; -var hideModal = function (id) { +var hideModal = function (id, onClickClose) { get(id).hide(); + if (onClickClose) { + onClickClose(); + } }; var toggleModal = function (id) { get(id).toggle(); @@ -28,8 +31,8 @@ var ToggleButton = function (_a) { return (_jsx("button", { "data-modal-target": id, "data-modal-toggle": id, type: "button", children: text })); }; var Component = function (_a) { - var title = _a.title, content = _a.content, contentData = _a.contentData, id = _a.id; - return (_jsx("div", { id: id, tabIndex: -1, "aria-hidden": "true", className: "hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full", children: _jsx("div", { className: "relative p-4 w-full max-w-sm max-h-full", children: _jsxs("div", { className: "relative bg-white rounded-lg shadow dark:bg-gray-700", children: [_jsxs("div", { className: "flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600", children: [_jsx("h3", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: title }), _jsxs("button", { type: "button", className: "text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white", "data-modal-hide": id, "data-modal-target": id, onClick: function () { return hideModal(id); }, children: [_jsx("svg", { className: "w-3 h-3", "aria-hidden": "true", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 14 14", children: _jsx("path", { stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" }) }), _jsx("span", { className: "sr-only", children: "Close modal" })] })] }), content(contentData)] }) }) })); + var title = _a.title, content = _a.content, contentData = _a.contentData, id = _a.id, onClickClose = _a.onClickClose; + return (_jsx("div", { id: id, tabIndex: -1, "aria-hidden": "true", style: { zIndex: 45 }, className: "hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full", children: _jsx("div", { className: "relative p-4 w-full max-w-sm max-h-full", children: _jsxs("div", { className: "relative bg-white rounded-lg shadow dark:bg-gray-700", children: [_jsxs("div", { className: "flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600", children: [_jsx("h3", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: title }), _jsxs("button", { type: "button", className: "text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white", "data-modal-hide": id, "data-modal-target": id, onClick: function () { return hideModal(id, onClickClose); }, children: [_jsx("svg", { className: "w-3 h-3", "aria-hidden": "true", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 14 14", children: _jsx("path", { stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" }) }), _jsx("span", { className: "sr-only", children: "Close modal" })] })] }), content(contentData)] }) }) })); }; export var Modal = { get: get, diff --git a/packages/components/built/Wallet.js b/packages/components/built/Wallet.js index 9c5ed81bd..8664f8393 100644 --- a/packages/components/built/Wallet.js +++ b/packages/components/built/Wallet.js @@ -37,6 +37,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) { import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useCallback, useContext, useEffect, useState } from 'react'; import { HiRefresh } from 'react-icons/hi'; +import { FiCopy, FiCheck } from 'react-icons/fi'; import { Auth } from './Auth'; import { Drawer } from './Drawer'; import { UtilsContext } from './UtilsContext'; @@ -120,11 +121,23 @@ var Balance = function (_a) { }; var Address = function (_a) { var computer = _a.computer; - return (_jsxs("div", { className: "mb-4", children: [_jsx("h6", { className: "text-lg font-bold dark:text-white", children: "Address" }), _jsx("p", { className: "mb-4 font-mono text-xs text-gray-500 dark:text-gray-400", children: computer.getAddress() })] })); + var _b = useState(false), copied = _b[0], setCopied = _b[1]; + var handleCopy = function () { + navigator.clipboard.writeText(computer.getAddress()); + setCopied(true); + setTimeout(function () { return setCopied(false); }, 2000); // Reset icon color after 2 seconds + }; + return (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "flex items-center", children: [_jsx("h6", { className: "text-lg font-bold dark:text-white", children: "Address" }), _jsx("button", { onClick: handleCopy, className: "ml-1 p-1 text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white", "aria-label": "Copy address", children: copied ? (_jsx(FiCheck, { className: "w-4 h-4 text-green-500 dark:text-green-400" })) : (_jsx(FiCopy, { className: "w-4 h-4" })) })] }), _jsx("p", { className: "mb-4 font-mono text-xs text-gray-500 dark:text-gray-400", children: computer.getAddress() })] })); }; var PublicKey = function (_a) { var computer = _a.computer; - return (_jsxs("div", { className: "mb-4", children: [_jsx("h6", { className: "text-lg font-bold dark:text-white", children: "Public Key" }), _jsx("p", { className: "mb-4 text-xs font-mono text-gray-500 dark:text-gray-400 break-words", children: computer.getPublicKey() })] })); + var _b = useState(false), copied = _b[0], setCopied = _b[1]; + var handleCopy = function () { + navigator.clipboard.writeText(computer.getPublicKey()); + setCopied(true); + setTimeout(function () { return setCopied(false); }, 2000); // Reset icon color after 2 seconds + }; + return (_jsxs("div", { className: "mb-4", children: [_jsxs("div", { className: "flex items-center", children: [_jsx("h6", { className: "text-lg font-bold dark:text-white", children: "Public Key" }), _jsx("button", { onClick: handleCopy, className: "ml-1 p-1 text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white", "aria-label": "Copy public key", children: copied ? (_jsx(FiCheck, { className: "w-4 h-4 text-green-500 dark:text-green-400" })) : (_jsx(FiCopy, { className: "w-4 h-4" })) })] }), _jsx("p", { className: "mb-4 text-xs font-mono text-gray-500 dark:text-gray-400 break-words", children: computer.getPublicKey() })] })); }; var Mnemonic = function (_a) { var computer = _a.computer; diff --git a/packages/components/src/Loader.tsx b/packages/components/src/Loader.tsx index 4063c52f5..7a065558e 100644 --- a/packages/components/src/Loader.tsx +++ b/packages/components/src/Loader.tsx @@ -1,6 +1,6 @@ export function Loader() { return ( -
    +