Adding in modal for card detail when card is clicked. Working through styling, and working with NextJS.
This commit is contained in:
19
components/ClientOnlyPortal.tsx
Normal file
19
components/ClientOnlyPortal.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useRef, useEffect, useState, ReactNode } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
selector: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientOnlyPortal({ children, selector }: Props) {
|
||||||
|
const ref = useRef();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = document.querySelector(selector);
|
||||||
|
setMounted(true);
|
||||||
|
}, [selector]);
|
||||||
|
|
||||||
|
return mounted ? createPortal(children, ref.current!) : null;
|
||||||
|
}
|
||||||
14
components/modal.tsx
Normal file
14
components/modal.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
import ClientOnlyPortal from "./ClientOnlyPortal";
|
||||||
|
|
||||||
|
export default function Modal({open}: { open: Function }) {
|
||||||
|
return (
|
||||||
|
<ClientOnlyPortal selector="#modal-root">
|
||||||
|
<div className='fixed bottom-0 container left-0 rounded-md modal'>
|
||||||
|
Hello
|
||||||
|
<button onClick={() => open(false)}>Close</button>
|
||||||
|
</div>
|
||||||
|
</ClientOnlyPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
pages/_document.tsx
Normal file
17
pages/_document.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head>
|
||||||
|
<title>Magic Card Valuator</title>
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
{/* Here we will mount our modal portal */}
|
||||||
|
<div id="modal-root" />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,25 +5,31 @@ import styles from '../styles/Home.module.css';
|
|||||||
import React, { useEffect, useState, useMemo } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import Modal from '../components/modal';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedCard, setSelectedCard] = useState({});
|
||||||
const [cards, setCards] = useState([]);
|
const [cards, setCards] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [queryParams, setQueryParams] = useState('');
|
const [queryParams, setQueryParams] = useState('');
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
const searchForCard = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const searchForCard = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await fetch(`https://api.scryfall.com/cards/search?q=${encodeURIComponent(event.target.value)}`)
|
if( event.target.value !== '' ) {
|
||||||
|
await fetch(`https://api.scryfall.com/cards/search?q=${encodeURIComponent(event.target.value)}`)
|
||||||
.then( res => res.json() )
|
.then( res => res.json() )
|
||||||
.then( ({data}) => {
|
.then( ({data}) => {
|
||||||
setCards(data);
|
setCards(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch( err => console.log(err) );
|
.catch( err => setError(err) );
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const debounceSearch = useMemo( () => debounce(searchForCard, 300), [search] );
|
const debounceSearch = useMemo( () => debounce(searchForCard, 300), [search] );
|
||||||
@@ -34,39 +40,48 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
|
||||||
<title>Magic Card Valuator</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<div className={`bg-center bg-no-repeat flex justify-center items-center mb-4 h-96 ${styles.hero}`}>
|
<div className="absolute bg-red-800 w-96 bg-red-800 right-0 top-0 rounded-md mr-4 mt-4 p-4">
|
||||||
<div className="w-3/4 text-center">
|
{error}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`bg-center bg-no-repeat flex justify-center items-center h-96 ${styles.hero}`}>
|
||||||
|
<div className="container text-center">
|
||||||
<label htmlFor="search" className="block mx-auto font-bold mb-2 text-2xl">Search for a Magic Card</label>
|
<label htmlFor="search" className="block mx-auto font-bold mb-2 text-2xl">Search for a Magic Card</label>
|
||||||
<input id="search" name="search" type="text" className={`block w-1/2 mx-auto rounded-full py-2 px-4 ${styles.search}`} onChange={debounceSearch} />
|
<input id="search" name="search" type="text" className={`block w-full md:w-1/2 mx-auto rounded-full py-2 px-4 ${styles.search}`} onChange={debounceSearch} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="container">
|
<main className="container my-4">
|
||||||
<section className="flex flex-wrap">
|
{ cards?.length === 0 || !cards ?
|
||||||
{loading && !cards.length ?
|
<div>Please search for a card</div>
|
||||||
<div className="spinner"></div>
|
:
|
||||||
:
|
<section className="flex flex-wrap justify-center items-center">
|
||||||
cards && cards.map( (card: any) => <Image className="hover:scale-110 transition-transform duration-150 ease-in-out w-1/6 p-2" key={card.id} alt={`${card.name} Card`} src={card.image_uris?.png || "https://via.placeholder.com/150" } width={100} height={100} /> )
|
{loading && search !== "" ?
|
||||||
}
|
<div className="spinner mx-auto"></div>
|
||||||
</section>
|
:
|
||||||
|
cards && cards.map( (card: any) => <Image onClick={() => {
|
||||||
|
setSelectedCard(card);
|
||||||
|
setModalOpen(true);
|
||||||
|
}} className="cursor-pointer hover:scale-110 transition-transform duration-150 ease-in-out w-full sm:w-1/2 md:w-1/3 lg:w-1/4 p-2" key={card.id} alt={`${card.name} Card`} src={card.image_uris?.png || "https://via.placeholder.com/150" } width={100} height={100} /> )
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<a
|
<a
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
href="https://www.linkedin.com/in/frank-delaguila/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer">
|
||||||
>
|
Frank Delaguila
|
||||||
Powered by{' '}
|
</a>
|
||||||
<span>
|
|
||||||
<Image className="fill-white" src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{modalOpen ?
|
||||||
|
<Modal open={setModalOpen} />
|
||||||
|
: null}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 1280px) {
|
||||||
|
.hero {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 2rem auto 0 auto;
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background: #1d1d1c;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner:after {
|
.spinner:after {
|
||||||
content: " ";
|
content: " ";
|
||||||
display: block;
|
display: block;
|
||||||
@@ -28,6 +35,7 @@
|
|||||||
border-color: #fff transparent #fff transparent;
|
border-color: #fff transparent #fff transparent;
|
||||||
animation: lds-dual-ring 1.2s linear infinite;
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes lds-dual-ring {
|
@keyframes lds-dual-ring {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|||||||
Reference in New Issue
Block a user