Skip to content

Commit

Permalink
The (Editor and Collection) Merge (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
norswap authored Mar 1, 2024
2 parents 8e28481 + 0f1f0aa commit 2fb40c1
Show file tree
Hide file tree
Showing 16 changed files with 465 additions and 494 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Environment variables
*.env
.vscode
.vs

# MacOS
.DS_Store
Expand Down
Empty file added packages/webapp/.vs/slnx.sqlite
Empty file.
4 changes: 4 additions & 0 deletions packages/webapp/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"javascript.validate.enable": false,
"typescript.validate.enable": false
}
2 changes: 1 addition & 1 deletion packages/webapp/src/actions/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CardCollectionDisplayProps> = ({ cards, isHydrated, setSelectedCard, selectedCards, onCardToggle, isEditing }) => {
return (
<>
<div className="col-span-7 flex rounded-xl border overflow-y-auto">
{isHydrated && cards.length === 0 && (
<div className="flex flex-row w-full justify-center items-center">
<MintDeckModal/>
</div>
)}

{isHydrated && cards.length > 0 && (
<div className="flex flex-wrap justify-around overflow-y-auto pb-4">
{cards.map((card, index) => (
<div
key={card.id}
className={`m-4 bg-slate-900/50 ${
selectedCards.some(c => 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)
}
}}
>
<Image className="aspect-square" src={testCards.find(tc => Number(tc.id) === index + 1)?.image || ""} alt={card.lore.name} width={256} height={256} />
<div className="text-center">{card.lore.name}</div>
<div className="flex items-end justify-between p-2 relative">
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-yellow-400 text-gray-900 font-bold text-lg absolute bottom-[-16px]">{card.stats.attack}</div>
<div className="flex items-center justify-center h-8 w-8 rounded-full bg-red-600 text-gray-900 font-bold text-lg absolute bottom-[-16px] right-3">{card.stats.defense}</div>
</div>
</div>
))}
</div>
)}
</div>
</>
)
}

export default CardCollectionDisplay
36 changes: 36 additions & 0 deletions packages/webapp/src/components/collection/deckList.tsx
Original file line number Diff line number Diff line change
@@ -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<DeckCollectionDisplayProps> = ({ decks, onDeckSelect }) => {
return (
<div className="w-full flex flex-col items-center p-3">
{/* New Deck Button */}
<div>
<Button variant="secondary" className="border-2 border-yellow-500 normal-case hover:scale-105 font-fable text-xl hover:border-yellow-400">
<Link href={"/collection?newDeck=true"}>
New Deck →
</Link>
</Button>
</div>

{/* Deck Buttons */}
{decks.map((deck, deckID) => (
<Button variant="secondary" width="full" className="border-2 border-yellow-500 normal-case hover:scale-105 font-fable text-xl hover:border-yellow-400"
key={deckID}
onClick={() => onDeckSelect(deckID)}
>
{deck.name}
</Button>
))}
</div>
)
}

export default DeckCollectionDisplay
88 changes: 88 additions & 0 deletions packages/webapp/src/components/collection/deckPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<DeckConstructionPanelProps> = ({ 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<HTMLInputElement>) => {
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 (
<div className="flex flex-col items-center w-full p-3 overflow-y-auto overflow-x-hidden">
{/* Deck Name Input */}
<div className="flex flex-wrap gap-2 justify-center w-full">
<input
type="text"
value={deckName}
onChange={handleDeckNameChange}
style={{ outline: deckNameValid ? "none" : "2px solid red" }}
className="flex-shrink min-w-0 px-2 py-2 border rounded-md text-black bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent m-1.5 placeholder-gray-700 flex-basis[auto] max-w-full"
placeholder="Deck name"
/>
</div>

{/* Save and Cancel Buttons */}
<div className="flex flex-wrap gap-2 justify-center w-full">
<Button variant="secondary" className="border-2 border-yellow-500 normal-case hover:scale-105 font-fable text-xl hover:border-yellow-400" onClick={handleSave}>
✓Save
</Button>
<Button variant="secondary" className="border-2 border-yellow-500 normal-case hover:scale-105 font-fable text-xl hover:border-yellow-400" onClick={onCancel}>
✕Cancel
</Button>
</div>

{/* List of Cards in the Deck */}
<div className="mt-4 w-full">
{selectedCards.length > 0 ? (
selectedCards.map((card, index) => (
<div
key={index}
className="p-2 cursor-pointer hover:bg-gray-100"
onClick={() => onCardSelect(card)}
>
<div className="flex items-center space-x-3">
<Image src={testCards.find(tc => tc.id === Number(card.id))?.image || '/card_art/1.jpg'} alt="Card art" width={40} height={40} className="object-cover rounded-full" />
<span className="font-medium">{card.lore.name}</span>
</div>
</div>
))
) : (
<div className="p-4 text-center text-gray-300">
Click on cards to add them to the deck.
</div>
)}
</div>
</div>
)
}

export default DeckConstructionPanel
84 changes: 84 additions & 0 deletions packages/webapp/src/components/collection/filterPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => void
selectedCard: Card | null
}

const FilterPanel: React.FC<FilterPanelProps> = ({
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 (
<div className="flex col-span-3 rounded-xl border overflow-y-auto">
<div className="overflow-y-auto">
{/* Search */}
<h2 className="text-2xl font-bold text-white m-1.5">Search</h2>
<div>
<input
type="text"
onChange={handleInputChange}
className="px-4 py-2 border rounded-md text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent m-1.5"
placeholder="Search by name" />
</div>

{/* Effects */}
<h3 className="text-xl font-bold text-white m-1.5">Effects</h3>
<div className="flex flex-wrap gap-2">
{effects.map((effect, index) => (
<button
key={index}
onClick={() => handleEffectClick(index)}
className={`text-white font-bold py-2 px-2 rounded m-1.5 ${effectMap[effect] ? 'bg-purple-900' : 'bg-gray-500'}`}>
{effect}
</button>)
)}
</div>

{/* Types */}
<h3 className="text-xl font-bold text-white m-1">Types</h3>
<div className="flex flex-wrap gap-2">
{types.map((type, index) => (
<button
key={index}
onClick={() => handleTypeClick(index)}
className={`text-white font-bold py-2 px-2 rounded m-1 ${typeMap[type] ? 'bg-purple-900' : 'bg-gray-500'}`}>
{type}
</button>)
)}
</div>

{/* todo @eviterin: makes sense to add a filter for the card collection display to only show one of each card. */}

{/* Selected Card Display */}
<div className="pb-5">
<h2 className="text-3xl font-bold text-white m-1.5">Card details</h2>
<div className="m-4 bg-slate-900/50 rounded-lg p-4 border-4 border-slate-900">
<Image src="/card_art/0.jpg" alt={cardName} width={256} height={256} className="m-auto" />
<div className="text-center">{cardName}</div>
</div>
<div className="text-center m-2">{cardFlavor}</div>
</div>
</div>
</div>
)
}

export default FilterPanel
31 changes: 31 additions & 0 deletions packages/webapp/src/components/link.tsx
Original file line number Diff line number Diff line change
@@ -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<QueryParamLinkProps> = ({ 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 (
<Link href={url}>
{children}
</Link>
)
}

export default QueryParamLink
8 changes: 6 additions & 2 deletions packages/webapp/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -41,11 +45,11 @@ export interface ButtonProps
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, width, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ variant, size, width, className }))}
ref={ref}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/webapp/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading

0 comments on commit 2fb40c1

Please sign in to comment.