diff --git a/apps/mobile/.env.example b/apps/mobile/.env.example index 1d7b842e..71e668bf 100644 --- a/apps/mobile/.env.example +++ b/apps/mobile/.env.example @@ -8,3 +8,7 @@ EXPO_NODE_ENV="development" EXPO_PUBLIC_INDEXER_BACKEND_URL="" EXPO_PUBLIC_PIXEL_URL="http://localhost:3000/pixel" +REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS= +REACT_APP_CANVAS_NFT_CONTRACT_ADDRESS= +REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS= +REACT_APP_NODE_ENV= diff --git a/apps/mobile/src/modules/PixelPeace/index.tsx b/apps/mobile/src/modules/PixelPeace/index.tsx index 7034ea90..343c1f0e 100644 --- a/apps/mobile/src/modules/PixelPeace/index.tsx +++ b/apps/mobile/src/modules/PixelPeace/index.tsx @@ -21,7 +21,8 @@ export const PixelPeace: React.FC = () => { {Platform.OS == "web" && process.env.EXPO_PUBLIC_PIXEL_URL && <> diff --git a/apps/website/src/app/components/NavbarPixel.tsx b/apps/website/src/app/components/NavbarPixel.tsx new file mode 100644 index 00000000..385f5271 --- /dev/null +++ b/apps/website/src/app/components/NavbarPixel.tsx @@ -0,0 +1,46 @@ +'use client'; + +import Link from 'next/link'; +import React, {useState} from 'react'; +import {createPortal} from 'react-dom'; + +import {MobileNavBar} from './MobileNavBar'; +import {NavigationLinks} from './NavigationLinks'; + +export function NavbarPixel() { + const [toggleNav, setToggleNav] = useState(false); + return ( +
+
+ + +
AFK
+ +
+ {/* */} + {/*
+ +
*/} + + + {toggleNav && + createPortal(, document.body)} +
+ ); +} diff --git a/apps/website/src/app/pixel/page.tsx b/apps/website/src/app/pixel/page.tsx index d0308dbc..56c8d2f7 100644 --- a/apps/website/src/app/pixel/page.tsx +++ b/apps/website/src/app/pixel/page.tsx @@ -1,17 +1,16 @@ 'use client'; import {AppRender} from 'pixel_ui'; - -import {Footer} from '../components/Footer'; -import {Navbar} from '../components/Navbar'; +import { NavbarPixel } from '../components/NavbarPixel'; export default function Pixel() { return (
- + {/* */} {typeof window !== 'undefined' && } -
); } diff --git a/onchain/solidity_contracts/src/launchpad/LaunchpadPumpDualVM.sol b/onchain/solidity_contracts/src/launchpad/LaunchpadPumpDualVM.sol index 27750c74..df5c1e48 100644 --- a/onchain/solidity_contracts/src/launchpad/LaunchpadPumpDualVM.sol +++ b/onchain/solidity_contracts/src/launchpad/LaunchpadPumpDualVM.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; +import {CairoLib} from "kakarot-lib/CairoLib.sol"; + +using CairoLib for uint256; + contract LaunchpadPumpDualVM { /// @dev The address of the cairo contract to call uint256 immutable starknetLaunchpad; @@ -69,20 +73,57 @@ contract LaunchpadPumpDualVM { function getLaunchPump(uint256 tokenAddress) public { uint256[] memory tokenAddressCalldata = new uint256[](1); - tokenAddressCalldata[0] = uint256(uint160(from)); + tokenAddressCalldata[0] = uint256(uint160(tokenAddress)); uint256 tokenStarknetAddress = - abi.decode(kakarot.staticcallCairo("compute_starknet_address", tokenAddressCalldata), (uint256)); + abi.decode(starknetLaunchpad.staticcallCairo("compute_starknet_address", tokenAddressCalldata), (uint256)); // call launch that sent struct // todo how do it? } - function createToken() public { + + /** */ + function createToken(address recipient, + bytes calldata symbol, + bytes calldata name, + uint256 initialSupply, + bytes calldata contractAddressSalt + ) public { + + uint256[] memory recipientAddressCalldata = new uint256[](1); + recipientAddressCalldata[0] = uint256(uint160(recipient)); + uint256 recipientStarknetAddress = + abi.decode(starknetLaunchpad.staticcallCairo("compute_starknet_address", recipientAddressCalldata), (uint256)); + + uint128 amountLow = uint128(initialSupply); + uint128 amountHigh = uint128(initialSupply >> 128); + + uint256[] memory createTokenCallData = new uint256[](6); + createTokenCallData[0] = recipientStarknetAddress; + // Decode the first 32 bytes (a uint256 is 32 bytes) + uint256 symbolResult = abi.decode(symbol, (uint256)); + uint256 nameResult = abi.decode(name, (uint256)); + uint256 contractAddressSaltResult = abi.decode(contractAddressSalt, (uint256)); + + createTokenCallData[1] = uint(symbolResult); + createTokenCallData[2] = uint(nameResult); + createTokenCallData[3] = uint256(amountLow); + createTokenCallData[4] = uint256(amountHigh); + createTokenCallData[5] = uint256(contractAddressSaltResult); + + starknetLaunchpad.callCairo(FUNCTION_SELECTOR_CREATE_TOKEN, createTokenCallData); } - function createAndLaunchToken() public { + function createAndLaunchToken( + address recipient, + bytes calldata symbol, + bytes calldata name, + uint256 initialSupply, + bytes calldata contractAddressSalt + ) public { + } diff --git a/onchain/solidity_contracts/src/nostr/Namespace.sol b/onchain/solidity_contracts/src/nostr/Namespace.sol index f856e0c1..11ff1a90 100644 --- a/onchain/solidity_contracts/src/nostr/Namespace.sol +++ b/onchain/solidity_contracts/src/nostr/Namespace.sol @@ -1,9 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; + +import {CairoLib} from "kakarot-lib/CairoLib.sol"; + +using CairoLib for uint256; + contract Namespace { - /// @dev The address of the starknet token to call + /// @dev The address of the starknet token to call uint256 immutable namespaceAddress; constructor(uint256 _namespaceAddress) { @@ -18,12 +23,12 @@ contract Namespace { kakarotCallData[0] = uint256(uint160(userAddress)); uint256 userStarknetAddress = - abi.decode(kakarot.staticcallCairo("compute_starknet_address", kakarotCallData), (uint256)); + abi.decode(namespaceAddress.staticcallCairo("compute_starknet_address", kakarotCallData), (uint256)); uint256[] memory addressOfCallData = new uint256[](1); addressOfCallData[0] = userStarknetAddress; - bytes memory returnData = namespaceAddress.staticcallCairo("get_nostr_by_sn_default", balanceOfCallData); - return abi.decode(returnData, (uint256)); + bytes memory returnData = namespaceAddress.staticcallCairo("get_nostr_by_sn_default", addressOfCallData); + // return abi.decode(returnData, (uint256)); } @@ -34,12 +39,12 @@ contract Namespace { kakarotCallData[0] = uint256(uint160(nostrAddress)); uint256 userStarknetAddress = - abi.decode(kakarot.staticcallCairo("compute_starknet_address", kakarotCallData), (uint256)); + abi.decode(namespaceAddress.staticcallCairo("compute_starknet_address", kakarotCallData), (uint256)); uint256[] memory addressOfCallData = new uint256[](1); addressOfCallData[0] = userStarknetAddress; - bytes memory returnData = namespaceAddress.staticcallCairo("get_sn_by_nostr_default", balanceOfCallData); - return abi.decode(returnData, (uint256)); + bytes memory returnData = namespaceAddress.staticcallCairo("get_sn_by_nostr_default", addressOfCallData); + // return abi.decode(returnData, (uint256)); } function linkNostrAddress() public { diff --git a/packages/pixel_ui/index.js b/packages/pixel_ui/index.js index d70cdca6..d1ec4477 100644 --- a/packages/pixel_ui/index.js +++ b/packages/pixel_ui/index.js @@ -1,2 +1,3 @@ -export * from "./src/App" +// export * from "./src/App" +export * from "./src/App.tsx" // export * from "./src" \ No newline at end of file diff --git a/packages/pixel_ui/src/App.js b/packages/pixel_ui/src/App.js index ba194aef..86be9f1d 100644 --- a/packages/pixel_ui/src/App.js +++ b/packages/pixel_ui/src/App.js @@ -23,7 +23,6 @@ import NotificationPanel from './tabs/NotificationPanel.js'; import ModalPanel from './ui/ModalPanel.js'; import Hamburger from './resources/icons/Hamburger.png'; import useMediaQuery from './hooks/useMediaQuery'; -// import { useMediaQuery } from 'react-responsive'; function App() { // Window management diff --git a/packages/pixel_ui/src/App.tsx b/packages/pixel_ui/src/App.tsx new file mode 100644 index 00000000..658424e8 --- /dev/null +++ b/packages/pixel_ui/src/App.tsx @@ -0,0 +1,838 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import useWebSocket, { ReadyState } from 'react-use-websocket'; +import { + useAccount, + useContract, + useNetwork, + useConnect +} from '@starknet-react/core'; +import './App.css'; +import CanvasContainer from './canvas/CanvasContainer.js'; +import PixelSelector from './footer/PixelSelector.js'; +import TabsFooter from './footer/TabsFooter.js'; +import TabPanel from './tabs/TabPanel.js'; +import { usePreventZoom, useLockScroll } from './utils/Window.js'; +import { backendUrl, wsUrl, devnetMode } from './utils/Consts.js'; +import logo from './resources/logo.png'; +import canvasConfig from './configs/canvas.config.json'; +import { fetchWrapper, getTodaysStartTime } from './services/apiService.js'; +import art_peace_abi from './contracts/art_peace.abi.json'; +import username_store_abi from './contracts/username_store.abi.json'; +import canvas_nft_abi from './contracts/canvas_nft.abi.json'; +import NotificationPanel from './tabs/NotificationPanel.js'; +import ModalPanel from './ui/ModalPanel.js'; +import Hamburger from './resources/icons/Hamburger.png'; +import useMediaQuery from './hooks/useMediaQuery.js'; +// import { useMediaQuery } from 'react-responsive'; + +interface IApp { + contractAddress?: string; + canvasAddress?: string; + nftAddress?: string; + factoryAddress?: string; +} + +function App({ contractAddress, canvasAddress, nftAddress, factoryAddress }: IApp) { + // Window management + usePreventZoom(); + const tabs = ['Canvas', 'Factions', 'Quests', 'Vote', 'NFTs', 'Account']; + const [activeTab, setActiveTab] = useState(tabs[0]); + useLockScroll(activeTab === 'Canvas'); + + const isDesktopOrLaptop = useMediaQuery({ + query: '(min-width: 1224px)' + }); + const isBigScreen = useMediaQuery({ query: '(min-width: 1824px)' }); + const isTabletOrMobile = useMediaQuery({ query: '(max-width: 1224px)' }); + const isPortrait = useMediaQuery({ query: '(orientation: portrait)' }); + const isRetina = useMediaQuery({ query: '(min-resolution: 2dppx)' }); + const isMobile = useMediaQuery({ query: '(max-width: 768px)' }); + const isFooterSplit = useMediaQuery({ query: '(max-width: 52rem)' }); + // TODO: height checks ? + // TODO: Animate logo exit on mobile + + const [footerExpanded, setFooterExpanded] = useState(false); + const [modal, setModal] = useState(null); + + const getDeviceTypeInfo = () => { + return { + isDesktopOrLaptop: isDesktopOrLaptop, + isBigScreen: isBigScreen, + isTabletOrMobile: isTabletOrMobile, + isPortrait: isPortrait, + isRetina: isRetina, + isMobile: isMobile + }; + }; + + // Starknet wallet + const { account, address } = useAccount(); + const { chain } = useNetwork(); + const [queryAddress, setQueryAddress] = useState('0'); + const [connected, setConnected] = useState(false); // TODO: change to only devnet + useEffect(() => { + if (devnetMode) { + if (connected) { + setQueryAddress( + '0328ced46664355fc4b885ae7011af202313056a7e3d44827fb24c9d3206aaa0' + ); + } else { + setQueryAddress('0'); + } + } else { + if (!address) { + setQueryAddress('0'); + } else { + setQueryAddress(address.slice(2).toLowerCase().padStart(64, '0')); + } + } + }, [address, connected]); + + // Contracts + // TODO: Pull addrs from api? + const { contract: artPeaceContract } = useContract({ + address: process.env.REACT_APP_STARKNET_CONTRACT_ADDRESS, + abi: art_peace_abi + }); + const { contract: usernameContract } = useContract({ + address: process.env.REACT_APP_USERNAME_STORE_CONTRACT_ADDRESS, + abi: username_store_abi + }); + const { contract: canvasNftContract } = useContract({ + address: process.env.REACT_APP_CANVAS_NFT_CONTRACT_ADDRESS, + abi: canvas_nft_abi + }); + + const [currentDay, setCurrentDay] = useState(0); + const [isLastDay, setIsLastDay] = useState(false); + const [gameEnded, setGameEnded] = useState(false); + const [host, setHost] = useState(''); + const [endTimestamp, setEndTimestamp] = useState(0); + useEffect(() => { + const fetchGameData = async () => { + let response = await fetchWrapper('get-game-data'); + if (!response.data) { + return; + } + setCurrentDay(response.data.day); + if (devnetMode) { + const days = 4; + if (response.data.day >= days) { + setGameEnded(true); + } else if (response.data.day === days - 1) { + setIsLastDay(true); + } + } else { + let now = new Date(); + const result = await getTodaysStartTime(); + let dayEnd = new Date(result.data); + dayEnd.setHours(dayEnd.getHours() + 24); + // Now in seconds + let nowInSeconds = Math.floor(now.getTime() / 1000); + let dayEndInSeconds = Math.floor(dayEnd.getTime() / 1000); + if (nowInSeconds >= response.data.endTime) { + setGameEnded(true); + } else if (dayEndInSeconds >= response.data.endTime) { + setIsLastDay(true); + } + } + setHost(response.data.host); + setEndTimestamp(response.data.endTime); + }; + fetchGameData(); + }, []); + + // Websocket + const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(wsUrl, { + share: false, + shouldReconnect: (_e) => true, + reconnectAttempts: 10, + reconnectInterval: (attempt) => Math.min(10000, Math.pow(2, attempt) * 1000) + }); + const [latestMintedTokenId, setLatestMintedTokenId] = useState(null); + + useEffect(() => { + if (readyState === ReadyState.OPEN) { + sendJsonMessage({ + event: 'subscribe', + data: { + channel: 'general' + } + }); + } + }, [readyState]); + + // Colors + const staticColors = canvasConfig.colors; + const [colors, setColors] = useState([]); + + const [notificationMessage, setNotificationMessage] = useState(''); + + const fetchColors = async () => { + try { + let getColorsEndpoint = backendUrl + '/get-colors'; + let response = await fetch(getColorsEndpoint); + let colors = await response.json(); + if (colors.error) { + setColors(staticColors); + console.error(colors.error); + return; + } + if (colors.data) { + setColors(colors.data); + } + } catch (error) { + setColors(staticColors); + console.error(error); + } + }; + useEffect(() => { + fetchColors(); + }, []); + + useEffect(() => { + const processMessage = async (message) => { + if (message) { + // Check the message type and handle accordingly + if (message.messageType === 'colorPixel') { + if (message.color >= colors.length) { + // Get new colors from backend + await fetchColors(); + } + colorPixel(message.position, message.color); + } else if ( + message.messageType === 'nftMinted' && + activeTab === 'NFTs' + ) { + if (message.minter === queryAddress) { + setLatestMintedTokenId(message.token_id); + } + } + } + }; + + processMessage(lastJsonMessage); + }, [lastJsonMessage]); + + // Canvas + const width = canvasConfig.canvas.width; + const height = canvasConfig.canvas.height; + + const canvasRef = useRef(null); + const extraPixelsCanvasRef = useRef(null); + + const colorPixel = (position, color) => { + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + const x = position % width; + const y = Math.floor(position / width); + const colorIdx = color; + const colorHex = `#${colors[colorIdx]}FF`; + context.fillStyle = colorHex; + context.fillRect(x, y, 1, 1); + }; + + // Pixel selection data + const [selectedColorId, setSelectedColorId] = useState(-1); + const [pixelSelectedMode, setPixelSelectedMode] = useState(false); + const [selectedPositionX, setSelectedPositionX] = useState(null); + const [selectedPositionY, setSelectedPositionY] = useState(null); + const [pixelPlacedBy, setPixelPlacedBy] = useState(''); + + const [lastPlacedTime, setLastPlacedTime] = useState(0); + const [basePixelUp, setBasePixelUp] = useState(false); + const [chainFactionPixelsData, setChainFactionPixelsData] = useState([]); + const [chainFactionPixels, setChainFactionPixels] = useState([]); + const [factionPixelsData, setFactionPixelsData] = useState([]); + const [factionPixels, setFactionPixels] = useState([]); + const [extraPixels, setExtraPixels] = useState(0); + const [availablePixels, setAvailablePixels] = useState(0); + const [availablePixelsUsed, setAvailablePixelsUsed] = useState(0); + const [extraPixelsData, setExtraPixelsData] = useState([]); + + const [selectorMode, setSelectorMode] = useState(false); + + const [isEraserMode, setIsEraserMode] = React.useState(false); + const [isExtraDeleteMode, setIsExtraDeleteMode] = React.useState(false); + + useEffect(() => { + const getLastPlacedPixel = `get-last-placed-time?address=${queryAddress}`; + async function fetchGetLastPlacedPixel() { + const response = await fetchWrapper(getLastPlacedPixel); + if (!response.data) { + return; + } + const time = new Date(response.data); + setLastPlacedTime(time?.getTime()); + } + + fetchGetLastPlacedPixel(); + }, [queryAddress]); + + const updateInterval = 1000; // 1 second + // TODO: make this a config + const timeBetweenPlacements = 120000; // 2 minutes + const [basePixelTimer, setBasePixelTimer] = useState('XX:XX'); + useEffect(() => { + const updateBasePixelTimer = () => { + let timeSinceLastPlacement = Date.now() - lastPlacedTime; + let basePixelAvailable = timeSinceLastPlacement > timeBetweenPlacements; + if (basePixelAvailable) { + setBasePixelUp(true); + setBasePixelTimer('00:00'); + clearInterval(interval); + } else { + let secondsTillPlacement = Math.floor( + (timeBetweenPlacements - timeSinceLastPlacement) / 1000 + ); + setBasePixelTimer( + `${Math.floor(secondsTillPlacement / 60)}:${secondsTillPlacement % 60 < 10 ? '0' : ''}${secondsTillPlacement % 60}` + ); + setBasePixelUp(false); + } + }; + const interval = setInterval(() => { + updateBasePixelTimer(); + }, updateInterval); + updateBasePixelTimer(); + return () => clearInterval(interval); + }, [lastPlacedTime]); + + const [chainFactionPixelTimers, setChainFactionPixelTimers] = useState([]); + useEffect(() => { + const updateChainFactionPixelTimers = () => { + let newChainFactionPixelTimers = []; + let newChainFactionPixels = []; + for (let i = 0; i < chainFactionPixelsData.length; i++) { + let memberPixels = chainFactionPixelsData[i].memberPixels; + if (memberPixels !== 0) { + newChainFactionPixelTimers.push('00:00'); + newChainFactionPixels.push(memberPixels); + continue; + } + let lastPlacedTime = new Date(chainFactionPixelsData[i].lastPlacedTime); + let timeSinceLastPlacement = Date.now() - lastPlacedTime?.getTime(); + let chainFactionPixelAvailable = + timeSinceLastPlacement > timeBetweenPlacements; + if (chainFactionPixelAvailable) { + newChainFactionPixelTimers.push('00:00'); + newChainFactionPixels.push(chainFactionPixelsData[i].allocation); + } else { + let secondsTillPlacement = Math.floor( + (timeBetweenPlacements - timeSinceLastPlacement) / 1000 + ); + newChainFactionPixelTimers.push( + `${Math.floor(secondsTillPlacement / 60)}:${secondsTillPlacement % 60 < 10 ? '0' : ''}${secondsTillPlacement % 60}` + ); + newChainFactionPixels.push(0); + } + } + setChainFactionPixelTimers(newChainFactionPixelTimers); + setChainFactionPixels(newChainFactionPixels); + }; + const interval = setInterval(() => { + updateChainFactionPixelTimers(); + }, updateInterval); + updateChainFactionPixelTimers(); + return () => clearInterval(interval); + }, [chainFactionPixelsData]); + + const [factionPixelTimers, setFactionPixelTimers] = useState([]); + useEffect(() => { + const updateFactionPixelTimers = () => { + let newFactionPixelTimers = []; + let newFactionPixels = []; + for (let i = 0; i < factionPixelsData.length; i++) { + let memberPixels = factionPixelsData[i].memberPixels; + if (memberPixels !== 0) { + newFactionPixelTimers.push('00:00'); + newFactionPixels.push(memberPixels); + continue; + } + let lastPlacedTime = new Date(factionPixelsData[i].lastPlacedTime); + let timeSinceLastPlacement = Date.now() - lastPlacedTime?.getTime(); + let factionPixelAvailable = + timeSinceLastPlacement > timeBetweenPlacements; + if (factionPixelAvailable) { + newFactionPixelTimers.push('00:00'); + newFactionPixels.push(factionPixelsData[i].allocation); + } else { + let secondsTillPlacement = Math.floor( + (timeBetweenPlacements - timeSinceLastPlacement) / 1000 + ); + newFactionPixelTimers.push( + `${Math.floor(secondsTillPlacement / 60)}:${secondsTillPlacement % 60 < 10 ? '0' : ''}${secondsTillPlacement % 60}` + ); + newFactionPixels.push(0); + } + } + setFactionPixelTimers(newFactionPixelTimers); + setFactionPixels(newFactionPixels); + }; + const interval = setInterval(() => { + updateFactionPixelTimers(); + }, updateInterval); + updateFactionPixelTimers(); + return () => clearInterval(interval); + }, [factionPixelsData]); + + useEffect(() => { + let totalChainFactionPixels = 0; + for (let i = 0; i < chainFactionPixels.length; i++) { + totalChainFactionPixels += chainFactionPixels[i]; + } + let totalFactionPixels = 0; + for (let i = 0; i < factionPixels.length; i++) { + totalFactionPixels += factionPixels[i]; + } + setAvailablePixels( + (basePixelUp ? 1 : 0) + + totalChainFactionPixels + + totalFactionPixels + + extraPixels + ); + }, [basePixelUp, chainFactionPixels, factionPixels, extraPixels]); + + useEffect(() => { + async function fetchExtraPixelsEndpoint() { + let extraPixelsResponse = await fetchWrapper( + `get-extra-pixels?address=${queryAddress}` + ); + if (!extraPixelsResponse.data) { + setExtraPixels(0); + return; + } + setExtraPixels(extraPixelsResponse.data); + } + fetchExtraPixelsEndpoint(); + + async function fetchChainFactionPixelsEndpoint() { + let chainFactionPixelsResponse = await fetchWrapper( + `get-chain-faction-pixels?address=${queryAddress}` + ); + if (!chainFactionPixelsResponse.data) { + setChainFactionPixelsData([]); + return; + } + setChainFactionPixelsData(chainFactionPixelsResponse.data); + } + fetchChainFactionPixelsEndpoint(); + + async function fetchFactionPixelsEndpoint() { + let factionPixelsResponse = await fetchWrapper( + `get-faction-pixels?address=${queryAddress}` + ); + if (!factionPixelsResponse.data) { + setFactionPixelsData([]); + return; + } + setFactionPixelsData(factionPixelsResponse.data); + } + fetchFactionPixelsEndpoint(); + }, [queryAddress]); + + const clearPixelSelection = () => { + setSelectedColorId(-1); + setSelectedPositionX(null); + setSelectedPositionY(null); + setPixelSelectedMode(false); + setPixelPlacedBy(''); + }; + + const setPixelSelection = (x, y) => { + setSelectedPositionX(x); + setSelectedPositionY(y); + setPixelSelectedMode(true); + // TODO: move http fetch for pixel data here? + }; + + const clearExtraPixels = useCallback(() => { + setAvailablePixelsUsed(0); + setExtraPixelsData([]); + + const canvas = extraPixelsCanvasRef.current; + const context = canvas.getContext('2d'); + context.clearRect(0, 0, width, height); + }, [width, height]); + + const clearExtraPixel = useCallback( + (index) => { + setAvailablePixelsUsed(availablePixelsUsed - 1); + setExtraPixelsData(extraPixelsData.filter((_, i) => i !== index)); + const canvas = extraPixelsCanvasRef.current; + const context = canvas.getContext('2d'); + const pixel = extraPixelsData[index]; + const x = pixel.x; + const y = pixel.y; + context.clearRect(x, y, 1, 1); + }, + [extraPixelsData, availablePixelsUsed] + ); + + const addExtraPixel = useCallback( + (x, y) => { + // Overwrite pixel if already placed + const existingPixelIndex = extraPixelsData.findIndex( + (pixel) => pixel.x === x && pixel.y === y + ); + if (existingPixelIndex !== -1) { + let newExtraPixelsData = [...extraPixelsData]; + newExtraPixelsData[existingPixelIndex].colorId = selectedColorId; + setExtraPixelsData(newExtraPixelsData); + } else { + setAvailablePixelsUsed(availablePixelsUsed + 1); + setExtraPixelsData([ + ...extraPixelsData, + { x: x, y: y, colorId: selectedColorId } + ]); + } + }, + [extraPixelsData, availablePixelsUsed, selectedColorId] + ); + + // Factions + const [chainFaction, setChainFaction] = useState(null); + const [userFactions, setUserFactions] = useState([]); + useEffect(() => { + async function fetchChainFaction() { + let chainFactionResponse = await fetchWrapper( + `get-my-chain-factions?address=${queryAddress}` + ); + if (!chainFactionResponse.data) { + return; + } + if (chainFactionResponse.data.length === 0) { + return; + } + setChainFaction(chainFactionResponse.data[0]); + } + async function fetchUserFactions() { + let userFactionsResponse = await fetchWrapper( + `get-my-factions?address=${queryAddress}` + ); + if (!userFactionsResponse.data) { + return; + } + setUserFactions(userFactionsResponse.data); + } + fetchChainFaction(); + fetchUserFactions(); + }, [queryAddress]); + + // Templates + const [templateOverlayMode, setTemplateOverlayMode] = useState(false); + const [overlayTemplate, setOverlayTemplate] = useState(null); + + const [templateFaction, setTemplateFaction] = useState(null); + const [templateImage, setTemplateImage] = useState(null); + const [templateColorIds, setTemplateColorIds] = useState([]); + const [templateCreationMode, setTemplateCreationMode] = useState(false); + const [templateCreationSelected, setTemplateCreationSelected] = + useState(false); + const [templatePosition, setTemplatePosition] = useState(0); + + // NFTs + const [nftMintingMode, setNftMintingMode] = useState(false); + const [nftSelectionStarted, setNftSelectionStarted] = useState(false); + const [nftSelected, setNftSelected] = useState(false); + const [nftPosition, setNftPosition] = useState(null); + const [nftWidth, setNftWidth] = useState(null); + const [nftHeight, setNftHeight] = useState(null); + + // Account + const { connect, connectors } = useConnect(); + const connectWallet = async (connector) => { + if (devnetMode) { + setConnected(true); + return; + } + connect({ connector }); + }; + useEffect(() => { + if (devnetMode) return; + if (!connectors) return; + if (connectors.length === 0) return; + + const connectIfReady = async () => { + for (let i = 0; i < connectors.length; i++) { + let ready = await connectors[i].ready(); + if (ready) { + connectWallet(connectors[i]); + break; + } + } + }; + connectIfReady(); + }, [connectors]); + + // Tabs + const [showExtraPixelsPanel, setShowExtraPixelsPanel] = useState(false); + + useEffect(() => { + // TODO: If selecting into other tab, ask to stop selecting? + if (activeTab !== tabs[0] && showExtraPixelsPanel) { + clearExtraPixels(); + setSelectedColorId(-1); + setShowExtraPixelsPanel(false); + return; + } + + if (selectedColorId !== -1) { + if (availablePixels > (basePixelUp ? 1 : 0)) { + setActiveTab(tabs[0]); + setShowExtraPixelsPanel(true); + return; + } else { + setShowExtraPixelsPanel(false); + return; + } + } else { + if (availablePixelsUsed > 0) { + setActiveTab(tabs[0]); + setShowExtraPixelsPanel(true); + return; + } else { + setShowExtraPixelsPanel(false); + return; + } + } + }, [ + activeTab, + selectedColorId, + availablePixels, + availablePixelsUsed, + basePixelUp + ]); + + return ( +
+
+ + {modal && } + + {(!isMobile || activeTab === tabs[0]) && ( + logo + )} +
+ +
+
+
+ {!gameEnded && ( + + )} + {isFooterSplit && !footerExpanded && ( +
{ + setActiveTab(tabs[0]); + setFooterExpanded(!footerExpanded); + }} + > + Tabs +
+ )} + {isFooterSplit && footerExpanded && ( + + )} +
+ {!isFooterSplit && ( + + )} +
+
+
+ ); +} + +export default App; diff --git a/packages/pixel_ui/src/configs/backend.config.json b/packages/pixel_ui/src/configs/backend.config.json index 69a04867..53521fac 100644 --- a/packages/pixel_ui/src/configs/backend.config.json +++ b/packages/pixel_ui/src/configs/backend.config.json @@ -1,6 +1,7 @@ { "host_local": "localhost", - "host": "https://backend-pixel.onrender.com/", + "host_p": "https://backend-pixel.onrender.com/", + "host": "http://localhost:8081", "port": 8082, "scripts": { "place_pixel_devnet": "../tests/integration/local/place_pixel.sh", diff --git a/packages/pixel_ui/src/utils/Consts.js b/packages/pixel_ui/src/utils/Consts.js index ea4c5b22..4777602f 100644 --- a/packages/pixel_ui/src/utils/Consts.js +++ b/packages/pixel_ui/src/utils/Consts.js @@ -1,15 +1,16 @@ import backendConfig from '../configs/backend.config.json'; /** TODO fix url */ -export const backendUrl = 'https://' + backendConfig.host; +// TODO used REACT_APP_NODE_ENV + +// export const backendUrl = 'https://' + backendConfig.host; +export const backendUrl = backendConfig.host; // export const backendUrl = 'https://' + backendConfig.host + ':' + backendConfig.port; // export const backendUrl = backendConfig.production // ? 'https://' + backendConfig.host // : 'http://' + backendConfig.host + ':' + backendConfig.port; - - export const wsUrl = backendConfig.production ? 'wss://' + backendConfig.host + '/ws' : 'ws://' + backendConfig.host + ':' + backendConfig.consumer_port + '/ws'; @@ -22,8 +23,8 @@ export const templateUrl = backendConfig.production ? 'https://' + backendConfig.host : 'http://' + backendConfig.host + ':' + backendConfig.port; +// TODO used REACT_APP_NODE_ENV export const devnetMode = backendConfig.production === false; - export const convertUrl = (url) => { if (!url) { return url; diff --git a/packages/pixel_ui/tsconfig.json b/packages/pixel_ui/tsconfig.json new file mode 100644 index 00000000..f27b54c2 --- /dev/null +++ b/packages/pixel_ui/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "composite": true, + "target": "ES6", + "module": "ES6", + "lib": ["dom", "dom.iterable", "esnext"], + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "declaration": true, + "sourceMap": true, + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + +} \ No newline at end of file