diff --git a/bun.lockb b/bun.lockb index 7971d2d..3e290cb 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 73ad9c4..65cb6cd 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ "preview": "vite preview" }, "dependencies": { - "@cartridge/connector": "^0.3.38", + "@cartridge/connector": "^0.3.40", "@fontsource/vt323": "^5.0.13", - "@starknet-react/core": "^2.9.0", + "buffer": "^6.0.3", + "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -23,6 +24,8 @@ }, "devDependencies": { "@eslint/js": "^9.8.0", + "@types/canvas-confetti": "^1.6.4", + "@types/node": "^22.4.1", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", @@ -35,6 +38,7 @@ "tailwindcss": "^3.4.9", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0", - "vite": "^5.4.0" + "vite": "^5.4.0", + "vite-plugin-mkcert": "^1.17.6" } } diff --git a/public/animations/coin-sprites.png b/public/animations/coin-sprites.png new file mode 100644 index 0000000..b5d755c Binary files /dev/null and b/public/animations/coin-sprites.png differ diff --git a/public/icons/cartridge.svg b/public/icons/cartridge.svg new file mode 100644 index 0000000..1d89f69 --- /dev/null +++ b/public/icons/cartridge.svg @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/public/icons/chevron.svg b/public/icons/chevron.svg new file mode 100644 index 0000000..a64ed90 --- /dev/null +++ b/public/icons/chevron.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/icons/twitter.svg b/public/icons/twitter.svg new file mode 100644 index 0000000..7a1d67e --- /dev/null +++ b/public/icons/twitter.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/scenes/fountain.png b/public/scenes/fountain.png new file mode 100644 index 0000000..406e5bd Binary files /dev/null and b/public/scenes/fountain.png differ diff --git a/public/scenes/opening.png b/public/scenes/opening.png new file mode 100644 index 0000000..155c1dc Binary files /dev/null and b/public/scenes/opening.png differ diff --git a/src/App.tsx b/src/App.tsx index 3bcd177..f825b36 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,105 +1,39 @@ -import React from "react"; -import { Button } from "./components/Button"; -import { useUiSounds, soundSelector } from "./hooks/useUiSound"; -import { useAccount, useConnect, useDisconnect } from "@starknet-react/core"; -import { getWalletConnectors } from "./lib/connectors"; -import { CompleteIcon } from "./components/Icons"; -import { COLLECTIONS_MAP } from "./lib/constants"; -import { displayAddress } from "./lib/utils"; +import { useEffect } from "react"; import Head from "./head"; +import Claim from "./containers/Claim"; +import Claimed from "./containers/Claimed"; +import Claiming from "./containers/Claiming"; +import { useUIStore } from "./hooks/useUIStore"; +import { networkConfig } from "./lib/networkConfig"; +import { fetchAdventurerMetadata } from "./api/fetchMetadata"; +import { Network } from "./lib/types"; const App = () => { - const { play: clickPlay } = useUiSounds(soundSelector.click); - - const { connectors, connect } = useConnect(); - const { disconnect } = useDisconnect(); - const { address } = useAccount(); - - const walletConnectors = getWalletConnectors(connectors); - - const unclaimedCollectionsByOwnerData = [ - { - token: "0x1", - tokenId: 1, - claimed: false, - }, - { - token: "0x1", - tokenId: 2, - claimed: false, - }, - { - token: "0x2", - tokenId: 1, - claimed: false, - }, - ]; - - const getCollectionFreeGames = (token: string) => { - return unclaimedCollectionsByOwnerData.filter( - (item) => item.token === token - ); - }; - - const renderCollection = (token: string, image: string, alt: string) => { - const freeGames = getCollectionFreeGames(token); - return ( -
- - - - - - {alt} - - {freeGames.length > 0 && ( - - {`${freeGames.length} - Game${freeGames.length > 1 ? "s" : ""} - `} - - )} -
- ); - }; - - const collectionsData = [ - { - token: COLLECTIONS_MAP["Duck"], - alt: "Duck", - image: "/Duck.png", - }, - { - token: COLLECTIONS_MAP["Blobert"], - alt: "Blobert", - image: "/Blobert.png", - }, - { - token: COLLECTIONS_MAP["Everai"], - alt: "Everai", - image: "/Everai.png", - }, - { - token: COLLECTIONS_MAP["Pixel Banners"], - alt: "Pixel Banners", - image: "/Pixel-Banners.png", - }, - { - token: COLLECTIONS_MAP["StarkID"], - alt: "StarkID", - image: "/StarkID.png", - }, - { - token: COLLECTIONS_MAP["Realms"], - alt: "Realms", - image: "/Realms.png", - }, - { - token: COLLECTIONS_MAP["Open Division"], - alt: "Open Division", - image: "/Open-Division.png", - }, - ]; + const { claimed, claiming, setAdventurersMetadata, setClaiming, setClaimed } = + useUIStore(); + + const adventurers = [99, 100, 101, 102, 103, 106]; + const network: Network = import.meta.env.VITE_NETWORK; + + useEffect(() => { + if (claiming) { + const fetchImages = async () => { + const adventurersMetadata = await Promise.all( + adventurers.map((adventurer) => + fetchAdventurerMetadata( + networkConfig[network!].gameAddress, + adventurer, + networkConfig[network!].rpcUrl + ) + ) + ); + setAdventurersMetadata(adventurersMetadata); + setClaiming(false); + setClaimed(true); + }; + fetchImages(); + } + }, [claiming]); return ( @@ -113,82 +47,8 @@ const App = () => { alt="crt green mask" className="absolute w-full pointer-events-none crt-frame hidden sm:block" /> -
-
-

- Mainnet Tournament Claim -

-
-
-

Collections

- {!address && ( -

- {displayAddress(address ?? "")} -

- )} -
-
- {collectionsData.map((collection) => - renderCollection( - collection.token, - collection.image, - collection.alt - ) - )} -
-
- {!address ? ( - <> -

Check Eligibility

-
- {walletConnectors.map((connector, index) => ( - - ))} -
-
- Claim Games on Desktop -
- - ) : ( -
-

- 10 Free Games Claimable -

- - -
- )} -
-
-
-
+ {claimed ? : } + {claiming && } ); diff --git a/src/api/fetchMetadata.ts b/src/api/fetchMetadata.ts new file mode 100644 index 0000000..551d8be --- /dev/null +++ b/src/api/fetchMetadata.ts @@ -0,0 +1,53 @@ +import { Buffer } from "buffer"; + +export const fetchAdventurerMetadata = async ( + gameAddress: string, + tokenId: number, + rpcUrl: string +) => { + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "starknet_call", + params: [ + { + contract_address: gameAddress, + entry_point_selector: + "0x0226ad7e84c1fe08eb4c525ed93cccadf9517670341304571e66f7c4f95cbe54", // token_uri + calldata: [tokenId.toString(16), "0x0"], + }, + "pending", + ], + id: 0, + }), + }); + + const data = await response.json(); + + if (response.ok) { + console.log("Beast fetched successfully"); + } else { + console.error("Error in response:", data); + } + + // Step 1: Convert hex strings to a single string + const hexString = data.result.slice(1).join("").slice(2); // Remove '0x' prefix + + // Step 2: Convert hex to ASCII + const fullString = hexString + .match(/.{1,2}/g)! + .map((hex: any) => String.fromCharCode(parseInt(hex, 16))) + .join(""); + + const encodedString = fullString.split(",")[1]; + + const decodedString = Buffer.from(encodedString, "base64").toString("utf-8"); + + const jsonData = JSON.parse(decodedString); + + return jsonData; +}; diff --git a/src/components/AdventurerCard.tsx b/src/components/AdventurerCard.tsx new file mode 100644 index 0000000..874b21a --- /dev/null +++ b/src/components/AdventurerCard.tsx @@ -0,0 +1,107 @@ +import { useState } from "react"; +import { Button } from "./Button"; + +export interface AdventurerCardProps { + meta: any; +} + +const AdventurerCard = ({ meta }: AdventurerCardProps) => { + const [isRevealing, setIsRevealing] = useState(false); + const [revealedStats, setRevealedStats] = useState({ + str: "?", + dex: "?", + int: "?", + vit: "?", + wis: "?", + cha: "?", + }); + + const statValues: Record = { + str: meta.attributes.find((attr: any) => attr.trait === "Strength").value, + dex: meta.attributes.find((attr: any) => attr.trait === "Dexterity").value, + int: meta.attributes.find((attr: any) => attr.trait === "Intelligence") + .value, + vit: meta.attributes.find((attr: any) => attr.trait === "Vitality").value, + wis: meta.attributes.find((attr: any) => attr.trait === "Wisdom").value, + cha: meta.attributes.find((attr: any) => attr.trait === "Charisma").value, + }; + + const revealStats = () => { + setIsRevealing(true); + const stats = ["str", "dex", "int", "vit", "wis", "cha"]; + + // Animate random numbers + const animationInterval = setInterval(() => { + const randomStats = Object.fromEntries( + stats.map((stat) => [stat, Math.floor(Math.random() * 10).toString()]) + ); + setRevealedStats(randomStats as typeof revealedStats); + }, 100); + + // Set final values after 1 second + setTimeout(() => { + clearInterval(animationInterval); + setRevealedStats(statValues as typeof revealedStats); + setIsRevealing(false); + }, 4000); + }; + + const colorMap = (stat: number) => { + if (isRevealing) return ""; + if (stat <= 0) return "bg-red-900"; + if (stat <= 2) return "bg-terminal-yellow-50"; + if (stat <= 4) return "bg-terminal-green-50"; + if (stat <= 9) return "bg-terminal-green text-terminal-black"; + return "bg-terminal-green text-terminal-black"; + }; + + return ( +
+ {(Object.values(revealedStats).every((stat) => stat === "?") || + isRevealing) && ( + <> + + {isRevealing ? ( +
+ Revealing +
+ ) : ( + + )} + + )} +
+ {["str", "dex", "int", "vit", "wis", "cha"].map((stat) => ( + +

{stat.toUpperCase()}

+

{revealedStats[stat as keyof typeof revealedStats]}

+
+ ))} +
+ {meta.name} +
+ ); +}; + +export default AdventurerCard; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 22effa7..ee1c6cd 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { VariantProps, cva } from "class-variance-authority"; import { cn } from "../lib/utils"; -import { soundSelector, useUiSounds } from "../hooks/useUiSound"; +import { soundSelector, useUiSounds } from "../hooks/useUISound"; const buttonVariants = cva( "active:scale-95 inline-flex items-center justify-center font-medium transition-colors focus:outline-none focus:ring-offset-2 disabled:bg-terminal-black disabled:text-terminal-green disabled:pointer-events-none data-[state=open]:bg-slate-100 uppercase font-sans-serif border border-transparent disabled:text-slate-600", diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index f251dc2..7112281 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -12,3 +12,133 @@ export const CompleteIcon: React.FC = () => ( /> ); + +export const TwitterIcon: React.FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const ChevronIcon: React.FC = () => ( + + + +); + +export const CartridgeIcon: React.FC = () => ( + + + + +); diff --git a/src/components/TwitterShareButton.tsx b/src/components/TwitterShareButton.tsx new file mode 100644 index 0000000..3c95493 --- /dev/null +++ b/src/components/TwitterShareButton.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Button } from "./Button"; +import { TwitterIcon } from "./Icons"; + +interface Props { + text: string; + className?: string; +} + +const TwitterShareButton: React.FC = ({ text, className }) => { + const tweetUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent( + text + )}`; + + return ( + + ); +}; + +export default TwitterShareButton; diff --git a/src/components/animations/ClaimingLoader.tsx b/src/components/animations/ClaimingLoader.tsx new file mode 100644 index 0000000..3715598 --- /dev/null +++ b/src/components/animations/ClaimingLoader.tsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; + +interface ClaimingLoaderProps { + loadingSeconds: number | null; +} + +const ClaimingLoader = ({ loadingSeconds }: ClaimingLoaderProps) => { + const [progress, setProgress] = useState(0); + + useEffect(() => { + if (loadingSeconds) { + const interval = setInterval(() => { + setProgress((prevProgress) => { + const newProgress = prevProgress + 100 / loadingSeconds / 10; + return newProgress >= 100 ? 100 : newProgress; + }); + }, 100); + + return () => clearInterval(interval); + } + }, [loadingSeconds]); + + return ( +
+
+
+
+
+ ); +}; + +export default ClaimingLoader; diff --git a/src/components/animations/Confetti.tsx b/src/components/animations/Confetti.tsx new file mode 100644 index 0000000..ab022e0 --- /dev/null +++ b/src/components/animations/Confetti.tsx @@ -0,0 +1,47 @@ +import React, { useEffect } from "react"; +import confetti from "canvas-confetti"; + +const Confetti: React.FC = () => { + useEffect(() => { + const duration = 1000; + const animationEnd = Date.now() + duration; + + const randomInRange = (min: number, max: number) => { + return Math.random() * (max - min) + min; + }; + + // Create a custom shape for pixelated confetti + const pixelShape = confetti.shapeFromPath({ + path: "M0 0 L1 0 L1 1 L0 1 Z", + matrix: new DOMMatrix([1, 0, 0, 1, 0, 0]), + }); + + const interval = setInterval(() => { + const timeLeft = animationEnd - Date.now(); + + if (timeLeft <= 0) { + return clearInterval(interval); + } + + confetti({ + particleCount: 50, + spread: 360, + origin: { y: 0.5 }, + colors: ["#00FF00"], // Bright green color + shapes: [pixelShape], + scalar: 1, // Increase size for more visible pixels + drift: 0, // Remove drift for a more "digital" look + gravity: 0.3, // Reduce gravity for slower fall + ticks: 400, // Increase ticks for longer-lasting particles + startVelocity: randomInRange(15, 30), + decay: 0.95, // Slower decay for longer-lasting effect + }); + }, 200); + + return () => clearInterval(interval); + }, []); + + return null; +}; + +export default Confetti; diff --git a/src/components/animations/SpriteAnimation.tsx b/src/components/animations/SpriteAnimation.tsx new file mode 100644 index 0000000..a7aec89 --- /dev/null +++ b/src/components/animations/SpriteAnimation.tsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from "react"; + +interface AnimationInfo { + name: string; + startFrame: number; + frameCount: number; +} + +interface SpriteAnimationProps { + frameWidth: number; + frameHeight: number; + columns: number; + rows: number; + frameRate: number; + className: string; + animations?: AnimationInfo[]; + currentAnimation?: string; + adjustment?: number; +} + +const SpriteAnimation: React.FC = ({ + frameWidth, + frameHeight, + columns, + rows, + frameRate, + animations, + currentAnimation, + adjustment, + className, +}) => { + const [frame, setFrame] = useState(0); + + const animationInfo = animations + ? animations.find((animation) => animation.name === currentAnimation) + : null; + + useEffect(() => { + if (animationInfo) { + const interval = setInterval(() => { + setFrame((prevFrame) => (prevFrame + 1) % animationInfo.frameCount); + }, 1000 / frameRate); + + return () => clearInterval(interval); + } else { + const interval = setInterval(() => { + setFrame((prevFrame) => (prevFrame + 1) % (columns * rows)); + }, 1000 / frameRate); + + return () => clearInterval(interval); + } + }, [frameRate, animationInfo]); + + const frameIndex = animationInfo ? animationInfo.startFrame + frame : frame; + const bgPositionX = -(frameIndex % columns) * frameWidth; + const bgPositionY = -Math.floor(frameIndex / columns) * frameHeight; + + return ( +
+ ); +}; + +export default SpriteAnimation; diff --git a/src/components/animations/TokenLoader.tsx b/src/components/animations/TokenLoader.tsx new file mode 100644 index 0000000..69467fa --- /dev/null +++ b/src/components/animations/TokenLoader.tsx @@ -0,0 +1,16 @@ +import SpriteAnimation from "./SpriteAnimation"; + +export default function TokenLoader() { + return ( +
+ +
+ ); +} diff --git a/src/containers/Claim.tsx b/src/containers/Claim.tsx new file mode 100644 index 0000000..e5df51e --- /dev/null +++ b/src/containers/Claim.tsx @@ -0,0 +1,234 @@ +import { useEffect, useState } from "react"; +import { Button } from "../components/Button"; +import { useUiSounds, soundSelector } from "../hooks/useUISound"; +import { useAccount, useConnect, useDisconnect } from "@starknet-react/core"; +import { getWalletConnectors } from "../lib/connectors"; +import { CartridgeIcon, CompleteIcon } from "../components/Icons"; +import { COLLECTIONS_MAP } from "../lib/constants"; +import { displayAddress } from "../lib/utils"; +import useSyscalls from "../hooks/useSyscalls"; +import { useUIStore } from "../hooks/useUIStore"; +// import { networkConfig } from "../lib/networkConfig"; +// import { Network } from "../lib/types"; + +const Claim = () => { + const [delegateAccount, setDelegateAccount] = useState(""); + const { play: clickPlay } = useUiSounds(soundSelector.click); + const { setClaiming } = useUIStore(); + + const { connectors, connect, connector } = useConnect(); + const { disconnect } = useDisconnect(); + const { account, address } = useAccount(); + // const network: Network = import.meta.env.VITE_NETWORK; + const { executeSetDelegate } = useSyscalls(); + + const walletConnectors = getWalletConnectors(connectors); + + const unclaimedCollectionsByOwnerData = [ + { + token: "0x1", + tokenId: 1, + claimed: false, + }, + { + token: "0x1", + tokenId: 2, + claimed: false, + }, + { + token: "0x2", + tokenId: 1, + claimed: false, + }, + { + token: "0x5", + tokenId: 2, + claimed: false, + }, + ]; + + const getCollectionFreeGames = (token: string) => { + return unclaimedCollectionsByOwnerData.filter( + (item) => item.token === token + ); + }; + + const renderCollection = (token: string, image: string, alt: string) => { + const freeGames = getCollectionFreeGames(token); + return ( +
+ {address && ( + <> + + {freeGames.length > 0 && ( + + + + )} + + )} + + {alt} + + {address && freeGames.length > 0 && ( + + {`${freeGames.length} + Game${freeGames.length > 1 ? "s" : ""} + `} + + )} +
+ ); + }; + + const handleCartridgeOnboarding = () => { + clickPlay(); + setDelegateAccount(address!); + const cartridgeConnector = connectors.find( + (connector) => connector.id === "cartridge" + ); + if (cartridgeConnector) { + connect({ connector: cartridgeConnector }); + } + }; + + useEffect(() => { + if (connector?.id === "cartridge") { + try { + executeSetDelegate(delegateAccount); + // executeClaim(networkConfig[network!].gameAddress, 1); + setClaiming(true); + } catch (error) { + console.log(error); + } + } + }, [delegateAccount, connector, account]); + + const collectionsData = [ + { + token: COLLECTIONS_MAP["Duck"], + alt: "Duck", + image: "/Duck.png", + }, + { + token: COLLECTIONS_MAP["Blobert"], + alt: "Blobert", + image: "/Blobert.png", + }, + { + token: COLLECTIONS_MAP["Everai"], + alt: "Everai", + image: "/Everai.png", + }, + { + token: COLLECTIONS_MAP["Pixel Banners"], + alt: "Pixel Banners", + image: "/Pixel-Banners.png", + }, + { + token: COLLECTIONS_MAP["StarkID"], + alt: "StarkID", + image: "/StarkID.png", + }, + { + token: COLLECTIONS_MAP["Realms"], + alt: "Realms", + image: "/Realms.png", + }, + { + token: COLLECTIONS_MAP["Open Division"], + alt: "Open Division", + image: "/Open-Division.png", + }, + ]; + + return ( +
+
+

+ Mainnet Tournament Claim +

+
+
+

Collections

+ {address && ( +
+

+ {displayAddress(address ?? "")} +

+ +
+ )} +
+
+ {collectionsData.map((collection) => + renderCollection( + collection.token, + collection.image, + collection.alt + ) + )} +
+
+ {!address ? ( + <> +

Check Eligibility

+
+ {walletConnectors.map((connector, index) => ( + + ))} +
+
+ Claim Games on Desktop +
+ + ) : ( +
+

+ {unclaimedCollectionsByOwnerData.length} Free Games Claimable +

+ +
+ )} +
+
+
+
+ ); +}; + +export default Claim; diff --git a/src/containers/Claimed.tsx b/src/containers/Claimed.tsx new file mode 100644 index 0000000..277a1be --- /dev/null +++ b/src/containers/Claimed.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import { Button } from "../components/Button"; +import { ChevronIcon } from "../components/Icons"; +import Confetti from "../components/animations/Confetti"; +import { useUiSounds, soundSelector } from "../hooks/useUISound"; +import AdventurerCard from "../components/AdventurerCard"; +import { useUIStore } from "../hooks/useUIStore"; + +const Claimed = () => { + const { adventurersMetadata } = useUIStore(); + const adventurers = [99, 100, 101, 102, 103, 106]; + const pageSize = 5; + const [currentPage, setCurrentPage] = useState(0); + const totalPages = Math.ceil(adventurers.length / pageSize); + const startIndex = currentPage * pageSize; + const endIndex = startIndex + pageSize; + const currentAdventurers = adventurersMetadata.slice(startIndex, endIndex); + const finalPage = currentPage === totalPages - 1; + + const { play: clickPlay } = useUiSounds(soundSelector.click); + + const handleClick = (newPage: number): void => { + clickPlay(); + setCurrentPage(newPage); + }; + + return ( +
+ +
+

+ Claimed Free Games +

+

20 Claimed

+
+
+ {currentPage > 0 && ( + handleClick(currentPage - 1)} + > + + + )} + {currentAdventurers.map((meta: any, index: any) => ( + + ))} + {!finalPage && ( + handleClick(currentPage + 1)} + > + + + )} +
+
+ + +
+
+ ); +}; + +export default Claimed; diff --git a/src/containers/Claiming.tsx b/src/containers/Claiming.tsx new file mode 100644 index 0000000..672199b --- /dev/null +++ b/src/containers/Claiming.tsx @@ -0,0 +1,37 @@ +import { useState, useEffect } from "react"; +import ClaimingLoader from "../components/animations/ClaimingLoader"; +import TokenLoader from "../components/animations/TokenLoader"; + +const Claiming = () => { + const [loadingMessage, setLoadingMessage] = useState(""); + const messages = [ + "Summoning your adventurer from the void", + "Forging legendary gear", + "Rolling for initiative", + "Generating random dungeons", + "Sharpening pixelated swords", + "Consulting the RNG gods", + "Unleashing chaos", + "Preparing for epic ragequits", + ]; + + useEffect(() => { + const randomIndex = Math.floor(Math.random() * messages.length); + setLoadingMessage(messages[randomIndex]); + }, []); + + return ( +
+

Claiming Your Games

+ +
+

+ {loadingMessage} +

+ +
+
+ ); +}; + +export default Claiming; diff --git a/src/head.tsx b/src/head.tsx index 8fa1b39..e995e3f 100644 --- a/src/head.tsx +++ b/src/head.tsx @@ -1,4 +1,3 @@ -import React from "react"; export default function Head() { return ( <> diff --git a/src/hooks/useSyscalls.ts b/src/hooks/useSyscalls.ts new file mode 100644 index 0000000..40fc46b --- /dev/null +++ b/src/hooks/useSyscalls.ts @@ -0,0 +1,49 @@ +import { useAccount, useConnect } from "@starknet-react/core"; +// import CartridgeConnector from "@cartridge/connector"; + +const useSyscalls = () => { + const { account } = useAccount(); + const { connector } = useConnect(); + + const executeSetDelegate = async (delegateAddress: string) => { + if (!account) { + return; + } + + if (connector?.id !== "cartridge") { + return; + } + + await account + .execute([ + { + contractAddress: account.address, + entrypoint: "set_delegate_account", + calldata: [delegateAddress], + }, + ]) + .catch((e) => console.error(e)); + }; + + const executeClaim = async (gameAddress: string, numberOfGames: number) => { + if (!account) { + return; + } + + if (connector?.id !== "cartridge") { + return; + } + + const calls = Array.from({ length: numberOfGames }, (_, index) => ({ + contractAddress: gameAddress, + entrypoint: "free_game", + calldata: [index], // Example of using the index if needed + })); + + await account.execute(calls).catch((e) => console.error(e)); + }; + + return { executeSetDelegate, executeClaim }; +}; + +export default useSyscalls; diff --git a/src/hooks/useUIStore.ts b/src/hooks/useUIStore.ts index 275fe3f..9c0699f 100644 --- a/src/hooks/useUIStore.ts +++ b/src/hooks/useUIStore.ts @@ -1,18 +1,24 @@ import { create } from "zustand"; - -export type Network = - | "mainnet" - | "katana" - | "sepolia" - | "localKatana" - | undefined; +import { AdventurerMetadata, Network } from "../lib/types"; type State = { network: Network; setNetwork: (network: Network) => void; + claimed: boolean; + setClaimed: (claimed: boolean) => void; + claiming: boolean; + setClaiming: (claiming: boolean) => void; + adventurersMetadata: AdventurerMetadata[]; + setAdventurersMetadata: (metadata: AdventurerMetadata[]) => void; }; export const useUIStore = create((set) => ({ network: undefined, setNetwork: (network) => set({ network }), + claimed: false, + setClaimed: (claimed) => set({ claimed }), + claiming: false, + setClaiming: (claiming) => set({ claiming }), + adventurersMetadata: [], + setAdventurersMetadata: (metadata) => set({ adventurersMetadata: metadata }), })); diff --git a/src/index.css b/src/index.css index 70647d3..5cc1094 100644 --- a/src/index.css +++ b/src/index.css @@ -25,4 +25,39 @@ input[type="text"] { left : 0; width : 100%; height : 100%; +} + +@layer components { + .loading-ellipsis::after { + content : ""; + animation: ellipsis 1s infinite; + } +} + +@keyframes ellipsis { + 0% { + content: ""; + } + + 25% { + content: "."; + } + + 50% { + content: ".."; + } + + 75% { + content: "..."; + } + + 100% { + content: ""; + } +} + +.coin-sprite { + background-image : url("/animations/coin-sprites.png"); + display : inline-block; + background-repeat: no-repeat; } \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..85100c4 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,7 @@ +export type Network = "mainnet" | "sepolia" | undefined; + +export type AdventurerMetadata = { + image: string; + name: string; + description: string; +}; diff --git a/src/main.tsx b/src/main.tsx index 108b0cc..0cad30a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ -import React, { StrictMode } from "react"; +import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { StarknetProvider } from "./provider"; -import { Network } from "./hooks/useUIStore"; +import { Network } from "./lib/types"; import App from "./App.tsx"; import "./index.css"; diff --git a/src/provider.tsx b/src/provider.tsx index 6990744..6addf62 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -8,7 +8,7 @@ import { import { mainnet, sepolia } from "@starknet-react/chains"; import { Chain } from "@starknet-react/chains"; import { networkConfig } from "./lib/networkConfig"; -import { Network } from "./hooks/useUIStore"; +import { Network } from "./lib/types"; import { useInjectedConnectors } from "@starknet-react/core"; import { cartridgeConnector } from "./lib/connectors"; @@ -34,7 +34,6 @@ export function StarknetProvider({ return (