diff --git a/app/about/page.tsx b/app/about/page.tsx
new file mode 100644
index 0000000..4ec0a4f
--- /dev/null
+++ b/app/about/page.tsx
@@ -0,0 +1,56 @@
+import { Sheet, Typography } from "@mui/joy"
+
+export default function About() {
+ return (
+
+
+ About this site
+
+
+ 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.
+
+
+ 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{" "}
+
+ Scryfall
+
+ .
+
+
+ This site was built by{" "}
+
+ Henry Hobbs
+
+ . Site source is available{" "}
+
+ here
+ {" "}
+ and the scripts used to generate the card data are available{" "}
+
+ here
+
+ .
+
+
+ Questions? Feature requests? Cards out of date? Email me at{" "}
+
+ mtg@hobbs.zone
+
+ .
+
+
+ )
+}
diff --git a/app/components/header.tsx b/app/components/header.tsx
new file mode 100644
index 0000000..5fb5486
--- /dev/null
+++ b/app/components/header.tsx
@@ -0,0 +1,23 @@
+import { Sheet, Typography } from "@mui/joy"
+import Link from "next/link"
+
+export default function Header() {
+ return (
+
+
+ MTG Visual Search
+
+
+ About
+
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 9ee7b3f..50af838 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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 (
-
{children}
+
+
+
+ {children}
+
+
)
}
diff --git a/app/page.tsx b/app/page.tsx
index 483fc21..45c3e51 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -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([])
const [cardsDisplayed, setCardsDisplayed] = useState([])
+
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 (
-
-
+ Search MTG Artwork
+
+ For best results use simple words, separated by spaces.
+
+
+ Search will return all cards that match at least one of the words,
+ so use many similar words to broaden your search.
+
+ >
+ )}
+
- {!!cards?.length && (
- }
- style={{ width: "100%" }}
- >
-
- {cardsDisplayed.map((card) => (
-
-
-
- ((e.target as HTMLImageElement).style.transform =
- "scale(1.1)")
- }
- onMouseOut={(e) =>
- ((e.target as HTMLImageElement).style.transform =
- "scale(1)")
- }
- />
-
-
- ))}
-
-
- )}
-
-
+ >
+ }
+ sx={{ width: "100%" }} // Add this line to make the search bar full width
+ />
+
+ {!cards?.length &&
}
+ {!!cards?.length && (
+ }
+ style={{ width: "100%" }}
+ >
+
+ {cardsDisplayed.map((card) => (
+
+
+
+ ((e.target as HTMLImageElement).style.transform =
+ "scale(1.1)")
+ }
+ onMouseOut={(e) =>
+ ((e.target as HTMLImageElement).style.transform =
+ "scale(1)")
+ }
+ />
+
+
+ ))}
+
+
+ )}
+
)
}
diff --git a/package-lock.json b/package-lock.json
index d5270b4..71a55cf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index d4b3691..dd06a7b 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}