Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { memo } from "react";
import './index.css';
import './App.scss';
import { useApi, useAccount } from '@gear-js/react-hooks';
import { Footer, Header } from 'components/layout';
import { ApiLoader } from 'components/loaders/api-loader';
import { withProviders } from 'app/hocs';
import { Home } from './pages/home';

const Component = () => {
const { isApiReady } = useApi();
const { isAccountReady } = useAccount();
return (
<div className="flex flex-col min-h-screen">
const Component = memo(() => {
const {
isApiReady
} = useApi();
const {
isAccountReady
} = useAccount();
return <div className="flex flex-col min-h-screen">
<Header />
<main className="flex flex-col flex-1">{isApiReady && isAccountReady ? <Home /> : <ApiLoader />}</main>
<Footer />
</div>
);
};

export const App = withProviders(Component);
</div>;
});
export const App = withProviders(Component);
35 changes: 19 additions & 16 deletions frontend/src/app/context/ctx-app.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { useRef } from "react";
import { createContext, ReactNode, useState } from "react";

export const AppCtx = createContext({} as ReturnType<typeof useProgram>);

export const AppCtx = createContext(({} as ReturnType<typeof useProgram>));
const useProgram = () => {
const [isPending, setIsPending] = useState<boolean>(false);
const [isAllowed, setIsAllowed] = useState<boolean>(false);
const [openEmptyPopup, setOpenEmptyPopup] = useState<boolean>(false);
const [openWinnerPopup, setOpenWinnerPopup] = useState<boolean>(false);

const isPending = useRef<boolean>(false);
const isAllowed = useRef<boolean>(false);
const openEmptyPopup = useRef<boolean>(false);
const openWinnerPopup = useRef<boolean>(false);
return {
isPending,
isPending: isPending.current,
setIsPending,
isAllowed,
isAllowed: isAllowed.current,
setIsAllowed,
openEmptyPopup,
openEmptyPopup: openEmptyPopup.current,
setOpenEmptyPopup,
openWinnerPopup,
openWinnerPopup: openWinnerPopup.current,
setOpenWinnerPopup
};
};

export function AppProvider({ children }: { children: ReactNode }) {
const { Provider } = AppCtx;
export function AppProvider({
children
}: {
children: ReactNode;
}) {
const {
Provider
} = AppCtx;
return <Provider value={useProgram()}>{children}</Provider>;
}
}
43 changes: 23 additions & 20 deletions frontend/src/app/context/ctx-game.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import { useRef } from "react";
import { createContext, ReactNode, useState } from "react";
import { DominoTileType, GameWasmStateResponse, IGameState, IPlayer, PlayerChoiceType } from "../types/game";

const useProgram = () => {
const [game, setGame] = useState<IGameState>();
const [gameWasm, setGameWasm] = useState<GameWasmStateResponse>();
const [players, setPlayers] = useState<IPlayer[]>([]);
const [selectedDomino, setSelectedDomino] = useState<[number, DominoTileType]>();
const [playerTiles, setPlayerTiles] = useState<DominoTileType[]>();
const [playerChoice, setPlayerChoice] = useState<PlayerChoiceType>();

const game = useRef<IGameState>();
const gameWasm = useRef<GameWasmStateResponse>();
const players = useRef<IPlayer[]>([]);
const selectedDomino = useRef<[number, DominoTileType]>();
const playerTiles = useRef<DominoTileType[]>();
const playerChoice = useRef<PlayerChoiceType>();
return {
game,
game: game.current,
setGame,
gameWasm,
gameWasm: gameWasm.current,
setGameWasm,
players,
players: players.current,
setPlayers,
playerTiles,
playerTiles: playerTiles.current,
setPlayerTiles,
selectedDomino,
selectedDomino: selectedDomino.current,
setSelectedDomino,
playerChoice,
playerChoice: playerChoice.current,
setPlayerChoice
};
};

export const GameCtx = createContext({} as ReturnType<typeof useProgram>);

export function GameProvider({ children }: { children: ReactNode }) {
const { Provider } = GameCtx;
export const GameCtx = createContext(({} as ReturnType<typeof useProgram>));
export function GameProvider({
children
}: {
children: ReactNode;
}) {
const {
Provider
} = GameCtx;
return <Provider value={useProgram()}>{children}</Provider>;
}
}
123 changes: 64 additions & 59 deletions frontend/src/app/hooks/use-game.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useRef } from "react";
import { useApp, useGame } from 'app/context';
import { useEffect, useState } from 'react';
import { useAccount, useAlert, useApi, useSendMessage } from '@gear-js/react-hooks';
Expand All @@ -9,20 +10,30 @@ import { GameWasmStateResponse, IGameState } from '../types/game';
import { HexString } from '@polkadot/util/types';
import { getStateMetadata, MessagesDispatched } from '@gear-js/api';
import { AnyJson } from '@polkadot/types/types';

export function useInitGame() {
const { setIsAllowed, setOpenWinnerPopup } = useApp();
const { account } = useAccount();
const { setGame, setPlayers, gameWasm } = useGame();
const { state } = useReadState<IGameState>({ programId: ENV.game, meta });

const {
setIsAllowed,
setOpenWinnerPopup
} = useApp();
const {
account
} = useAccount();
const {
setGame,
setPlayers,
gameWasm
} = useGame();
const {
state
} = useReadState<IGameState>({
programId: ENV.game,
meta
});
useEffect(() => {
setGame(state);
if (state && account && state.isStarted && gameWasm) {
setPlayers(state.players);

setIsAllowed(account.decodedAddress === state.players[+state.gameState?.currentPlayer][0]);

if (state.gameState?.state?.winner) {
setOpenWinnerPopup(true);
}
Expand All @@ -33,91 +44,85 @@ export function useInitGame() {
//
}, [state, account, gameWasm]);
}

export function useGameMessage() {
const metadata = useProgramMetadata(meta);
return useSendMessage(ENV.game, metadata, { isMaxGasLimit: true });
return useSendMessage(ENV.game, metadata, {
isMaxGasLimit: true
});
}

export function useWasmState(payload?: AnyJson, isReadOnError?: boolean) {
const { api } = useApi();
const { game } = useGame();
const { setGameWasm, setPlayerTiles } = useGame();
const {
api
} = useApi();
const {
game
} = useGame();
const {
setGameWasm,
setPlayerTiles
} = useGame();
const alert = useAlert();
const [state, setState] = useState<GameWasmStateResponse>();
const [error, setError] = useState('');
const [isStateRead, setIsStateRead] = useState(true);

const state = useRef<GameWasmStateResponse>();
const error = useRef('');
const isStateRead = useRef(true);
const data = useStateMetadata(metaWasm);

const programId: HexString | undefined = ENV.game;
const wasm: Buffer | Uint8Array | undefined = data?.buffer;
const functionName: string | undefined = 'game_state';
const setupReady = !!(programId && wasm && functionName && game?.isStarted);

const resetError = () => setError('');

const resetError = () => error.current = '';
const readWasmState = () => {
if (!setupReady) return;

return getStateMetadata(wasm).then((stateMetadata) =>
api.programState.readUsingWasm({ programId, wasm, fn_name: functionName, argument: payload }, stateMetadata),
);
return getStateMetadata(wasm).then(stateMetadata => api.programState.readUsingWasm({
programId,
wasm,
fn_name: functionName,
argument: payload
}, stateMetadata));
};

const readState = (isInitLoad?: boolean) => {
if (isInitLoad) setIsStateRead(false);

readWasmState()
?.then((codecState) => codecState.toJSON())
.then((result) => {
setState(result as unknown as GameWasmStateResponse);
if (!isReadOnError) setIsStateRead(true);
})
.catch(({ message }: Error) => setError(message))
.finally(() => {
if (isReadOnError) setIsStateRead(true);
});
if (isInitLoad) isStateRead.current = false;
readWasmState()?.then(codecState => codecState.toJSON()).then(result => {
state.current = ((result as unknown) as GameWasmStateResponse);
if (!isReadOnError) isStateRead.current = true;
}).catch(({
message
}: Error) => error.current = message).finally(() => {
if (isReadOnError) isStateRead.current = true;
});
};

useEffect(() => {
if (error) alert.error(error);
if (error.current) alert.error(error.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);

}, [error.current]);
useEffect(() => {
readState(true);
resetError();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [programId, wasm, functionName, game?.isStarted]);

const handleStateChange = ({ data }: MessagesDispatched) => {
const changedIDs = data.stateChanges.toHuman() as HexString[];
const isAnyChange = changedIDs.some((id) => id === programId);

const handleStateChange = ({
data
}: MessagesDispatched) => {
const changedIDs = (data.stateChanges.toHuman() as HexString[]);
const isAnyChange = changedIDs.some(id => id === programId);
if (isAnyChange) readState();
};

useEffect(() => {
if (!setupReady) return;

const unsub = api?.gearEvents.subscribeToGearEvent('MessagesDispatched', handleStateChange);

return () => {
unsub?.then((unsubCallback) => unsubCallback());
unsub?.then(unsubCallback => unsubCallback());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [api, programId, wasm, functionName, game?.isStarted]);

useEffect(() => {
// console.log('wasm state: ', state);
setGameWasm(state);

if (state) {
setPlayerTiles(state.playersTiles[+state.currentPlayer]);
setGameWasm(state.current);
if (state.current) {
setPlayerTiles(state.current.playersTiles[+state.current.currentPlayer]);
} else {
setPlayerTiles(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
}
}, [state.current]);
}
52 changes: 22 additions & 30 deletions frontend/src/app/hooks/use-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,46 @@
import { useRef } from "react";
import { useEffect, useState } from 'react';
import { getProgramMetadata, getStateMetadata, ProgramMetadata, StateMetadata } from '@gear-js/api';
import { Buffer } from 'buffer';
import { useAlert, useReadFullState } from '@gear-js/react-hooks';
import { HexString } from '@polkadot/util/types';

export function useProgramMetadata(source: string) {
const alert = useAlert();

const [metadata, setMetadata] = useState<ProgramMetadata>();

const metadata = useRef<ProgramMetadata>();
useEffect(() => {
fetch(source)
.then((response) => response.text())
.then((raw) => `0x${raw}` as HexString)
.then((metaHex) => getProgramMetadata(metaHex))
.then((result) => setMetadata(result))
.catch(({ message }: Error) => alert.error(message));
fetch(source).then(response => response.text()).then(raw => (`0x${raw}` as HexString)).then(metaHex => getProgramMetadata(metaHex)).then(result => metadata.current = result).catch(({
message
}: Error) => alert.error(message));

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return metadata;
return metadata.current;
}

export function useStateMetadata(source: string) {
const alert = useAlert();

const [data, setData] = useState<{
const data = useRef<{
buffer: Buffer;
meta: StateMetadata;
}>();

useEffect(() => {
fetch(source)
.then((response) => response.arrayBuffer())
.then((arrayBuffer) => Buffer.from(arrayBuffer))
.then(async (buffer) => ({
buffer,
meta: await getStateMetadata(buffer),
}))
.then((result) => setData(result))
.catch(({ message }: Error) => alert.error(message));
fetch(source).then(response => response.arrayBuffer()).then(arrayBuffer => Buffer.from(arrayBuffer)).then(async buffer => ({
buffer,
meta: await getStateMetadata(buffer)
})).then(result => data.current = result).catch(({
message
}: Error) => alert.error(message));

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return data;
return data.current;
}

export function useReadState<T>({ programId, meta }: { programId?: HexString; meta: string }) {
export function useReadState<T>({
programId,
meta
}: {
programId?: HexString;
meta: string;
}) {
const metadata = useProgramMetadata(meta);
return useReadFullState<T>(programId, metadata);
}
}
Loading