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 && (
+
+ {suggestions.map((item) => (
+ - handleSelect(item.symbol)}
+ className="p-2 hover:bg-gray-200 cursor-pointer"
+ >
+ {item.description} ({item.symbol})
+
+ ))}
+
+ )}
+
+ )
+}
+
+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 && (
-
- )}
)
}
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