mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 04:00:34 +00:00
Compare commits
2 commits
bd84089d7b
...
1f64c881b9
Author | SHA1 | Date | |
---|---|---|---|
1f64c881b9 | |||
ad64cff81f |
7 changed files with 145 additions and 24 deletions
2
.env.example
Normal file
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
# sparkle base url
|
||||
NEXT_PUBLIC_SPARKLE_BASE_URL=
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "14.2.8",
|
||||
"react": "^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 SearchBar from './SearchBar'
|
||||
|
||||
const StockPrice = () => {
|
||||
const [symbol, setSymbol] = useState('')
|
||||
const [price, setPrice] = useState<number | null>(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 (
|
||||
<div className="my-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter stock symbol"
|
||||
value={symbol}
|
||||
onChange={(e) => setSymbol(e.target.value)}
|
||||
className="border p-2 mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchStockPrice}
|
||||
className="bg-blue-500 text-white p-2"
|
||||
>
|
||||
Get Price
|
||||
</button>
|
||||
{price !== null && (
|
||||
<div className="mt-4">
|
||||
<p>Current Price: ${price}</p>
|
||||
<SearchBar onSelectSymbol={fetchStockPrice} />
|
||||
{symbol && (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mt-4">Symbol: {symbol}</h2>
|
||||
{price !== null && (
|
||||
<div className="mt-2">
|
||||
<p>Current Price: ${price}</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-2 text-red-500">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-4 text-red-500">
|
||||
<p>{error}</p>
|
||||
</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" });
|
||||
}
|
||||
}
|
|
@ -8,3 +8,12 @@ export const fetchQuote = async (symbol: string) => {
|
|||
}
|
||||
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