diff --git a/.gitignore b/.gitignore index b2d4a128..96cf9015 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Environment variables *.env +.vscode +.vs # MacOS .DS_Store diff --git a/packages/webapp/.vs/slnx.sqlite b/packages/webapp/.vs/slnx.sqlite new file mode 100644 index 00000000..e69de29b diff --git a/packages/webapp/.vscode/settings.json b/packages/webapp/.vscode/settings.json new file mode 100644 index 00000000..255c9f62 --- /dev/null +++ b/packages/webapp/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "javascript.validate.enable": false, + "typescript.validate.enable": false +} \ No newline at end of file diff --git a/packages/webapp/src/actions/errors.ts b/packages/webapp/src/actions/errors.ts index 2492ccc9..e13284bc 100644 --- a/packages/webapp/src/actions/errors.ts +++ b/packages/webapp/src/actions/errors.ts @@ -178,7 +178,7 @@ export function defaultContractWriteErrorHandling(err: ContractWriteError): fals setError({ title: "Contract execution error", - message: `Transaction reverted (${err.args.functionName}) ${signatureMsg}.` + message: `Transaction reverted (${err.args.functionName}) ${signatureMsg}. ` + `Please report to ${GIT_ISSUES}`, buttons: [DISMISS_BUTTON] }) diff --git a/packages/webapp/src/components/collection/cardCollectionDisplay.tsx b/packages/webapp/src/components/collection/cardCollectionDisplay.tsx new file mode 100644 index 00000000..912c8fbf --- /dev/null +++ b/packages/webapp/src/components/collection/cardCollectionDisplay.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import Image from 'next/image' +import { Card } from 'src/store/types' +import { MintDeckModal } from 'src/components/modals/mintDeckModal' +import { testCards } from 'src/utils/card-list' + +interface CardCollectionDisplayProps { + cards: Card[] + isHydrated: boolean + setSelectedCard: (card: Card | null) => void + onCardToggle: (card: Card) => void + selectedCards: Card[] + isEditing: boolean +} + +const CardCollectionDisplay: React.FC = ({ cards, isHydrated, setSelectedCard, selectedCards, onCardToggle, isEditing }) => { + return ( + <> +
+ {isHydrated && cards.length === 0 && ( +
+ +
+ )} + + {isHydrated && cards.length > 0 && ( +
+ {cards.map((card, index) => ( +
c.id === card.id) ? 'shadow-highlight shadow-orange-300' : '' + } hover:bg-slate-800 rounded-lg p-4 border-4 border-slate-900 grow w-[220px] max-w-[330px]`} + onMouseEnter={() => setSelectedCard(card)} + onClick={() => { + if (isEditing) { + onCardToggle(card) + } + }} + > + Number(tc.id) === index + 1)?.image || ""} alt={card.lore.name} width={256} height={256} /> +
{card.lore.name}
+
+
{card.stats.attack}
+
{card.stats.defense}
+
+
+ ))} +
+ )} +
+ + ) +} + +export default CardCollectionDisplay diff --git a/packages/webapp/src/components/collection/deckList.tsx b/packages/webapp/src/components/collection/deckList.tsx new file mode 100644 index 00000000..c4fcf0f6 --- /dev/null +++ b/packages/webapp/src/components/collection/deckList.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import Link from "src/components/link" +import { Deck } from 'src/store/types' +import { Button } from "src/components/ui/button" + +interface DeckCollectionDisplayProps { + decks: Deck[] + onDeckSelect: (deckID: number) => void +} + +const DeckCollectionDisplay: React.FC = ({ decks, onDeckSelect }) => { + return ( +
+ {/* New Deck Button */} +
+ +
+ + {/* Deck Buttons */} + {decks.map((deck, deckID) => ( + + ))} +
+ ) +} + +export default DeckCollectionDisplay \ No newline at end of file diff --git a/packages/webapp/src/components/collection/deckPanel.tsx b/packages/webapp/src/components/collection/deckPanel.tsx new file mode 100644 index 00000000..f9faeac1 --- /dev/null +++ b/packages/webapp/src/components/collection/deckPanel.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react' +import { Deck, Card } from 'src/store/types' +import Image from 'next/image' +import { testCards } from 'src/utils/card-list' +import { Button } from "src/components/ui/button" + +interface DeckConstructionPanelProps { + deck: Deck + selectedCards: Card[] + onCardSelect: (card: Card) => void + onSave: (deck: Deck) => void + onCancel: () => void + } + + + const DeckConstructionPanel : React.FC = ({ deck, selectedCards = [], onCardSelect, onSave, onCancel }) => { + const [ deckName, setDeckName ] = useState(deck.name) + const [ deckNameValid, setIsDeckNameValid ] = useState(false) + + const nameValid = (name: string) => name.trim().length > 0 + + const handleDeckNameChange = (event: React.ChangeEvent) => { + const newName = event.target.value + setDeckName(event.target.value) + setIsDeckNameValid(nameValid(newName)) + } + + const handleSave = () => { + if(!nameValid(deckName)) return + + const newDeck = { + name: deckName.trim(), + cards: selectedCards + } + + onSave(newDeck) + } + + return ( +
+ {/* Deck Name Input */} +
+ +
+ + {/* Save and Cancel Buttons */} +
+ + +
+ + {/* List of Cards in the Deck */} +
+ {selectedCards.length > 0 ? ( + selectedCards.map((card, index) => ( +
onCardSelect(card)} + > +
+ tc.id === Number(card.id))?.image || '/card_art/1.jpg'} alt="Card art" width={40} height={40} className="object-cover rounded-full" /> + {card.lore.name} +
+
+ )) + ) : ( +
+ Click on cards to add them to the deck. +
+ )} +
+
+ ) +} + +export default DeckConstructionPanel \ No newline at end of file diff --git a/packages/webapp/src/components/collection/filterPanel.tsx b/packages/webapp/src/components/collection/filterPanel.tsx new file mode 100644 index 00000000..43b00274 --- /dev/null +++ b/packages/webapp/src/components/collection/filterPanel.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import Image from 'next/image' +import { Card } from 'src/store/types' + +interface FilterPanelProps { + effects: string[] + types: string[] + effectMap: { [key: string]: boolean } + typeMap: { [key: string]: boolean } + handleEffectClick: (index: number) => void + handleTypeClick: (index: number) => void + handleInputChange: (event: React.ChangeEvent) => void + selectedCard: Card | null +} + +const FilterPanel: React.FC = ({ + effects, + types, + effectMap, + typeMap, + handleEffectClick, + handleTypeClick, + handleInputChange, + selectedCard +}) => { + const cardName = selectedCard?.lore.name || "Select a card" + const cardFlavor = selectedCard?.lore.flavor || "Select a card to see its details" + + return ( +
+
+ {/* Search */} +

Search

+
+ +
+ + {/* Effects */} +

Effects

+
+ {effects.map((effect, index) => ( + ) + )} +
+ + {/* Types */} +

Types

+
+ {types.map((type, index) => ( + ) + )} +
+ + {/* todo @eviterin: makes sense to add a filter for the card collection display to only show one of each card. */} + + {/* Selected Card Display */} +
+

Card details

+
+ {cardName} +
{cardName}
+
+
{cardFlavor}
+
+
+
+ ) +} + +export default FilterPanel \ No newline at end of file diff --git a/packages/webapp/src/components/link.tsx b/packages/webapp/src/components/link.tsx new file mode 100644 index 00000000..34f81147 --- /dev/null +++ b/packages/webapp/src/components/link.tsx @@ -0,0 +1,31 @@ +import React from "react" +import { useRouter } from "next/router" +import Link from "next/link" + +interface QueryParamLinkProps { + children: React.ReactNode + href: string +} + +/** + * A Link component wrapper that appends a 'index' query parameter to the URL in development mode. + * This is used to persist state across navigation during testing. + */ +const QueryParamLink : React.FC = ({ children, href }) => { + const router = useRouter() + + let url = href + + if (process.env.NODE_ENV === "development") { + const index = parseInt(router.query.index as string) + if (index !== undefined && !isNaN(index) && 0 <= index && index <= 9) + url += (url.includes("?") ? "&" : "?") + `index=${index}` + } + return ( + + {children} + + ) +} + +export default QueryParamLink \ No newline at end of file diff --git a/packages/webapp/src/components/ui/button.tsx b/packages/webapp/src/components/ui/button.tsx index 9d853c9d..bf9393be 100644 --- a/packages/webapp/src/components/ui/button.tsx +++ b/packages/webapp/src/components/ui/button.tsx @@ -26,6 +26,10 @@ const buttonVariants = cva( lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, + width: { + full: "w-full", + auto: "w-auto", + } }, defaultVariants: { variant: "default", @@ -41,11 +45,11 @@ export interface ButtonProps } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, width, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( diff --git a/packages/webapp/src/constants.ts b/packages/webapp/src/constants.ts index 0b4307f8..d8cd289a 100644 --- a/packages/webapp/src/constants.ts +++ b/packages/webapp/src/constants.ts @@ -4,7 +4,7 @@ * @module constants */ -export const GIT_REPO = "https://github.com/norswap/0xFable" +export const GIT_REPO = "https://github.com/0xFableOrg/0xFable" export const GIT_ISSUES = `${GIT_REPO}/issues` /** Proof generation timeout (in seconds) for the proof of the initial hand. */ diff --git a/packages/webapp/src/pages/_app.tsx b/packages/webapp/src/pages/_app.tsx index b3c7c95d..c5388cbe 100644 --- a/packages/webapp/src/pages/_app.tsx +++ b/packages/webapp/src/pages/_app.tsx @@ -3,23 +3,19 @@ // Must come first, so that can we can hook global members before they're used by imports. import "src/setup" import "src/store/setup" - import { ConnectKitProvider } from "connectkit" import { NextPage } from "next" import type { AppType } from "next/app" import Head from "next/head" import { useAccount, WagmiConfig } from "wagmi" -import { Dispatch, SetStateAction, useState } from "react" import { ensureLocalAccountIndex, wagmiConfig } from "src/chain" import jotaiDebug from "src/components/lib/jotaiDebug" import { GlobalErrorModal } from "src/components/modals/globalErrorModal" import { useIsHydrated } from "src/hooks/useIsHydrated" import { useErrorConfig } from "src/store/hooks" - import "src/styles/globals.css" import { useRouter } from "next/router" import { ComponentType, useEffect } from "react" -import { Deck } from "src/store/types" import { Toaster } from "src/components/ui/sonner" // ================================================================================================= @@ -28,7 +24,7 @@ import { Toaster } from "src/components/ui/sonner" * Make pages in the app conform to this type. * See [@link useIsHydrated] for more info on the meaning of the `isHydrated` prop. */ -export type FablePage = NextPage<{ decks: Deck[], isHydrated: boolean, setDecks: Dispatch> }> +export type FablePage = NextPage<{ isHydrated: boolean }> // ================================================================================================= @@ -77,11 +73,6 @@ const ComponentWrapper = ({ const isHydrated = useIsHydrated() const errorConfig = useErrorConfig() - // todo @eviterin: i've understood it so that decks are stored on chain. thus, below part is not going to be needed. - const _testCards = []; - const [decks, setDecks] = useState([]); - // - if (process.env.NODE_ENV === "development") { // constant // eslint-disable-next-line react-hooks/rules-of-hooks const router = useRouter() @@ -97,12 +88,13 @@ const ComponentWrapper = ({ // will ignore our existence and try to override us with their own account (depending on how // async code scheduling ends up working out). - // To carry the `index` query parameter to other parts of the app, be sure to use the `navigate` - // function from `utils/navigate.ts` instead of `router.push`. + // To carry the `index` query parameter to other parts of the app, be sure to either use: + // - the `navigate` function from `utils/navigate.ts` instead of `router.push`. + // - the `link` component from `components/link.tsx` instead of `next/link` } return <> - + {/* Global error modal for errors that don't have obvious in-flow resolutions. */} {isHydrated && errorConfig && } diff --git a/packages/webapp/src/pages/collection.tsx b/packages/webapp/src/pages/collection.tsx index 01a3fb91..e9463bf7 100644 --- a/packages/webapp/src/pages/collection.tsx +++ b/packages/webapp/src/pages/collection.tsx @@ -1,56 +1,63 @@ import debounce from "lodash/debounce" import Head from "next/head" -// This causes the "Ignoring unsupported entryTypes: largest-contentful-paint.", presumably -// because Firefox does not support some associated features. -import Image from "next/image" -import { useState, useMemo } from "react" + +import React, { useState, useMemo, useEffect } from "react" import { useAccount } from "wagmi" import jotaiDebug from "src/components/lib/jotaiDebug" -import { MintDeckModal } from "src/components/modals/mintDeckModal" import { Navbar } from "src/components/navbar" import { deployment } from "src/deployment" import { useInventoryCardsCollectionGetCollection } from "src/generated" -import { Card } from "src/store/types" +import { Deck, Card } from "src/store/types" import { Address } from "src/chain" import { FablePage } from "src/pages/_app" -import Link from "next/link" import { useRouter } from 'next/router' +import { navigate } from "utils/navigate" + +import FilterPanel from 'src/components/collection/filterPanel' +import CardCollectionDisplay from 'src/components/collection/cardCollectionDisplay' +import DeckList from 'src/components/collection/deckList' +import DeckPanel from 'src/components/collection/deckPanel' + // NOTE(norswap & geniusgarlic): Just an example, when the game actually has effects & types, // fetch those from the chain instead of hardcoding them here. - type Effect = string - const effects: Effect[] = ['Charge', 'Flight', 'Courage', 'Undying', 'Frenzy', 'Enlightened'] const initialEffectMap = Object.assign({}, ...effects.map(name => ({[name]: false}))) const types = ['Creature', 'Magic', 'Weapon'] const initialTypeMap = Object.assign({}, ...types.map(name => ({[name]: false}))) -const Collection: FablePage = ({ decks, isHydrated }) => { +const Collection: FablePage = ({ isHydrated }) => { + const router = useRouter() const { address } = useAccount() - const [ selectedCard, setSelectedCard ] = useState(null) + const [ isEditing, setIsEditing ] = useState(false) + + // Filter Panel / Sorting Panel const [ searchInput, setSearchInput ] = useState('') const [ effectMap, setEffectMap ] = useState(initialEffectMap) const [ typeMap, setTypeMap ] = useState(initialTypeMap) + const [ selectedCard, setSelectedCard ] = useState(null) - const router = useRouter() + // Deck Collection Display + const [ editingDeckIndex, setEditingDeckIndex ] = useState(null) + const [decks, setDecks] = useState([]) - const cardName = selectedCard?.lore.name || "Select a card" - const cardFlavor = selectedCard?.lore.flavor || "Select a card to see its details" + // Deck Construction Panel + const [ currentDeck, setCurrentDeck] = useState(null) + const [ selectedCards, setSelectedCards ] = useState([]) const activeEffects = Object.keys(effectMap).filter(key => effectMap[key]) const activeTypes = Object.keys(typeMap).filter(key => typeMap[key]) - const { data: unfilteredCards, refetch } = useInventoryCardsCollectionGetCollection({ + const { data: unfilteredCards } = useInventoryCardsCollectionGetCollection({ address: deployment.InventoryCardsCollection, args: [address as Address] // TODO not ideal but safe in practice }) as { // make the wagmi type soup understandable, there are many more fields in reality data: readonly Card[], - refetch: () => Promise<{ data?: readonly Card[], error: Error|null }> } const cards: Card[] = (unfilteredCards || []).filter(card => { @@ -79,6 +86,78 @@ const Collection: FablePage = ({ decks, isHydrated }) => { setTypeMap({...typeMap, [type]: !typeMap[type]}) } + const handleDeckSelect = (deckID: number) => { + const selectedDeck = decks[deckID] + setCurrentDeck(selectedDeck) + setEditingDeckIndex(deckID) + setIsEditing(true) + setSelectedCards(selectedDeck.cards) + } + + const handleSaveDeck = (updatedDeck: Deck) => { + const updatedDecks = [...(decks || [])] + if (editingDeckIndex !== null) { + // Update existing deck + updatedDecks[editingDeckIndex] = updatedDeck + } else { + // Add the new deck to the list + updatedDecks.push(updatedDeck) + } + setDecks(updatedDecks) + setIsEditing(false) + setSelectedCards([]) + void navigate(router, '/collection') + } + + const handleCancelEditing = () => { + setIsEditing(false) + setSelectedCards([]) + void navigate(router, '/collection') + } + + const addToDeck = (card: Card) => { + setSelectedCards(prevSelectedCards => { + // Add or remove card from the selectedCards + const isCardSelected = prevSelectedCards.some(selectedCard => selectedCard.id === card.id) + if (isCardSelected) { + return prevSelectedCards.filter(selectedCard => selectedCard.id !== card.id) + } else { + return [...prevSelectedCards, card] + } + }) + } + + const onCardToggle = (card: Card) => { + setSelectedCards((prevSelectedCards) => { + if (prevSelectedCards.some(selectedCard => selectedCard.id === card.id)) { + // Remove the card if it's already selected + return prevSelectedCards.filter(selectedCard => selectedCard.id !== card.id) + } else { + // Add the card if it's not already selected + return [...prevSelectedCards, card] + } + }) + } + + + // Sets up an event listener for route changes when deck editor is rendered. + useEffect(() => { + const handleRouteChange = () => { + if (router.query.newDeck) { + setCurrentDeck({ name: '', cards: [] }) + setIsEditing(true) + setEditingDeckIndex(null) + } + } + + router.events.on('routeChangeComplete', handleRouteChange) + + // Clean up the event listener when exiting the deck editor. + return () => { + router.events.off('routeChangeComplete', handleRouteChange) + } + }, [router.events, router.query.newDeck]) + return ( <> @@ -88,116 +167,48 @@ const Collection: FablePage = ({ decks, isHydrated }) => {
- - {/* Left Panel */} + {/* Left Panel - Search and Filters */}
-
- - {/* Search*/} -

Search

-
- -
- - {/*Effects*/} -

Effects

-
- {effects.map((effect, index) => { - const bgColor = effectMap[effect] ? 'bg-purple-900' : 'bg-gray-500' - return ( - ) - })} -
- - {/*Types*/} -

Types

-
- {types.map((type, index) => { - const bgColor = typeMap[type] ? 'bg-purple-900' : 'bg-gray-500' - return ( - ) - })} -
- - {/* Selected Card Display */} -
-

Card details

-
- {/*TODO handle the image*/} - {selectedCard?.lore.name -
{cardName}
-
-
{cardFlavor}
-
-
+
- {/* Card Collection Display */} + {/* Middle Panel - Card Collection Display */}
- { isHydrated && cards.length == 0 && -
- -
} - - { isHydrated && cards.length > 0 && -
- {cards.map(card => ( -
setSelectedCard(card)}> - {/*TODO handle the image*/} - {card.lore.name} -
{card.lore.name}
-
-
- {`${card.stats.attack}`} -
-
- {`${card.stats.defense}`} -
-
-
- ))} -
} +
- {/* Deck Panel */} -
-
- {/* New Deck Button */} - - New Deck → - - - {/* Deck Buttons */} - {decks.map((deck, deckID) => ( - - ))} -
+ {/* Right Panel - Deck List */} +
+ {isEditing && currentDeck ? ( + + ) : ( + + )}
@@ -205,4 +216,4 @@ const Collection: FablePage = ({ decks, isHydrated }) => { ) } -export default Collection \ No newline at end of file +export default Collection diff --git a/packages/webapp/src/pages/editor.tsx b/packages/webapp/src/pages/editor.tsx deleted file mode 100644 index 192949e0..00000000 --- a/packages/webapp/src/pages/editor.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import debounce from "lodash/debounce" -import Head from "next/head" -// This causes the "Ignoring unsupported entryTypes: largest-contentful-paint.", presumably -// because Firefox does not support some associated features. -import Image from "next/image" -import { useState, useMemo } from "react" -import { useAccount } from "wagmi" - -import jotaiDebug from "src/components/lib/jotaiDebug" -import { MintDeckModal } from "src/components/modals/mintDeckModal" -import { Navbar } from "src/components/navbar" -import { deployment } from "src/deployment" -import { useInventoryCardsCollectionGetCollection } from "src/generated" -import { Card } from "src/store/types" -import { Address } from "src/chain" -import { FablePage } from "src/pages/_app" -import { useRouter } from 'next/router' -import { useEffect} from 'react' - -// NOTE(norswap & geniusgarlic): Just an example, when the game actually has effects & types, -// fetch those from the chain instead of hardcoding them here. - -type Effect = string - -const effects: Effect[] = ['Charge', 'Flight', 'Courage', 'Undying', 'Frenzy', 'Enlightened'] -const initialEffectMap = Object.assign({}, ...effects.map(name => ({[name]: false}))) - -const types = ['Creature', 'Magic', 'Weapon'] -const initialTypeMap = Object.assign({}, ...types.map(name => ({[name]: false}))) - -const Editor: FablePage = ({ decks, setDecks, isHydrated }) => { - const { address } = useAccount() - const [ selectedCard, setSelectedCard ] = useState(null) - const [ searchInput, setSearchInput ] = useState('') - const [ effectMap, setEffectMap ] = useState(initialEffectMap) - const [ typeMap, setTypeMap ] = useState(initialTypeMap) - const [ deckName, setDeckName ] = useState('') - const [ deck, setDeck ] = useState([]) - const [ originalDeckIndex, setOriginalDeckIndex ] = useState(null) - - const router = useRouter() - - const cardName = selectedCard?.lore.name || "Hover a card" - const cardFlavor = selectedCard?.lore.flavor || "Hover a card to see its details" - - const activeEffects = Object.keys(effectMap).filter(key => effectMap[key]) - const activeTypes = Object.keys(typeMap).filter(key => typeMap[key]) - - const [isDeckValid, setIsDeckValid] = useState(true) - - const { data: unfilteredCards, refetch } = useInventoryCardsCollectionGetCollection({ - address: deployment.InventoryCardsCollection, - args: [address as Address] // TODO not ideal but safe in practice - }) as { - // make the wagmi type soup understandable, there are many more fields in reality - data: readonly Card[], - refetch: () => Promise<{ data?: readonly Card[], error: Error|null }> - } - - const cards: Card[] = (unfilteredCards || []).filter(card => { - // TODO(norswap): it would look like this if the card had effects & types - // const cardEffects = card.stats.effects || [] - // const cardTypes = card.stats.types || [] - const cardEffects: Effect[] = [] - const cardTypes: Effect[] = [] - return activeEffects.every(effect => cardEffects.includes(effect)) - && activeTypes.every(type => cardTypes.includes(type)) - && card.lore.name.toLowerCase().includes(searchInput.toLowerCase()) - }) - - const addToDeck = (card: Card) => { - setDeck(prevDeck => { - const isAlreadyInDeck = prevDeck.some(cardInDeck => cardInDeck.id === card.id) - if (isAlreadyInDeck) { - // Remove the card from the deck - return prevDeck.filter(cardInDeck => cardInDeck.id !== card.id) - } else { - // Add the card to the deck - return [...prevDeck, card] - } - }) - } - - // Check url for an index, which is passed if the user wants to modify an existing deck - useEffect(() => { - if (typeof router.query.deckID === "string") { - const deckIndex = parseInt(router.query.deckID) - if (!isNaN(deckIndex) && decks[deckIndex] != null) { - setOriginalDeckIndex(deckIndex) // Store the original index - const selectedDeck = decks[deckIndex] - setDeckName(selectedDeck.name) - setDeck(selectedDeck.cards) - } else { - setOriginalDeckIndex(null) // Reset if not editing an existing deck - } - } - }, [router.query.deckID, decks]) - - const isCardInDeck = (cardToCheck: Card) => { - return deck.some(cardInDeck => cardInDeck.id === cardToCheck.id) - } - - const removeFromDeck = (index: number) => { - setDeck(prevDeck => prevDeck.filter((_, i) => i !== index)) - } - - const handleInputChangeBouncy = (event: React.ChangeEvent) => { - setSearchInput(event.target.value) - } - - const handleDeckNameChange = (event: React.ChangeEvent) => { - setDeckName(event.target.value) // Update deck name state - } - - const handleCancel = () => { - router.push('/collection') - } - - const handleSave = () => { - // Check if deck name is empty OR if the deck doesn't have any cards - if (!deckName.trim() || deck.length === 0) { - setIsDeckValid(false) - return - } - - setIsDeckValid(true) - - const updatedDecks = [...decks] - - // Check if editing an existing deck and the name has changed - if (originalDeckIndex !== null && decks[originalDeckIndex].name !== deckName) { - // Remove the old deck - updatedDecks.splice(originalDeckIndex, 1) - } - - // Find if a deck with the new name already exists - const existingDeckIndex = updatedDecks.findIndex(d => d.name === deckName) - - if (existingDeckIndex !== -1) { - // Replace existing deck with the new name - updatedDecks[existingDeckIndex] = { name: deckName, cards: deck } - } else { - // Add as a new deck - updatedDecks.push({ name: deckName, cards: deck }) - } - - setDecks(updatedDecks) - // Redirect to the collections page - router.push('/collection') - } - - const handleInputChange = useMemo(() => debounce(handleInputChangeBouncy, 300), []) - - const handleEffectClick = (effectIndex: number) => { - const effect = effects[effectIndex] - setEffectMap({...effectMap, [effect]: !effectMap[effect]}) - } - - const handleTypeClick = (typeIndex: number) => { - const type = types[typeIndex] - setTypeMap({...typeMap, [type]: !typeMap[type]}) - } - - return ( - <> - - 0xFable: My Collection - - - {jotaiDebug()} -
- -
- - {/* Left Panel */} -
-
- - {/* Search*/} -

Search

-
- -
- - {/*Effects*/} -

Effects

-
- {effects.map((effect, index) => { - const bgColor = effectMap[effect] ? 'bg-purple-900' : 'bg-gray-500' - return ( - ) - })} -
- - {/*Types*/} -

Types

-
- {types.map((type, index) => { - const bgColor = typeMap[type] ? 'bg-purple-900' : 'bg-gray-500' - return ( - ) - })} -
- - {/* Selected Card Display */} -
-

Card details

-
- {/*TODO handle the image*/} - {selectedCard?.lore.name -
{cardName}
-
-
{cardFlavor}
-
-
-
- - {/* Card Collection Display */} -
- { isHydrated && cards.length == 0 && -
- -
} - - { isHydrated && cards.length > 0 && -
- {cards.map(card => ( -
addToDeck(card)} - onMouseEnter={() => setSelectedCard(card)} - > - - {card.lore.name} -
{card.lore.name}
-
-
- {`${card.stats.attack}`} -
-
- {`${card.stats.defense}`} -
-
-
- - ))} -
} -
- - {/* Deck Panel */} -
- {/* name and save */} -
-
-
- - - - -
-
- - {/* Container for the Card Names */} -
- {deck.map((card, index) => ( -
removeFromDeck(index)} - onMouseEnter={() => setSelectedCard(card)} - > -
{card.lore.name}
-
- ))} -
-
-
-
-
- - ) -} - -export default Editor \ No newline at end of file diff --git a/packages/webapp/src/pages/index.tsx b/packages/webapp/src/pages/index.tsx index 5e6a6533..48256f94 100644 --- a/packages/webapp/src/pages/index.tsx +++ b/packages/webapp/src/pages/index.tsx @@ -1,5 +1,4 @@ import { ConnectKitButton, useModal } from "connectkit" -import Link from "next/link" import { useAccount, useNetwork } from "wagmi" import { Address, chains } from "src/chain" @@ -11,6 +10,7 @@ import { useGameInGame } from "src/generated" import { FablePage } from "src/pages/_app" import { useGameID } from "src/store/hooks" import { Button } from "src/components/ui/button" +import Link from "src/components/link" const Home: FablePage = ({ isHydrated }) => { const { address } = useAccount() @@ -54,27 +54,19 @@ const Home: FablePage = ({ isHydrated }) => { {isWrongNetwork && } - {isRightNetwork && ( - <> -
- - - - - - - - - -
- - - - )} + + + + + } ) diff --git a/packages/webapp/tailwind.config.cjs b/packages/webapp/tailwind.config.cjs index 04eb984a..ca9541d0 100644 --- a/packages/webapp/tailwind.config.cjs +++ b/packages/webapp/tailwind.config.cjs @@ -71,6 +71,12 @@ module.exports = { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, + // Custom box shadow that adds a 'highlight' effect + // For example, add 'shadow-highlight shadow-orange-300' to className + // See: https://tailwindcss.com/docs/box-shadow#customizing-your-theme + boxShadow: { + 'highlight': '0 0 20px', + }, }, }, plugins: [require("tailwindcss-animate")],