diff --git a/package-lock.json b/package-lock.json index 80702c4..9dd9bd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "twinkle", "version": "0.1.0", "dependencies": { + "lodash.debounce": "^4.0.8", "next": "14.2.8", "react": "^18", "react-dom": "^18", @@ -3659,6 +3660,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 4928d0d..3d19bcc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "lodash.debounce": "^4.0.8", "next": "14.2.8", "react": "^18", "react-dom": "^18", diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx new file mode 100644 index 0000000..f370b13 --- /dev/null +++ b/src/components/SearchBar.tsx @@ -0,0 +1,87 @@ +import { useState, useEffect, useCallback } from 'react' +import debounce from 'lodash.debounce' + +const SearchBar = ({ onSelectSymbol }: { onSelectSymbol: (symbol: string) => void }) => { + const [query, setQuery] = useState('') + const [suggestions, setSuggestions] = useState<{ symbol: string, description: string }[]>([]) + const [loading, setLoading] = useState(false) + const [notFound, setNotFound] = useState(false) + + const fetchSuggestions = async (query: string) => { + setLoading(true) + setNotFound(false) + try { + const res = await fetch(`/api/search?query=${query}`) + const data = await res.json() + + if (data.result && data.result.length > 0) { + setSuggestions(data.result) + } else { + setNotFound(true) + setSuggestions([]) + } + } catch (error) { + console.error('Error fetching suggestions:', error) + setNotFound(true) + setSuggestions([]) + } finally { + setLoading(false) + } + } + + const debouncedFetchSuggestions = useCallback(debounce((query: string) => { + fetchSuggestions(query) + }, 300), []) + + useEffect(() => { + if (query.length > 1) { + debouncedFetchSuggestions(query) + } else { + setSuggestions([]) + setNotFound(false) + } + }, [query, debouncedFetchSuggestions]) + + const handleSelect = (symbol: string) => { + setQuery('') + setSuggestions([]) + onSelectSymbol(symbol) + } + + return ( +
+ setQuery(e.target.value)} + className="border p-2 w-full" + /> + {loading && ( +
+ Loading... +
+ )} + {!loading && notFound && ( +
+ No results found +
+ )} + {suggestions.length > 0 && ( + + )} +
+ ) +} + +export default SearchBar \ No newline at end of file diff --git a/src/components/StockPrice.tsx b/src/components/StockPrice.tsx index b8fc481..a5ce1e8 100644 --- a/src/components/StockPrice.tsx +++ b/src/components/StockPrice.tsx @@ -1,19 +1,21 @@ import { useState } from 'react' +import SearchBar from './SearchBar' const StockPrice = () => { const [symbol, setSymbol] = useState('') const [price, setPrice] = useState(null) const [error, setError] = useState('') - const fetchStockPrice = async () => { + const fetchStockPrice = async (selectedSymbol: string) => { setError('') try { - const res = await fetch(`/api/quote?symbol=${symbol}`) + const res = await fetch(`/api/quote?symbol=${selectedSymbol}`) const data = await res.json() if (data.error) { setError(data.error) } else { setPrice(data.c) + setSymbol(selectedSymbol) } } catch (err) { setError('Failed to fetch stock price') @@ -22,29 +24,22 @@ const StockPrice = () => { return (
- setSymbol(e.target.value)} - className="border p-2 mr-2" - /> - - {price !== null && ( -
-

Current Price: ${price}

+ + {symbol && ( +
+

Symbol: {symbol}

+ {price !== null && ( +
+

Current Price: ${price}

+
+ )} + {error && ( +
+

{error}

+
+ )}
)} - {error && ( -
-

{error}

-
- )}
) } diff --git a/src/pages/api/search.ts b/src/pages/api/search.ts new file mode 100644 index 0000000..e063e82 --- /dev/null +++ b/src/pages/api/search.ts @@ -0,0 +1,20 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { fetchSymbols } from "../../utils/sparkle"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { query } = req.query; + if (!query || typeof query !== "string") { + res.status(400).json({ error: "Invalid symbol" }); + return; + } + + try { + const quote = await fetchSymbols(query); + res.status(200).json(quote); + } catch (error) { + res.status(500).json({ error: "Error fetching quote" }); + } +} diff --git a/src/utils/sparkle.ts b/src/utils/sparkle.ts index d7494ec..1126fc6 100644 --- a/src/utils/sparkle.ts +++ b/src/utils/sparkle.ts @@ -7,4 +7,13 @@ export const fetchQuote = async (symbol: string) => { throw new Error('Error fetching quote') } return res.json() -} \ No newline at end of file +} + +export const fetchSymbols = async (symbol: string) => { + const url = `${SPARKLE_BASE_URL}/search?query=${symbol}` + const res = await fetch(url) + if (!res.ok) { + throw new Error('Error fetching quote') + } + return res.json() + } \ No newline at end of file