diff --git a/package.json b/package.json index 9f68c8a..570057a 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@egjs/react-infinitegrid": "^3.0.5", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", @@ -13,6 +14,7 @@ "node-sass": "^4.14.1", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-modal": "^3.11.2", "react-scripts": "3.4.3", "sass": "^1.26.10", "typescript": "^3.9.3" @@ -38,6 +40,7 @@ ] }, "devDependencies": { + "@types/react-modal": "^3.10.6", "http-server": "^0.12.3" } } diff --git a/src/App.scss b/src/App.scss index 1aa2256..1a28231 100644 --- a/src/App.scss +++ b/src/App.scss @@ -9,6 +9,21 @@ body { font-family: "Marcellus"; } +.current-card { + background: repeat url("images/elder_scrolling_bg.png") #0c0c0c; + color: #f8f8f8; + +} + +.fullcard tr td { + padding-right: 2rem; +} + +.fullcard tr td:nth-child(3) { + padding-left: 8rem; +} + + body * { font-family: "Marcellus"; } @@ -50,32 +65,29 @@ header { } } -.cardbox { - display: flex; - flex-wrap: wrap; - flex-direction: row; - font-size: 1.4rem; - justify-content: space-between; - align-content: space-between; +.card { + width: 192px; + height: 540px; + border: 1px solid #707c80; + border-radius: 8px; + padding: 0.5rem; + margin: 0.5rem; + + img { + height: 290px; + width: 175px; + } + + .card-text { + line-height: 1.4; + font-size: 1.4rem; + } - .card { - width: 192px; - border: 1px solid #707c80; - border-radius: 8px; - padding: 0.5rem; - margin: 0.5rem; - - img { - height: 290px; - width: 175px; - } - - .card-text { - line-height: 1.4; - } + .content table td { + font-size: 1.4rem; } } - + .lds-spinner-container { display: flex; justify-content: center; diff --git a/src/cards/Card.tsx b/src/cards/Card.tsx index 5423dbf..9e35d23 100644 --- a/src/cards/Card.tsx +++ b/src/cards/Card.tsx @@ -1,9 +1,19 @@ import * as React from "react"; import { CardProps } from "./types"; -export const Card = ({ card }: { card: CardProps }) => { +export const Card = ({ + card, + onClick +}: { + card: CardProps; + onClick: Function | null; +}) => { return ( -
+
onClick && onClick(card.id)} + > {card.name}

{card.name}

@@ -28,4 +38,3 @@ export const Card = ({ card }: { card: CardProps }) => {
); }; - diff --git a/src/cards/Cards.tsx b/src/cards/Cards.tsx index 5f2bdb7..d18cd95 100644 --- a/src/cards/Cards.tsx +++ b/src/cards/Cards.tsx @@ -2,7 +2,9 @@ import * as React from "react"; import { useState, useEffect, useRef } from "react"; import { Loading } from "./Loading"; import { Card } from "./Card"; -import { useInfiniteScroll } from "./infiniteScroll"; +import { JustifiedLayout, GridLayout } from "@egjs/react-infinitegrid"; +import ReactModal from "react-modal"; +import { FullCard, fullcardStyles } from "./FullCard"; import { CardProps, CardRequestProps, @@ -16,54 +18,85 @@ const firstUrl = `https://api.elderscrollslegends.io/v1/cards?pageSize=${PAGE_SI const emptyCards: CardState = { cards: [], - next: firstUrl + next: firstUrl, + maxCards: -1 }; export const Cards = () => { const [cards, setCards]: CardStateHandler = useState(emptyCards); - const [cardPage, setCardPage]: [string | null, Function] = useState(null); const [loading, setLoading]: [boolean, Function] = useState(false); + const [currentCard, setCurrentCard]: [CardProps | null, Function] = useState( + null + ); - const getNextPage = () => { - setCardPage(cards.next); - setLoading(true); - }; - - let pageBottomRef = useRef(null); - useInfiniteScroll(pageBottomRef, getNextPage); - - const fetchMoreCards = async () => { - if (cards.next === null || cardPage === null) { - return cards; + const loadItems = () => { + if (cards.next === null || cards.cards.length === cards.maxCards) { + return; } - const request = new Request(cardPage!); - const response = await fetch(request); - const data: CardRequestProps = await response.json(); - setCards({ - cards: cards.cards.concat(data.cards), - next: data._links && data._links.next ? data._links.next : null - }); - setLoading(false); + setLoading(true); + const request = new Request(cards.next); + fetch(request) + .then(response => response.json()) + .then((data: CardRequestProps) => { + setCards({ + cards: [...cards.cards, ...data.cards], + next: data._links && data._links.next ? data._links.next : null, + maxcards: data._totalCount + }); + setLoading(false); + }); }; - if (cards.next && cardPage === null) { - getNextPage(); - } + const loadMoreCards = (options: any) => { + if (options.startLoading !== null) { + options.startLoading(); + } + loadItems(); + }; - useEffect(() => { - fetchMoreCards(); - }, [cardPage]); + const onLayoutComplete = (options: any) => { + !options.isLayout && options.endLoading(); + }; + + const handleCloseCard = () => { + setCurrentCard(null); + }; + + const handleOpenCard = (id: string) => { + const card = cards.cards.find(card => card.id === id); + if (card) { + setCurrentCard(card); + } + }; return (
-
+ + + + {cards.cards.map((card: CardProps, count: number) => ( - + ))} -
- {loading && } -
+ + {loading ? : ""}
); }; diff --git a/src/cards/FullCard.tsx b/src/cards/FullCard.tsx new file mode 100644 index 0000000..b83e606 --- /dev/null +++ b/src/cards/FullCard.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import { CardProps } from "./types"; + +export const fullcardStyles = { + content: { + backgroundColor: "#0c0c0c", + color: "#f8f8f8" + } +}; + +export const FullCard = ({ card }: { card: CardProps | null }) => { + if (card === null) { + return <>; + } + + return ( +
+ {card.name} +
+

{card.name}

+

{card.text}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Type + {card.type} + Cost + {card.cost}
+ Subtypes + {card.subtypes.join(", ")} + Power + {card.power}
+ Rarity + {card.rarity} + Health + {card.health}
+ Set + {card.set.name} + Soul Summon + {card.soulSummon}
+ Attributes + {card.attributes.join(", ")} + Soul Trap + {card.soulTrap}
+ Keywords + {card.keywords} +
+ + + + + + + + + + + + +
+ Set Name: + {card.set.name}
+ Type: + {card.type}
+ {card.collectible ?

This card is considered collectible.

: ""} + {card.unique ? ( +

+ This card is unique. Unique cards may only be used once per deck. +

+ ) : ( + "" + )} +
+
+ ); +}; diff --git a/src/cards/infiniteScroll.ts b/src/cards/infiniteScroll.ts deleted file mode 100644 index 3f49403..0000000 --- a/src/cards/infiniteScroll.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from "react"; -import { useEffect, useCallback } from "react"; - -// From the Smashing Magazine article, -// https://www.smashingmagazine.com/2020/03/infinite-scroll-lazy-image-loading-react/ - -// As this is new to me, I'm going to try to explain it; -// IntersectionObserver takes a callback to be called when it's -// triggred. It's trigged when the node it's been asked to observe -// changes its relationship with another object or, without a -// specified object, with the viewport. When the `intersectionRatio` -// exceed zero, it is _intersecting_ the viewport, i.e. it is now -// visible. - -// useCallback is a memoizer; it only updates the contents of the -// function its wrapped if the included function changes, which -// it shouldn't. - -export const useInfiniteScroll = (scrollRef: React.RefObject, stateUpdate: Function) => { - const scrollObserver = (node: HTMLElement) => { - new IntersectionObserver(entries => { - if (entries.some(en => en.intersectionRatio > 0)) { - stateUpdate(); - } - }).observe(node); - }; - - useEffect(() => { - if (scrollRef.current) { - scrollObserver(scrollRef.current); - } - }, [scrollObserver, scrollRef]); -} diff --git a/src/cards/types.ts b/src/cards/types.ts index 254c764..840bbf7 100644 --- a/src/cards/types.ts +++ b/src/cards/types.ts @@ -9,11 +9,14 @@ export interface CardProps { rarity: string; type: string; cost: number; + power: number; + health: number; set: SetProps; collectible: boolean; soulSummon: number; soulTrap: number; text: string; + subtypes: string[]; attributes: string[]; keywords: string[]; unique: boolean; @@ -33,11 +36,10 @@ export interface CardRequestProps { _totalCount: number; } - export interface CardState { cards: CardProps[]; next: string | null; + maxCards: number; } export type CardStateHandler = [CardState, Function]; - diff --git a/yarn.lock b/yarn.lock index a844027..75feefb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1166,6 +1166,40 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18" integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== +"@egjs/component@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-2.1.2.tgz#c466a0a6fc6ba2d479814dcbe6ee4643c25a2e5b" + integrity sha512-7tnPiqxbSZ0porzlm0+/O3qZdanMj0zOq0sb17wQXuaRG49XKKKJaO+SacGnZDqf308N5hzJ0m9fZ4+j+VBvXA== + +"@egjs/infinitegrid@^3.6.3": + version "3.6.3" + resolved "https://registry.yarnpkg.com/@egjs/infinitegrid/-/infinitegrid-3.6.3.tgz#f12f60b4d7d983a6cc0371735df69991101618cc" + integrity sha512-GELg3eeOjIvk6DkUB61sab2eyV8yJHB2npR7mHmp0rjGf3nJoEBR6A1FqjIrAtT+NjseBxTP2EqYRyI9CwhrIA== + dependencies: + "@egjs/component" "^2.1.2" + "@egjs/lazyloaded" "0.0.2" + "@egjs/list-differ" "^1.0.0" + +"@egjs/lazyloaded@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@egjs/lazyloaded/-/lazyloaded-0.0.2.tgz#1658a2188239e1c5e9dee71734830bcca6625388" + integrity sha512-UWgJCDHxsEP/sF+ztwl8F79rP1RHmlwG5ur0fFUT6RgaqKN6aQTnuYm9G/MQB48mWpw/tsY80Ha7xhvhryl+FA== + dependencies: + "@egjs/component" "^2.1.2" + +"@egjs/list-differ@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.0.tgz#2277aff52e3e4bd9318d5c30ffc3ba3b6216f05e" + integrity sha512-HsbMKc0ZAQH+EUeCmI/2PvTYSybmkaWwakU8QGDYYgMVIg9BQ5sM0A0Nnombjxo2+JzXHxmH+jw//yGX+y6GYw== + +"@egjs/react-infinitegrid@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@egjs/react-infinitegrid/-/react-infinitegrid-3.0.5.tgz#be3759b219c67d7f8e487a2857ccd7d453949b26" + integrity sha512-Lf90xlCuThKeg9JfZEd9gdn4a5WgHaL/DmsiS/qWe3dUs7XmEolvfh3tzCP6j8APt6ezQsblr0q/DpUnG/7xkg== + dependencies: + "@egjs/infinitegrid" "^3.6.3" + "@egjs/list-differ" "^1.0.0" + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -1671,6 +1705,13 @@ dependencies: "@types/react" "*" +"@types/react-modal@^3.10.6": + version "3.10.6" + resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.10.6.tgz#76717220f32bc72769190692147814a540053c7e" + integrity sha512-XpshhwVYir1TRZ2HS5EfmNotJjB8UEC2IkT3omNtiQzROOXSzVLz5xsjwEpACP8U+PctkpfZepX+WT5oDf0a9g== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@^16.9.49": version "16.9.49" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872" @@ -4528,6 +4569,11 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -8856,7 +8902,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9082,6 +9128,21 @@ react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-modal@^3.11.2: + version "3.11.2" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4" + integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w== + dependencies: + exenv "^1.2.0" + prop-types "^15.5.10" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-scripts@3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.3.tgz#21de5eb93de41ee92cd0b85b0e1298d0bb2e6c51" @@ -10990,6 +11051,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack-chokidar2@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"