-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The (Editor and Collection) Merge #108
Changes from all commits
cc89c49
1a23a29
6dfca99
d66d902
69ef672
0ff3fc6
a368f86
e711cea
d2f0de7
b6cb6a3
67636a1
e7fa296
c85e050
da488da
9fd6a0b
36e70d5
05d0817
8b21444
d4f508b
109500a
168087a
3080773
b1ce4ba
de6fc70
474592a
0c01ce5
f431eba
c141bd0
3d2223b
99c3b1e
46956eb
102fc6d
634a867
f029ccd
a270ef7
2661a57
70c9423
7705e70
aa8b578
f859439
9a8eaf9
345996f
3294eab
661c69f
1c06d68
3a83806
36e35ae
42f25f7
928aded
60aff3f
3cf1f44
0f1f0aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
# Environment variables | ||
*.env | ||
.vscode | ||
.vs | ||
|
||
# MacOS | ||
.DS_Store | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"javascript.validate.enable": false, | ||
"typescript.validate.enable": false | ||
} |
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 |
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 |
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)) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's wrap this function in a debounce. Debounce basically holds off on firing the callback until the value stabilizes for a certain amount of time. So if you're typing in the input box pretty quickly, it would only fire after you stop typing. Here's a good visualization of debounce vs throttle (which rate limits the amount of time the callback is called instead): https://web.archive.org/web/20220117092326/http://demo.nimius.net/debounce_throttle/ You can use the debounce feature in lodash, which we already have as a dependency: https://lodash.com/docs/4.17.15#debounce |
||
|
||
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" | ||
/> | ||
eviterin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</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> | ||
eviterin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
{/* 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 |
eviterin marked this conversation as resolved.
Show resolved
Hide resolved
|
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. */} | ||
eviterin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
{/* 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 |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever clicking some of the filter items in the left panel, this is shown. Clearly not what we want (because the filtered set size is 0).
The condition here should be changed to display this modal only if the full card collection (not the filtered one) is empty.
Btw, I want to refactor the left panel according to this later: #93 and remove these filter buttons. But I think it's good to keep the generic filtering ability. This (refactor left panel) is for a further PR!