diff --git a/src/components/Header.tsx b/src/components/Header.tsx deleted file mode 100644 index 9a585b7..0000000 --- a/src/components/Header.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const Header = () => ( -
-

- Stock Price Application -

-
- ) - - export default Header \ No newline at end of file diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx new file mode 100644 index 0000000..9136bc8 --- /dev/null +++ b/src/components/NavigationBar.tsx @@ -0,0 +1,86 @@ +import { useState, useEffect } from 'react' +import SearchBar from './SearchBar' +import Link from 'next/link' +import ThemeSwitcher from './ThemeSwitcher' + +interface NavigationBarProps { + onSelectSymbol: (symbol: string) => void +} + +const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => { + const [currency, setCurrency] = useState('USD') + const [watchlistView, setWatchlistView] = useState('priceChange') + const [dropdownOpen, setDropdownOpen] = useState(false) + + const handleCurrencyChange = (event: React.ChangeEvent) => { + setCurrency(event.target.value) + } + + const handleWatchlistViewChange = (event: React.ChangeEvent) => { + setWatchlistView(event.target.value) + } + + return ( + + ) +} + +export default NavigationBar \ No newline at end of file diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index f370b13..56b8add 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,7 +1,11 @@ import { useState, useEffect, useCallback } from 'react' import debounce from 'lodash.debounce' -const SearchBar = ({ onSelectSymbol }: { onSelectSymbol: (symbol: string) => void }) => { +interface SearchBarProps { + onSelectSymbol: (symbol: string) => void +} + +const SearchBar = ({ onSelectSymbol }: SearchBarProps) => { const [query, setQuery] = useState('') const [suggestions, setSuggestions] = useState<{ symbol: string, description: string }[]>([]) const [loading, setLoading] = useState(false) @@ -13,7 +17,6 @@ const SearchBar = ({ onSelectSymbol }: { onSelectSymbol: (symbol: string) => voi 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 { diff --git a/src/components/StockPrice.tsx b/src/components/StockPrice.tsx index a5ce1e8..b3b95f7 100644 --- a/src/components/StockPrice.tsx +++ b/src/components/StockPrice.tsx @@ -1,8 +1,12 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' +import Ticker from './Ticker' import SearchBar from './SearchBar' -const StockPrice = () => { - const [symbol, setSymbol] = useState('') +interface StockPriceProps { + symbol: string +} + +const StockPrice = ({ symbol }: StockPriceProps) => { const [price, setPrice] = useState(null) const [error, setError] = useState('') @@ -13,18 +17,27 @@ const StockPrice = () => { const data = await res.json() if (data.error) { setError(data.error) + setPrice(null) } else { setPrice(data.c) - setSymbol(selectedSymbol) } } catch (err) { setError('Failed to fetch stock price') + setPrice(null) } } + useEffect(() => { + let intervalId: NodeJS.Timeout + if (symbol) { + fetchStockPrice(symbol) + intervalId = setInterval(() => fetchStockPrice(symbol), 300000) // 300000 ms = 5 minutes + } + return () => clearInterval(intervalId) + }, [symbol]) + return (
- {symbol && (

Symbol: {symbol}

@@ -38,6 +51,7 @@ const StockPrice = () => {

{error}

)} +
)} diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx new file mode 100644 index 0000000..0d0b9fe --- /dev/null +++ b/src/components/ThemeSwitcher.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; + +const ThemeSwitcher = () => { + const [theme, setTheme] = useState('light'); + + useEffect(() => { + if (theme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [theme]); + + const handleThemeToggle = () => { + setTheme(theme === 'light' ? 'dark' : 'light'); + }; + + return ( + + ); +}; + +export default ThemeSwitcher; \ No newline at end of file diff --git a/src/components/Ticker.tsx b/src/components/Ticker.tsx new file mode 100644 index 0000000..598510d --- /dev/null +++ b/src/components/Ticker.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react' +import { fetchQuote } from '../utils/sparkle' +import { tradeConditions } from '../utils/tradeConditions' + +interface Trade { + p: number; // Price + s: string; // Symbol + t: number; // Timestamp + v: number; // Volume + c?: number[]; // Conditions (DOES NOT EXIST FOR ALL TRADES) +} + +const Ticker = ({ symbol }: { symbol: string }) => { + const [trades, setTrades] = useState([]) + const [bid, setBid] = useState(null) + const [ask, setAsk] = useState(null) + const [webSocketInitialized, setWebSocketInitialized] = useState(false) + + useEffect(() => { + // Fetch initial bid and ask prices + const initializePrices = async () => { + try { + const quote = await fetchQuote(symbol) + setBid(quote.b) + setAsk(quote.a) + } catch (error) { + console.error('Failed to fetch initial bid/ask prices:', error) + } + } + + initializePrices() + }, [symbol]) + + useEffect(() => { + const initializeWebSocket = async () => { + try { + const response = await fetch('/api/ws/start') + const data = await response.json() + if (data.status === 'WebSocket server is running') { + setWebSocketInitialized(true) + } + } catch (error) { + console.error('Failed to initialize WebSocket:', error) + } + } + + initializeWebSocket() + }, []) + + useEffect(() => { + if (!webSocketInitialized) return + const socket = new WebSocket(`${process.env.NEXT_PUBLIC_SPARKLE_BASE_URL!.replace('http', 'ws')}/ws/trades`) + + socket.onopen = () => { + console.log('WebSocket connection established') + socket.send(JSON.stringify({ type: 'subscribe', symbol })) + } + + socket.onmessage = (event) => { + const data = JSON.parse(event.data) + if (data.type === 'trade') { + setTrades((prevTrades) => [...data.data, ...prevTrades]) + } + } + + socket.onclose = () => { + console.log('WebSocket connection closed') + } + + return () => { + socket.send(JSON.stringify({ type: 'unsubscribe', symbol })) + socket.close() + } + }, [symbol, webSocketInitialized]) + + const identifyTradeType = (trade: Trade) => { + if (bid !== null && ask !== null) { + if (trade.p >= ask) { + return 'Buy' + } else if (trade.p <= bid) { + return 'Sell' + } + } + return 'Unknown' + } + + const getTradeConditions = (trade: Trade) => { + if (trade.c && trade.c.length > 0) { + return trade.c.map(code => tradeConditions[code] || `Unknown Condition: ${code}`).join(', ') + } + return 'No conditions' + } + + return ( +
+

Latest Trades for {symbol}

+
    + {trades.slice(0, 5).map((trade, index) => ( +
  • +
    Price: ${trade.p}
    +
    Volume: {trade.v}
    +
    Time: {new Date(trade.t).toLocaleTimeString()}
    +
    Type: {identifyTradeType(trade)}
    +
    Conditions: {getTradeConditions(trade)}
    +
  • + ))} +
+
+ ) +} + +export default Ticker \ No newline at end of file diff --git a/src/pages/api/ws/start.ts b/src/pages/api/ws/start.ts new file mode 100644 index 0000000..74b30b3 --- /dev/null +++ b/src/pages/api/ws/start.ts @@ -0,0 +1,13 @@ +import { startWebSocket } from '@/utils/sparkle' +import type { NextApiRequest, NextApiResponse } from 'next' + +let isWebSocketRunning = false + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (!isWebSocketRunning) { + startWebSocket(); + isWebSocketRunning = true; + } + + res.status(200).json({ status: 'WebSocket server is running' }) +} \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6880145..ee929bb 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,11 +1,20 @@ -import Header from '../components/Header' +import { useState } from 'react' +import NavigationBar from '../components/NavigationBar' import StockPrice from '../components/StockPrice' export default function Home() { + const [symbol, setSymbol] = useState('') + + const handleSelectSymbol = (selectedSymbol: string) => { + setSymbol(selectedSymbol) + } + return ( -
-
- +
+ +
+ +
) } \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index bd6213e..22660d7 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,3 +1,42 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +:root { + --color-base: #ebe6df; /* Latte base color */ + --color-mantle: #e2ddd6; + --color-crust: #f5f4ed; + --color-surface0: #f2e7dc; + --color-surface1: #ece5df; + --color-surface2: #f4ede8; + --color-overlay0: #dad5d0; + --color-overlay1: #e3ded8; + --color-overlay2: #edebe7; + --color-text: #4c4f52; + --color-subtext1: #55595e; + --color-subtext0: #5f6368; + --background: var(--color-base); + --foreground: var(--color-text); + } + + .dark { + --color-base: #1e1e2e; /* Mocha base color */ + --color-mantle: #181825; + --color-crust: #13111e; + --color-surface0: #313244; + --color-surface1: #3a3c51; + --color-surface2: #4e4f68; + --color-overlay0: #6c6f85; + --color-overlay1: #898ba5; + --color-overlay2: #aabbcc; + --color-text: #cdd6f4; + --color-subtext1: #bac2de; + --color-subtext0: #a6adc8; + --background: var(--color-base); + --foreground: var(--color-text); + } + + body { + background-color: var(--background); + color: var(--foreground); + } \ No newline at end of file diff --git a/src/utils/sparkle.ts b/src/utils/sparkle.ts index 1126fc6..fc446a5 100644 --- a/src/utils/sparkle.ts +++ b/src/utils/sparkle.ts @@ -1,7 +1,7 @@ const SPARKLE_BASE_URL = process.env.NEXT_PUBLIC_SPARKLE_BASE_URL; export const fetchQuote = async (symbol: string) => { - const url = `${SPARKLE_BASE_URL}/quote?symbol=${symbol}` + const url = `${SPARKLE_BASE_URL}/api/v1/quote?symbol=${symbol}` const res = await fetch(url) if (!res.ok) { throw new Error('Error fetching quote') @@ -10,10 +10,19 @@ export const fetchQuote = async (symbol: string) => { } export const fetchSymbols = async (symbol: string) => { - const url = `${SPARKLE_BASE_URL}/search?query=${symbol}` + const url = `${SPARKLE_BASE_URL}/api/v1/search?query=${symbol}` const res = await fetch(url) if (!res.ok) { throw new Error('Error fetching quote') } return res.json() + } + + export const startWebSocket = async () => { + const url = `${SPARKLE_BASE_URL}/ws/start-websocket` + const res = await fetch(url) + if (!res.ok) { + throw new Error('Error starting WebSocket') + } + return res.json() } \ No newline at end of file diff --git a/src/utils/tradeConditions.ts b/src/utils/tradeConditions.ts new file mode 100644 index 0000000..427cf90 --- /dev/null +++ b/src/utils/tradeConditions.ts @@ -0,0 +1,43 @@ +export const tradeConditions: Record = { + 1: "Regular", + 2: "Acquisition", + 3: "Average Price Trade", + 4: "Bunched", + 5: "Cash Sale", + 6: "Distribution", + 7: "Automatic Execution", + 8: "Intermarket Sweep Order", + 9: "Bunched Sold", + 10: "Price Variation Trade", + 11: "Cap Election", + 12: "Odd Lot Trade", + 13: "Rule 127", + 14: "Rule 155", + 15: "Sold last", + 16: "Market Center Official Close", + 17: "Next day", + 18: "Market Center Opening Trade", + 19: "Opening Prints", + 20: "Market Center Official Open", + 21: "Prior Reference Price", + 22: "Seller", + 23: "Split Trade", + 24: "Form-T Trade", + 25: "Extended Hours (Sold Out of Sequence)", + 26: "Contingent Trade", + 27: "Stock Option Trade", + 28: "Cross Trade", + 29: "Yellow Flag", + 30: "Sold (Out of Sequence)", + 31: "Stopped Stock", + 32: "Derivatively Priced", + 33: "Market Center Re-opening Trade", + 34: "Re-opening Prints", + 35: "Market Center Closing Trade", + 36: "Closing Prints", + 37: "Qualified Contingent Trade", + 38: "Placeholder for 611 Exempt", + 39: "Corrected Consolidated Close", + 40: "Opened", + 41: "Trade Through Exempt (TTE)", + }; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index 021c393..ab50aa0 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,19 +1,64 @@ -import type { Config } from "tailwindcss"; +import type { Config } from 'tailwindcss' + +const latte = { + base: '#ebe6df', + mantle: '#e2ddd6', + crust: '#f5f4ed', + surface0: '#f2e7dc', + surface1: '#ece5df', + surface2: '#f4ede8', + overlay0: '#dad5d0', + overlay1: '#e3ded8', + overlay2: '#edebe7', + text: '#4c4f52', + subtext1: '#55595e', + subtext0: '#5f6368' +} + +const mocha = { + base: '#1e1e2e', + mantle: '#181825', + crust: '#13111e', + surface0: '#313244', + surface1: '#3a3c51', + surface2: '#4e4f68', + overlay0: '#6c6f85', + overlay1: '#898ba5', + overlay2: '#aabbcc', + text: '#cdd6f4', + subtext1: '#bac2de', + subtext0: '#a6adc8' +} const config: Config = { content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}' ], + darkMode: 'class', theme: { extend: { colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, + background: 'var(--background)', + foreground: 'var(--foreground)', + base: 'var(--color-base)', + mantle: 'var(--color-mantle)', + crust: 'var(--color-crust)', + surface0: 'var(--color-surface0)', + surface1: 'var(--color-surface1)', + surface2: 'var(--color-surface2)', + overlay0: 'var(--color-overlay0)', + overlay1: 'var(--color-overlay1)', + overlay2: 'var(--color-overlay2)', + text: 'var(--color-text)', + subtext1: 'var(--color-subtext1)', + latte, + mocha + } + } }, - plugins: [], -}; -export default config; + plugins: [] +} + +export default config \ No newline at end of file