mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 05:50:33 +00:00
new: search stocks
This commit is contained in:
parent
ad64cff81f
commit
1f64c881b9
6 changed files with 143 additions and 24 deletions
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "twinkle",
|
"name": "twinkle",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"next": "14.2.8",
|
"next": "14.2.8",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
@ -3659,6 +3660,12 @@
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"next": "14.2.8",
|
"next": "14.2.8",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
|
87
src/components/SearchBar.tsx
Normal file
87
src/components/SearchBar.tsx
Normal file
|
@ -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 (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search stocks..."
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
className="border p-2 w-full"
|
||||||
|
/>
|
||||||
|
{loading && (
|
||||||
|
<div className="absolute border bg-white p-2 w-full mt-1 text-center">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && notFound && (
|
||||||
|
<div className="absolute border bg-white p-2 w-full mt-1 text-center text-red-500">
|
||||||
|
No results found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<ul className="absolute border bg-white w-full mt-1">
|
||||||
|
{suggestions.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.symbol}
|
||||||
|
onClick={() => handleSelect(item.symbol)}
|
||||||
|
className="p-2 hover:bg-gray-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{item.description} ({item.symbol})
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBar
|
|
@ -1,19 +1,21 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import SearchBar from './SearchBar'
|
||||||
|
|
||||||
const StockPrice = () => {
|
const StockPrice = () => {
|
||||||
const [symbol, setSymbol] = useState('')
|
const [symbol, setSymbol] = useState('')
|
||||||
const [price, setPrice] = useState<number | null>(null)
|
const [price, setPrice] = useState<number | null>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const fetchStockPrice = async () => {
|
const fetchStockPrice = async (selectedSymbol: string) => {
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/quote?symbol=${symbol}`)
|
const res = await fetch(`/api/quote?symbol=${selectedSymbol}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
setError(data.error)
|
setError(data.error)
|
||||||
} else {
|
} else {
|
||||||
setPrice(data.c)
|
setPrice(data.c)
|
||||||
|
setSymbol(selectedSymbol)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to fetch stock price')
|
setError('Failed to fetch stock price')
|
||||||
|
@ -22,29 +24,22 @@ const StockPrice = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<input
|
<SearchBar onSelectSymbol={fetchStockPrice} />
|
||||||
type="text"
|
{symbol && (
|
||||||
placeholder="Enter stock symbol"
|
<div>
|
||||||
value={symbol}
|
<h2 className="text-2xl font-bold mt-4">Symbol: {symbol}</h2>
|
||||||
onChange={(e) => setSymbol(e.target.value)}
|
{price !== null && (
|
||||||
className="border p-2 mr-2"
|
<div className="mt-2">
|
||||||
/>
|
<p>Current Price: ${price}</p>
|
||||||
<button
|
</div>
|
||||||
onClick={fetchStockPrice}
|
)}
|
||||||
className="bg-blue-500 text-white p-2"
|
{error && (
|
||||||
>
|
<div className="mt-2 text-red-500">
|
||||||
Get Price
|
<p>{error}</p>
|
||||||
</button>
|
</div>
|
||||||
{price !== null && (
|
)}
|
||||||
<div className="mt-4">
|
|
||||||
<p>Current Price: ${price}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && (
|
|
||||||
<div className="mt-4 text-red-500">
|
|
||||||
<p>{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
20
src/pages/api/search.ts
Normal file
20
src/pages/api/search.ts
Normal file
|
@ -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" });
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,4 +7,13 @@ export const fetchQuote = async (symbol: string) => {
|
||||||
throw new Error('Error fetching quote')
|
throw new Error('Error fetching quote')
|
||||||
}
|
}
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
Loading…
Reference in a new issue