Skip to content

Commit eff7ed2

Browse files
committed
Keyword suggestions and animations
- Display a list of suggested keywords for user to click on - Cascade animate the keyboards through useTrail - Minor improvements to Artifact animations
1 parent 66b2510 commit eff7ed2

19 files changed

Lines changed: 324 additions & 52 deletions

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react/react-in-jsx-scope": "off",
2222
"react/prop-types": "off",
2323
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
24-
"react/no-unescaped-entities": "off"
24+
"react/no-unescaped-entities": "off",
25+
"no-console": "warn"
2526
},
2627
"settings": {
2728
"react": {

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This project creates a simplified user experience for exploring the Metropolitan Museum of Art collection using their [public API](https://metmuseum.github.io/). It demonstrates techniques for efficiently scrolling through thousands of artifacts without compromising performance.
66

7-
![Screen cast of using the app to search for the keyword scarab](./ReadmeAssets/metScreenCast.gif)
7+
[![Screen cast of using the app to search for the keyword scarab](http://i.ytimg.com/vi/hfpoIOHFre8/hqdefault.jpg)](https://www.youtube.com/watch?v=hfpoIOHFre8)
88

99
Try the live app: [Met Museum React Experience](https://met.bahador.dev/)
1010

115 KB
Loading
35 KB
Loading

ReadmeAssets/lighthouse.png

85.9 KB
Loading

index.html

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5+
<meta
6+
name="description"
7+
content="Explore the Metropolitan Museum of Art collection with our React app. Discover thousands of artifacts from around the world, from ancient Egypt to modern art. Search by keyword, artist, or object type. Use the search bar to find what you're looking for, or browse by category. Learn about the history and culture behind each artifact. Our app is designed for easy navigation and a seamless user experience. Discover the art and artifacts of the Met with our React app."
8+
/>
59
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
610
<link rel="preconnect" href="https://fonts.googleapis.com" />
711
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -10,11 +14,20 @@
1014
rel="stylesheet"
1115
/>
1216
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
13-
<title>Metropolitan Museum of Art</title>
17+
<title>Metropolitan Museum Explorer</title>
1418
</head>
1519
<body>
1620
<div id="root"></div>
21+
<footer role="contentinfo">
22+
<p>© 2024 Metropolitan Museum Explorer</p>
23+
<a
24+
href="https://github.com/bahaaador/met-museum-react"
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
>
28+
View Source Code on GitHub
29+
</a>
30+
</footer>
1731
<script type="module" src="src/index.jsx"></script>
1832
</body>
19-
<footer role="contentinfo">© 2020 Sample Copyright Message</footer>
20-
</html>
33+
</html>

src/App.css

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,53 @@
2828
}
2929
}
3030
}
31+
32+
.example-queries-container {
33+
position: relative;
34+
margin-top: 20vh;
35+
}
36+
37+
.example-queries {
38+
display: flex;
39+
flex-flow: column;
40+
align-items: center;
41+
width: 100%;
42+
}
43+
44+
.example-queries ul {
45+
display: grid;
46+
grid-template-columns: repeat(2, 1fr);
47+
gap: 20px;
48+
padding: 20px;
49+
margin: 0 auto;
50+
}
51+
52+
@media (min-width: 900px) {
53+
.example-queries ul {
54+
grid-template-columns: repeat(3, 1fr);
55+
}
56+
}
57+
58+
.example-queries li {
59+
list-style: none;
60+
}
61+
62+
.example-queries button {
63+
font-size: 1.1em;
64+
font-weight: 500;
65+
color: #4a4a4a;
66+
text-decoration: none;
67+
padding: 8px 15px;
68+
border-radius: 20px;
69+
background-color: #f0f0f0;
70+
border: none;
71+
cursor: pointer;
72+
transition: background-color 0.3s ease;
73+
white-space: nowrap;
74+
overflow: hidden;
75+
text-overflow: ellipsis;
76+
}
77+
78+
.example-queries button:hover {
79+
background-color: #e0e0e0;
80+
}

src/App.jsx

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,42 @@
1-
import React, { useEffect, lazy, Suspense } from "react";
1+
import React, { useEffect, lazy, Suspense, useMemo } from "react";
22
import {
33
useSpring,
44
animated,
5-
Globals,
5+
useTrail,
66
useReducedMotion,
7+
Globals,
78
} from "@react-spring/web";
8-
99
import { useArtStore } from "@store/useArtStore";
1010
import Header from "@components/Header";
11-
1211
import "./App.css";
1312
import { chunkArray } from "@utils";
13+
import { exampleQueries } from "./exampleQueries";
14+
15+
const AnimatedSuggestionButton = ({ suggestion, onClick }) => {
16+
const [spring, api] = useSpring(() => ({
17+
from: { scale: 1 },
18+
config: { tension: 300, friction: 10 },
19+
}));
20+
21+
const handleHover = () => {
22+
api.start({ scale: 1.05 });
23+
};
24+
25+
const handleLeave = () => {
26+
api.start({ scale: 1 });
27+
};
28+
29+
return (
30+
<animated.button
31+
style={spring}
32+
onMouseEnter={handleHover}
33+
onMouseLeave={handleLeave}
34+
onClick={() => onClick(suggestion)}
35+
>
36+
{suggestion}
37+
</animated.button>
38+
);
39+
};
1440

1541
const ItemsGrid = lazy(() => import("@components/ItemsGrid"));
1642
const DetailsModal = lazy(() => import("@components/DetailsModal"));
@@ -23,6 +49,8 @@ function App() {
2349
const detailsModalOpen = useArtStore((state) => state.detailsModalOpen);
2450
const error = useArtStore((state) => state.error);
2551
const reset = useArtStore((state) => state.reset);
52+
const keyword = useArtStore((state) => state.keyword);
53+
const setKeyword = useArtStore((state) => state.setKeyword);
2654

2755
const prefersReducedMotion = useReducedMotion();
2856

@@ -39,6 +67,24 @@ function App() {
3967

4068
const fadeInProps = useSpring({ opacity: 1, from: { opacity: 0 } });
4169

70+
const randomExampleQueries = useMemo(() => {
71+
return exampleQueries.sort(() => 0.5 - Math.random()).slice(0, 8);
72+
}, []);
73+
74+
const trail = useTrail(randomExampleQueries.length, {
75+
from: { opacity: 0, transform: "translateY(20px)" },
76+
to: { opacity: 1, transform: "translateY(0)" },
77+
});
78+
79+
const handleSuggestionClick = (suggestion) => {
80+
setKeyword(suggestion);
81+
};
82+
83+
const suggestionSectionFade = useSpring({
84+
opacity: keyword ? 0 : 1,
85+
config: { duration: 300 },
86+
});
87+
4288
return (
4389
<animated.div style={fadeInProps} className="app">
4490
<Header />
@@ -52,6 +98,29 @@ function App() {
5298
<LoadingIndicator />
5399
) : (
54100
<>
101+
{!keyword && (
102+
<div className="example-queries-container">
103+
<animated.div
104+
className="example-queries"
105+
style={suggestionSectionFade}
106+
>
107+
<h2 className="example-queries-title">Try searching for:</h2>
108+
<ul>
109+
{trail.map((style, index) => (
110+
<animated.li
111+
key={randomExampleQueries[index]}
112+
style={style}
113+
>
114+
<AnimatedSuggestionButton
115+
suggestion={randomExampleQueries[index]}
116+
onClick={handleSuggestionClick}
117+
/>
118+
</animated.li>
119+
))}
120+
</ul>
121+
</animated.div>
122+
</div>
123+
)}
55124
{objectIDs &&
56125
chunkArray(objectIDs, data_chunk_size).map((ids) => (
57126
<ItemsGrid key={ids[0]} data={ids} />

src/App.test.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import React from "react";
22
import { it, expect } from "vitest";
3-
import { render, fireEvent } from "@testing-library/react";
3+
import { render, fireEvent, waitFor } from "@testing-library/react";
44
import App from "./App";
55

6-
it("renders search input", () => {
6+
it("renders search input", async () => {
77
const { getByPlaceholderText } = render(<App />);
88

99
const searchBox = getByPlaceholderText(/Enter keyword here/i);
1010
expect(searchBox).toBeInTheDocument();
1111

1212
fireEvent.change(searchBox, { target: { value: "test" } });
1313

14-
expect(searchBox.value).toBe("test");
14+
await waitFor(() => {
15+
expect(searchBox.value).toBe("test");
16+
});
1517
});

src/components/ArtifactComponent/ArtifactComponent.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
.card .artifact-info {
2525
position: absolute;
2626
color: white;
27-
background: linear-gradient(90deg, #6f6965 0%, #ae967c 35%, #fff 100%);
27+
background: linear-gradient(90deg, #6f6965 0%, #ae967c 50%, #c5ad93 70%, #ae967c 850%, #6f6965 100%);
2828
width: 100%;
2929
left: 0;
3030
bottom: 0;
@@ -42,6 +42,9 @@
4242
font-size: 1.3rem;
4343
font-weight: 600;
4444
margin: 0;
45+
overflow: hidden;
46+
text-overflow: ellipsis;
47+
white-space: nowrap;
4548
}
4649

4750
.artifact-subtitle {

0 commit comments

Comments
 (0)