diff --git a/frontend/package.json b/frontend/package.json index fb509566..8b45fba6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.10.1", + "sonner": "^1.4.41", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3ae6e9dd..0b6a27ea 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@aptos-labs/ts-sdk': specifier: ^1.5.1 @@ -46,6 +50,9 @@ dependencies: react-icons: specifier: ^4.10.1 version: 4.10.1(react@18.2.0) + sonner: + specifier: ^1.4.41 + version: 1.4.41(react-dom@18.2.0)(react@18.2.0) tailwind-merge: specifier: ^1.14.0 version: 1.14.0 @@ -249,6 +256,7 @@ packages: /@emotion/memoize@0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true dev: false optional: true @@ -3491,6 +3499,16 @@ packages: engines: {node: '>=8'} dev: true + /sonner@1.4.41(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 93b3f015..b227b2ed 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -4,7 +4,6 @@ *, ::before, ::after { box-sizing: border-box; - word-spacing: -0.25rem; } body { diff --git a/frontend/src/app/home/Connected.tsx b/frontend/src/app/home/Connected.tsx index 4092aa7b..eb8c8936 100644 --- a/frontend/src/app/home/Connected.tsx +++ b/frontend/src/app/home/Connected.tsx @@ -1,45 +1,55 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useEffect, useCallback } from "react"; import { Pet } from "./Pet"; import { useWallet } from "@aptos-labs/wallet-adapter-react"; import { Mint } from "./Mint"; import { getAptosClient } from "@/utils/aptosClient"; import { Modal } from "@/components/Modal"; import { ABI } from "@/utils/abi"; +import { usePet } from "@/context/PetContext"; const TESTNET_ID = "2"; const aptosClient = getAptosClient(); export function Connected() { - const [pet, setPet] = useState(); + const { pet, setPet } = usePet(); const { account, network } = useWallet(); const fetchPet = useCallback(async () => { if (!account?.address) return; - const [hasPet] = await aptosClient.view({ + const hasPet = await aptosClient.view({ payload: { function: `${ABI.address}::main::has_aptogotchi`, functionArguments: [account.address], }, }); - if (hasPet as boolean) { - const response = await aptosClient.view({ - payload: { - function: `${ABI.address}::main::get_aptogotchi`, - functionArguments: [account.address], - }, - }); - const [name, birthday, energyPoints, parts] = response; - const typedParts = parts as { body: number; ear: number; face: number }; - setPet({ - name: name as string, - birthday: birthday as number, - energy_points: energyPoints as number, - parts: typedParts, - }); + + if (hasPet) { + let response; + + try { + response = await aptosClient.view({ + payload: { + function: `${ABI.address}::main::get_aptogotchi`, + functionArguments: [account.address], + }, + }); + + const [name, birthday, energyPoints, parts] = response; + const typedParts = parts as { body: number; ear: number; face: number }; + + setPet({ + name: name as string, + birthday: birthday as number, + energy_points: energyPoints as number, + parts: typedParts, + }); + } catch (error) { + console.error(error); + } } }, [account?.address]); @@ -52,7 +62,7 @@ export function Connected() { return (
{network?.chainId !== TESTNET_ID && } - {pet ? : } + {pet ? : }
); } diff --git a/frontend/src/app/home/NotConnected.tsx b/frontend/src/app/home/NotConnected.tsx index 3e8486bb..ff80f802 100644 --- a/frontend/src/app/home/NotConnected.tsx +++ b/frontend/src/app/home/NotConnected.tsx @@ -16,7 +16,7 @@ export function NotConnected() { return (
-
+

Welcome

{text}

diff --git a/frontend/src/app/home/Pet/Actions.tsx b/frontend/src/app/home/Pet/Actions.tsx index 755da88b..9b2eaa95 100644 --- a/frontend/src/app/home/Pet/Actions.tsx +++ b/frontend/src/app/home/Pet/Actions.tsx @@ -1,8 +1,7 @@ "use client"; -import { Dispatch, SetStateAction, useState } from "react"; +import { useState } from "react"; import { useWallet } from "@aptos-labs/wallet-adapter-react"; -import { Pet } from "."; import { getAptosClient } from "@/utils/aptosClient"; import { NEXT_PUBLIC_ENERGY_CAP, @@ -10,24 +9,21 @@ import { NEXT_PUBLIC_ENERGY_INCREASE, } from "@/utils/env"; import { ABI } from "@/utils/abi"; +import { toast } from "sonner"; +import { usePet } from "@/context/PetContext"; const aptosClient = getAptosClient(); export type PetAction = "feed" | "play"; export interface ActionsProps { - pet: Pet; selectedAction: PetAction; setSelectedAction: (action: PetAction) => void; - setPet: Dispatch>; } -export function Actions({ - selectedAction, - setSelectedAction, - setPet, - pet, -}: ActionsProps) { +export function Actions({ selectedAction, setSelectedAction }: ActionsProps) { + const { pet, setPet } = usePet(); + const [transactionInProgress, setTransactionInProgress] = useState(false); const { account, network, signAndSubmitTransaction } = useWallet(); @@ -75,8 +71,10 @@ export function Actions({ }); } catch (error: any) { console.error(error); + toast.error("Failed to feed your pet. Please try again."); } finally { setTransactionInProgress(false); + toast.success(`Thanks for feeding your pet, ${pet?.name}!`); } }; @@ -111,16 +109,18 @@ export function Actions({ }); } catch (error: any) { console.error(error); + toast.error("Failed to play with your pet. Please try again."); } finally { setTransactionInProgress(false); + toast.success(`Thanks for playing with your pet, ${pet?.name}!`); } }; const feedDisabled = selectedAction === "feed" && - pet.energy_points === Number(NEXT_PUBLIC_ENERGY_CAP); + pet?.energy_points === Number(NEXT_PUBLIC_ENERGY_CAP); const playDisabled = - selectedAction === "play" && pet.energy_points === Number(0); + selectedAction === "play" && pet?.energy_points === Number(0); return (
diff --git a/frontend/src/app/home/Pet/Details.tsx b/frontend/src/app/home/Pet/Details.tsx index 10ad2ecc..42f99ea2 100644 --- a/frontend/src/app/home/Pet/Details.tsx +++ b/frontend/src/app/home/Pet/Details.tsx @@ -1,29 +1,27 @@ "use client"; import { AiFillSave } from "react-icons/ai"; -import { FaCopy } from "react-icons/fa"; +import { FaCopy, FaExternalLinkAlt } from "react-icons/fa"; import { HealthBar } from "@/components/HealthBar"; -import { Pet } from "."; -import { Dispatch, SetStateAction, useState } from "react"; +import { useState } from "react"; import { useWallet } from "@aptos-labs/wallet-adapter-react"; import { getAptosClient } from "@/utils/aptosClient"; import { ABI } from "@/utils/abi"; - -export interface PetDetailsProps { - pet: Pet; - setPet: Dispatch>; -} +import { toast } from "sonner"; +import { usePet } from "@/context/PetContext"; const aptosClient = getAptosClient(); -export function PetDetails({ pet, setPet }: PetDetailsProps) { - const [newName, setNewName] = useState(pet.name); +export function PetDetails() { + const { pet, setPet } = usePet(); + + const [newName, setNewName] = useState(pet?.name || ""); const { account, network, signAndSubmitTransaction } = useWallet(); const owner = account?.ansName ? `${account?.ansName}.apt` : account?.address || ""; - const canSave = newName !== pet.name; + const canSave = newName !== pet?.name; const handleNameChange = async () => { if (!account || !network) return; @@ -45,11 +43,17 @@ export function PetDetails({ pet, setPet }: PetDetailsProps) { }); } catch (error: any) { console.error(error); + toast.error("Failed to update name. Please try again."); + } finally { + toast.success( + `Name was successfully updated from ${pet?.name} to ${newName}!` + ); } }; const handleCopyOwnerAddrOrName = () => { navigator.clipboard.writeText(owner); + toast.success("Owner address copied to clipboard."); }; const nameFieldComponent = ( @@ -76,7 +80,16 @@ export function PetDetails({ pet, setPet }: PetDetailsProps) { const ownerFieldComponent = (
- + + + +
Energy Points
diff --git a/frontend/src/app/home/Pet/Image.tsx b/frontend/src/app/home/Pet/Image.tsx index 9a12da48..7b27ddfa 100644 --- a/frontend/src/app/home/Pet/Image.tsx +++ b/frontend/src/app/home/Pet/Image.tsx @@ -6,12 +6,15 @@ import { PetAction } from "@/app/home/Pet/Actions"; export interface PetImageProps { selectedAction?: PetAction; - petParts: PetParts; + petParts: PetParts | undefined; avatarStyle?: boolean; } export function PetImage(props: PetImageProps) { const { avatarStyle, petParts, selectedAction } = props; + + if (!petParts) return
; + const head = BASE_PATH + "head.png"; const body = BASE_PATH + bodies[petParts.body]; const ear = BASE_PATH + ears[petParts.ear]; diff --git a/frontend/src/app/home/Pet/ShufflePetImage.tsx b/frontend/src/app/home/Pet/ShufflePetImage.tsx index 745838b0..6a6d60ef 100644 --- a/frontend/src/app/home/Pet/ShufflePetImage.tsx +++ b/frontend/src/app/home/Pet/ShufflePetImage.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { Pet, PetParts } from "."; +import { PetParts } from "."; import { PetImage } from "./Image"; import { ShuffleButton } from "@/components/ShuffleButton"; import { diff --git a/frontend/src/app/home/Pet/Summary.tsx b/frontend/src/app/home/Pet/Summary.tsx index 93a92842..c19fbfc0 100644 --- a/frontend/src/app/home/Pet/Summary.tsx +++ b/frontend/src/app/home/Pet/Summary.tsx @@ -1,13 +1,13 @@ "use client"; import { useTypingEffect } from "@/utils/useTypingEffect"; -import { Pet } from "."; +import { usePet } from "@/context/PetContext"; -export interface SummaryProps { - pet: Pet; -} +export function Summary() { + const { pet } = usePet(); + + if (!pet) return null; -export function Summary({ pet }: SummaryProps) { let text = `${pet.name} is doing great! 😄`; if (pet.energy_points >= 8) { diff --git a/frontend/src/app/home/Pet/index.tsx b/frontend/src/app/home/Pet/index.tsx index 7f664916..d6512f34 100644 --- a/frontend/src/app/home/Pet/index.tsx +++ b/frontend/src/app/home/Pet/index.tsx @@ -1,11 +1,12 @@ "use client"; -import { Dispatch, SetStateAction, useState } from "react"; +import { useState } from "react"; import { Actions, PetAction } from "./Actions"; import { PetDetails } from "./Details"; import { PetImage } from "./Image"; import { Summary } from "./Summary"; import { AptogotchiCollection } from "@/components/AptogotchiCollection"; +import { usePet } from "@/context/PetContext"; export interface Pet { name: string; @@ -30,33 +31,28 @@ export const DEFAULT_PET = { }, }; -interface PetProps { - pet: Pet; - setPet: Dispatch>; -} +export function Pet() { + const { pet, setPet } = usePet(); -export function Pet({ pet, setPet }: PetProps) { const [selectedAction, setSelectedAction] = useState("play"); return ( -
-
-
+
+
+
- +
-
+
- +
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index d7ba7ce7..0538cd32 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -4,6 +4,8 @@ import localFont from "next/font/local"; import { PropsWithChildren } from "react"; import { GeoTargetly } from "@/utils/GeoTargetly"; import "nes.css/css/nes.min.css"; +import { Toaster } from "sonner"; +import { PetProvider } from "@/context/PetContext"; import "./globals.css"; const kongtext = localFont({ @@ -38,7 +40,22 @@ export default function RootLayout({ children }: PropsWithChildren) { /> - {children} + + + {children} + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 3dde4a78..10b1dff7 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,44 +1,21 @@ import dynamic from "next/dynamic"; import { Body } from "./home/Body"; -import { PropsWithChildren } from "react"; - -const FixedSizeWrapper = ({ children }: PropsWithChildren) => { - const fixedStyle = { - width: "1200px", - height: "800px", - border: "6px solid", - margin: "auto", - }; - - return ( -
-
{children}
-
- ); -}; export default function Home() { return ( -
- +
+
- -
+
+
); } function Header() { return ( -
-

Aptogotchi

+
+

Aptogotchi

); diff --git a/frontend/src/components/AptogotchiCollection/index.tsx b/frontend/src/components/AptogotchiCollection/index.tsx index 56d62789..89393a49 100644 --- a/frontend/src/components/AptogotchiCollection/index.tsx +++ b/frontend/src/components/AptogotchiCollection/index.tsx @@ -15,7 +15,7 @@ export function AptogotchiCollection() { if (loading || !collection) return null; return ( -
+

{`There are a total of ${collection.current_supply} Aptogotchis in existence.`}

{`Meet your fellow Aptogotchis: ${firstFewAptogotchiName?.join(", ")}${ (firstFewAptogotchiName?.length || 0) < collection.current_supply diff --git a/frontend/src/components/WalletButtons/index.tsx b/frontend/src/components/WalletButtons/index.tsx index 7c8de5d9..d96ee785 100644 --- a/frontend/src/components/WalletButtons/index.tsx +++ b/frontend/src/components/WalletButtons/index.tsx @@ -8,18 +8,30 @@ import { WalletName, } from "@aptos-labs/wallet-adapter-react"; import { cn } from "@/utils/styling"; +import { toast } from "sonner"; -const buttonStyles = "nes-btn is-primary"; +const buttonStyles = "nes-btn is-primary m-auto sm:m-0 sm:px-4"; export const WalletButtons = () => { const { wallets, connected, disconnect, isLoading } = useWallet(); + const onWalletDisconnectRequest = async () => { + try { + disconnect(); + } catch (error) { + console.warn(error); + toast.error("Failed to disconnect wallet. Please try again."); + } finally { + toast.success("Wallet successfully disconnected!"); + } + }; + if (connected) { return ( -

+
Disconnect
@@ -50,7 +62,9 @@ const WalletView = ({ wallet }: { wallet: Wallet }) => { await connect(walletName); } catch (error) { console.warn(error); - window.alert("Failed to connect wallet"); + toast.error("Failed to connect wallet. Please try again."); + } finally { + toast.success("Wallet successfully connected!"); } }; diff --git a/frontend/src/context/PetContext.tsx b/frontend/src/context/PetContext.tsx new file mode 100644 index 00000000..0b85eab9 --- /dev/null +++ b/frontend/src/context/PetContext.tsx @@ -0,0 +1,37 @@ +"use client"; + +import React, { + createContext, + useContext, + useState, + Dispatch, + SetStateAction, +} from "react"; +import { Pet } from "@/app/home/Pet"; + +interface PetContextType { + pet: Pet | undefined; + setPet: Dispatch>; +} + +const PetContext = createContext(undefined); + +export const PetProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [pet, setPet] = useState(undefined); + + return ( + + {children} + + ); +}; + +export const usePet = () => { + const context = useContext(PetContext); + if (!context) { + throw new Error("usePet must be used within a PetProvider"); + } + return context; +};