Added about page and small styling tweaks

This commit is contained in:
Henry Hobbs 2024-06-19 17:17:05 -04:00
parent 376e9bfd1a
commit 9b5a44bdc6
6 changed files with 469 additions and 81 deletions

56
app/about/page.tsx Normal file
View File

@ -0,0 +1,56 @@
import { Sheet, Typography } from "@mui/joy"
export default function About() {
return (
<Sheet sx={{ maxWidth: 700, margin: "auto" }}>
<Typography padding={1} level="h3">
About this site
</Typography>
<Typography padding={1}>
Inspired by a fried's need to build a deck using cards featuring chairs,
this search engine helps you find Magic: The Gathering cards by their
artwork.
</Typography>
<Typography padding={1}>
Azure Cognitive Services was used to analyze the images and generate
captions, which are then compared against the search query. Card data
and images are from{" "}
<a style={{ textDecoration: "underline" }} href="https://scryfall.com/">
Scryfall
</a>
.
</Typography>
<Typography padding={1}>
This site was built by{" "}
<a
style={{ textDecoration: "underline" }}
href="https://hobbs.zone/henry/"
>
Henry Hobbs
</a>
. Site source is available{" "}
<a
style={{ textDecoration: "underline" }}
href="https://git.hobbs.zone/henry/mtg-visual-search"
>
here
</a>{" "}
and the scripts used to generate the card data are available{" "}
<a
style={{ textDecoration: "underline" }}
href="https://git.hobbs.zone/henry/mtg-chair-ai"
>
here
</a>
.
</Typography>
<Typography padding={1}>
Questions? Feature requests? Cards out of date? Email me at{" "}
<a style={{ textDecoration: "underline" }} href="mailto:mtg@hobbs.zone">
mtg@hobbs.zone
</a>
.
</Typography>
</Sheet>
)
}

23
app/components/header.tsx Normal file
View File

@ -0,0 +1,23 @@
import { Sheet, Typography } from "@mui/joy"
import Link from "next/link"
export default function Header() {
return (
<Sheet
variant="solid"
sx={{
flexDirection: "row",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography level="h1" padding={2} variant="solid">
<Link href="/">MTG Visual Search</Link>
</Typography>
<Typography level="h4" padding={2} variant="solid" textAlign="right">
<Link href="about">About</Link>
</Typography>
</Sheet>
)
}

View File

@ -1,6 +1,8 @@
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import Header from "./components/header"
import { Sheet } from "@mui/joy"
const inter = Inter({ subsets: ["latin"] })
@ -16,7 +18,18 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<Sheet
sx={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
}}
>
<Header />
{children}
</Sheet>
</body>
</html>
)
}

View File

@ -4,22 +4,28 @@ import {
Button,
CircularProgress,
Grid,
IconButton,
Input,
Sheet,
Stack,
Typography,
} from "@mui/joy"
import { FormEvent, useState } from "react"
import { CardType } from "@/types/types"
import InfiniteScroll from "react-infinite-scroller"
import { Clear } from "@mui/icons-material"
export default function Home() {
const [searchQuery, setSearchQuery] = useState("")
const [loading, setLoading] = useState(false)
const [cards, setCards] = useState<CardType[]>([])
const [cardsDisplayed, setCardsDisplayed] = useState<CardType[]>([])
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
if (!searchQuery) {
handleClear()
return
}
setLoading(true)
const response = await fetch(`/api/?search=${searchQuery}`)
const data = await response.json()
@ -27,47 +33,60 @@ export default function Home() {
setCardsDisplayed(data.slice(0, 12))
setLoading(false)
}
const handleClear = () => {
setCards([])
setCardsDisplayed([])
setSearchQuery("")
}
const loadMore = async () => {
if (cardsDisplayed.length >= cards.length) return
setCardsDisplayed(
cards.slice(0, Math.min(cardsDisplayed.length + 12, cards.length))
)
}
return (
<Sheet
sx={{
padding: 5,
minHeight: "100vh",
}}
<Stack
spacing={2}
sx={{ margin: "auto", flex: 1, width: "100%", padding: 5 }}
alignItems="center"
justifyContent={cards?.length ? "flex-start" : "center"}
textAlign="center"
>
<Stack
spacing={2}
alignItems="center"
justifyContent="center"
textAlign="center"
{!cards?.length && (
<>
<Typography level="h1">Search MTG Artwork</Typography>
<Typography level="h4">
For best results use simple words, separated by spaces.
</Typography>
<Typography>
Search will return all cards that match at least one of the words,
so use many similar words to broaden your search.
</Typography>
</>
)}
<form
onSubmit={handleSubmit}
style={{ width: "100%", maxWidth: "700px", paddingBottom: "16px" }}
>
{!cards?.length && (
<>
<Typography level="h1">Search MTG Artwork</Typography>
<Typography level="h4">
For best results use simple words, separated by spaces.
</Typography>
<Typography>
Search will return all cards that match at least one of the words,
so use many similar words to broaden your search.
</Typography>
</>
)}
<form
onSubmit={handleSubmit}
style={{ width: "100%", maxWidth: "700px", paddingBottom: "16px" }}
>
<Input
placeholder="Type something..."
size="lg"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
endDecorator={
<Input
placeholder="Type something..."
size="lg"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
endDecorator={
<>
{!!cards?.length && (
<IconButton
sx={{ marginRight: 0 }}
onClick={handleClear}
color="danger"
>
<Clear />
</IconButton>
)}
<Button
variant="solid"
color="primary"
@ -78,47 +97,48 @@ export default function Home() {
>
Search
</Button>
}
sx={{ width: "100%" }} // Add this line to make the search bar full width
/>
</form>
{!!cards?.length && (
<InfiniteScroll
pageStart={0}
initialLoad={false}
loadMore={loadMore}
hasMore={cardsDisplayed.length < cards.length}
loader={<CircularProgress />}
style={{ width: "100%" }}
>
<Grid container spacing={2} sx={{ flexGrow: 1 }} key="cardGrid">
{cardsDisplayed.map((card) => (
<Grid xs={12} sm={6} md={4} lg={3} xl={2} key={card.id}>
<a href={card.url} target="_blank" rel="noreferrer">
<img
src={card.mediumImageUrl}
alt={card.name}
style={{
width: "100%",
borderRadius: 16,
transition: "transform 0.1s",
}}
onMouseOver={(e) =>
((e.target as HTMLImageElement).style.transform =
"scale(1.1)")
}
onMouseOut={(e) =>
((e.target as HTMLImageElement).style.transform =
"scale(1)")
}
/>
</a>
</Grid>
))}
</Grid>
</InfiniteScroll>
)}
</Stack>
</Sheet>
</>
}
sx={{ width: "100%" }} // Add this line to make the search bar full width
/>
</form>
{!cards?.length && <div style={{ height: "10vh" }}></div>}
{!!cards?.length && (
<InfiniteScroll
pageStart={0}
initialLoad={false}
loadMore={loadMore}
hasMore={cardsDisplayed.length < cards.length}
loader={<CircularProgress />}
style={{ width: "100%" }}
>
<Grid container spacing={2} sx={{ flexGrow: 1 }} key="cardGrid">
{cardsDisplayed.map((card) => (
<Grid xs={12} sm={6} md={4} lg={3} xl={2} key={card.id}>
<a href={card.url} target="_blank" rel="noreferrer">
<img
src={card.mediumImageUrl}
alt={card.name}
style={{
width: "100%",
borderRadius: 16,
transition: "transform 0.1s",
}}
onMouseOver={(e) =>
((e.target as HTMLImageElement).style.transform =
"scale(1.1)")
}
onMouseOut={(e) =>
((e.target as HTMLImageElement).style.transform =
"scale(1)")
}
/>
</a>
</Grid>
))}
</Grid>
</InfiniteScroll>
)}
</Stack>
)
}

275
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/joy": "^5.0.0-beta.36",
"@types/react-infinite-scroller": "^1.2.5",
"next": "14.2.3",
@ -308,6 +309,31 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.20.tgz",
"integrity": "sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==",
"dependencies": {
"@babel/runtime": "^7.23.9"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/joy": {
"version": "5.0.0-beta.36",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.36.tgz",
@ -348,6 +374,220 @@
}
}
},
"node_modules/@mui/material": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.20.tgz",
"integrity": "sha512-tVq3l4qoXx/NxUgIx/x3lZiPn/5xDbdTE8VrLczNpfblLYZzlrbxA7kb9mI8NoBF6+w9WE9IrxWnKK5KlPI2bg==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.40",
"@mui/core-downloads-tracker": "^5.15.20",
"@mui/system": "^5.15.20",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.20",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/base": {
"version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
"integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.20.tgz",
"integrity": "sha512-DoL2ppgldL16utL8nNyj/P12f8mCNdx/Hb/AJnX9rLY4b52hCMIx1kH83pbXQ6uMy6n54M3StmEbvSGoj2OFuA==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material/node_modules/@mui/private-theming": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.20.tgz",
"integrity": "sha512-BK8F94AIqSrnaPYXf2KAOjGZJgWfvqAVQ2gVR3EryvQFtuBnG6RwodxrCvd3B48VuMy6Wsk897+lQMUxJyk+6g==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.15.20",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/styled-engine": {
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
"integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/system": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.20.tgz",
"integrity": "sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.15.20",
"@mui/styled-engine": "^5.15.14",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.20",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/@mui/utils": {
"version": "5.15.20",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.20.tgz",
"integrity": "sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "6.0.0-alpha.8",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.0.0-alpha.8.tgz",
@ -692,6 +932,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"peer": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -827,6 +1076,16 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -1154,6 +1413,22 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",

View File

@ -9,19 +9,20 @@
"lint": "next lint"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.2.3",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.20",
"@mui/joy": "^5.0.0-beta.36",
"@types/react-infinite-scroller": "^1.2.5",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"react-infinite-scroller": "^1.2.6"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18"
"@types/react-dom": "^18",
"typescript": "^5"
}
}