-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/front end #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
953b694
14f3942
ec44842
811a475
57d7d96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Docker setup | ||
|
|
||
| This repository contains two services: | ||
|
|
||
| - server: market-trading-service (Node + TypeScript) — exposes port 3005 | ||
| - client: trading-dashboard (Next.js) — exposes port 3000 | ||
|
|
||
| Prerequisites: Docker and docker-compose installed. | ||
|
|
||
| Build and run both services: | ||
|
|
||
| ```bash | ||
| docker-compose build | ||
| docker-compose up | ||
| ``` | ||
|
|
||
| Open the client at: http://localhost:3000 | ||
| The server REST API will be available at: http://localhost:3005/api | ||
|
|
||
| Notes: | ||
|
|
||
| - The server uses environment variable `ALLOWED_ORIGINS` to allow CORS from the client. Default in compose is `http://localhost:3000`. | ||
| - If you make TypeScript or source changes, rebuild the server image to pick up compiled output. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| node_modules | ||
| .next | ||
| .next/cache | ||
| npm-debug.log* | ||
| .env | ||
| .DS_Store | ||
| .git | ||
| .vscode |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
|
||
| # dependencies | ||
| /node_modules | ||
| /.pnp | ||
| .pnp.* | ||
| .yarn/* | ||
| !.yarn/patches | ||
| !.yarn/plugins | ||
| !.yarn/releases | ||
| !.yarn/versions | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| # next.js | ||
| /.next/ | ||
| /out/ | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| # misc | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # debug | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| .pnpm-debug.log* | ||
|
|
||
| # env files (can opt-in for committing if needed) | ||
| .env* | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Build stage | ||
| FROM node:20-alpine AS builder | ||
| WORKDIR /app | ||
|
|
||
| # Install deps and build | ||
| COPY package*.json next.config.ts tsconfig.json ./ | ||
| COPY . . | ||
| RUN npm ci --silent && npm run build | ||
|
|
||
| # Production stage | ||
| FROM node:20-alpine AS runner | ||
| WORKDIR /app | ||
| ENV NODE_ENV=production | ||
| ENV NEXT_TELEMETRY_DISABLED=1 | ||
|
|
||
| # Copy built app and necessary files | ||
| COPY --from=builder /app/.next ./.next | ||
| COPY --from=builder /app/public ./public | ||
| COPY --from=builder /app/package*.json ./ | ||
|
|
||
| # Install only production deps | ||
| RUN npm ci --only=production --silent | ||
|
|
||
| EXPOSE 3000 | ||
| CMD ["npm", "run", "start"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Trading Dashboard (Next.js) | ||
|
|
||
| Multi-bank trading dashboard with real-time ticker data, interactive charts, and market news. | ||
|
|
||
| Quick start | ||
|
|
||
| ```bash | ||
| cd client/trading-dashboard | ||
| npm install | ||
| ``` | ||
|
|
||
| Config / Environment | ||
| - The service uses `.env` for configuration. Please check example.env for details | ||
|
|
||
| To run the development server: | ||
|
|
||
| ```bash | ||
| npm run dev | ||
| # or | ||
| yarn dev | ||
| # or | ||
| pnpm dev | ||
| # or | ||
| bun dev | ||
| ``` | ||
|
|
||
| Testing | ||
|
|
||
| - Unit (Jest): `npm run test` (watch: `npm run test:watch`) | ||
| - E2E (Cypress): start the app then `npm run cypress:open` or `npm run cypress:run` | ||
|
|
||
| Docker | ||
|
|
||
| - The repository root contains `docker-compose.yml` to build and run both services. Use `docker-compose up` from the root to start both client and server. | ||
|
|
||
| CI suggestions | ||
|
|
||
| - Add a job that runs `npm ci` and `npm run test` for unit tests. | ||
| - For E2E, consider a separate job that builds the app and runs Cypress in headless mode (use cypress/browsers Docker images). |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,190 @@ | ||||||
| "use client"; | ||||||
|
|
||||||
| import { useEffect, useState } from "react"; | ||||||
|
|
||||||
| import { BarChart3, AlertCircle, Menu, X, Moon, Sun } from "lucide-react"; | ||||||
| import { Sidebar } from "@/components/Sidebar"; | ||||||
| import { useTradingData } from "@/hooks/useTradingData"; | ||||||
| import { Ticker } from "@/types"; | ||||||
| import { TickerDetails } from "@/components/TickerDetails"; | ||||||
| import { StatsGrid } from "@/components/StatsGrid"; | ||||||
| import { PriceChart } from "@/components/PriceChart"; | ||||||
| import { useDarkMode } from "@/hooks/userDarkMode"; | ||||||
|
|
||||||
| const TradingDashboard = () => { | ||||||
| const [sidebarOpen, setSidebarOpen] = useState(false); | ||||||
|
|
||||||
| const { | ||||||
| tickers, | ||||||
| selectedTicker, | ||||||
| setSelectedTicker, | ||||||
| chartData, | ||||||
| chartDays, | ||||||
| setChartDays, | ||||||
| loading, | ||||||
| error, | ||||||
| priceStatus, | ||||||
| } = useTradingData(); | ||||||
|
|
||||||
| const { isDarkMode, setIsDarkMode } = useDarkMode(); | ||||||
|
|
||||||
| // Toggle theme and save to localStorage | ||||||
| const toggleTheme = () => { | ||||||
| setIsDarkMode((prev) => { | ||||||
| const newTheme = !prev; | ||||||
| localStorage.setItem("theme", newTheme ? "dark" : "light"); | ||||||
| return newTheme; | ||||||
| }); | ||||||
| }; | ||||||
|
|
||||||
| const handleSelectTicker = (ticker: Ticker) => { | ||||||
| setSelectedTicker(ticker); | ||||||
| setSidebarOpen(false); // Close sidebar on mobile after selection | ||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <div className="min-h-screen bg-gray-900"> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Background color should respect theme state. The root div uses a hardcoded - <div className="min-h-screen bg-gray-900">
+ <div className={`min-h-screen ${isDarkMode ? "bg-gray-900" : "bg-gray-50"}`}>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| {/* Header */} | ||||||
| <header | ||||||
| className={`${ | ||||||
| isDarkMode | ||||||
| ? "bg-gray-950 border-gray-800" | ||||||
| : "bg-white border-gray-200" | ||||||
| } border-b sticky top-0 z-50`} | ||||||
| > | ||||||
| <div className="px-4 sm:px-6 lg:px-8 lg:pr-16"> | ||||||
| <div className="flex items-center justify-between h-16"> | ||||||
| <div className="flex items-center space-x-1"> | ||||||
| <button | ||||||
| onClick={() => setSidebarOpen(!sidebarOpen)} | ||||||
| className={`lg:hidden p-2 rounded-lg ${ | ||||||
| isDarkMode ? "hover:bg-gray-800" : "hover:bg-gray-100" | ||||||
| } transition-colors`} | ||||||
| aria-label={sidebarOpen ? "Close menu" : "Open menu"} | ||||||
| > | ||||||
| {sidebarOpen ? ( | ||||||
| <X | ||||||
| className={`w-5 h-5 ${ | ||||||
| isDarkMode ? "text-gray-400" : "text-gray-600" | ||||||
| }`} | ||||||
| /> | ||||||
| ) : ( | ||||||
| <Menu | ||||||
| className={`w-5 h-5 ${ | ||||||
| isDarkMode ? "text-gray-400" : "text-gray-600" | ||||||
| }`} | ||||||
| /> | ||||||
| )} | ||||||
| </button> | ||||||
| <div className="flex items-center space-x-2 overflow-hidden"></div> | ||||||
| </div> | ||||||
| <div className="flex items-center space-x-2"> | ||||||
| <div | ||||||
| className={`hidden sm:flex items-center space-x-2 px-3 py-1.5 bg-green-50 dark:bg-green-900 rounded-lg`} | ||||||
| > | ||||||
| <div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" /> | ||||||
| <span | ||||||
| className={`text-sm font-medium ${ | ||||||
| isDarkMode ? "text-green-400" : "text-green-700" | ||||||
| }`} | ||||||
| > | ||||||
| Live | ||||||
| </span> | ||||||
| </div> | ||||||
| <button | ||||||
| onClick={toggleTheme} | ||||||
| className={`p-2 rounded-lg ${ | ||||||
| isDarkMode | ||||||
| ? "bg-gray-800 hover:bg-gray-700" | ||||||
| : "bg-gray-100 hover:bg-gray-200" | ||||||
| } transition-colors`} | ||||||
| aria-label={ | ||||||
| isDarkMode ? "Switch to light mode" : "Switch to dark mode" | ||||||
| } | ||||||
| > | ||||||
| {isDarkMode ? ( | ||||||
| <Sun className="w-5 h-5 text-yellow-400" /> | ||||||
| ) : ( | ||||||
| <Moon className="w-5 h-5 text-gray-600" /> | ||||||
| )} | ||||||
| </button> | ||||||
| </div> | ||||||
| </div> | ||||||
| </div> | ||||||
| </header> | ||||||
|
|
||||||
| {/* Main layout */} | ||||||
| <div className="flex h-[calc(100vh-64px)]"> | ||||||
| {/* Sidebar */} | ||||||
| <Sidebar | ||||||
| tickers={tickers} | ||||||
| selectedTicker={selectedTicker} | ||||||
| onSelectTicker={handleSelectTicker} | ||||||
| isLoading={loading.tickers} | ||||||
| priceStatus={priceStatus} | ||||||
| isOpen={sidebarOpen} | ||||||
| isDarkMode={isDarkMode} | ||||||
| /> | ||||||
|
|
||||||
| {/* Main content */} | ||||||
| <main | ||||||
| className={`flex-grow p-6 overflow-y-auto ${ | ||||||
| isDarkMode ? "bg-gray-900" : "bg-gray-50" | ||||||
| }`} | ||||||
| > | ||||||
| {error && ( | ||||||
| <div | ||||||
| className={`${ | ||||||
| isDarkMode | ||||||
| ? "bg-red-800 border-red-700 text-red-200" | ||||||
| : "bg-red-100 border-red-400 text-red-700" | ||||||
| } px-4 py-3 rounded-lg relative mb-6 flex items-center`} | ||||||
| role="alert" | ||||||
| > | ||||||
| <AlertCircle | ||||||
| className={`w-5 h-5 mr-3 ${ | ||||||
| isDarkMode ? "text-red-300" : "text-red-700" | ||||||
| }`} | ||||||
| aria-hidden="true" | ||||||
| /> | ||||||
| <span className="block sm:inline">{error}</span> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {selectedTicker ? ( | ||||||
| <> | ||||||
| {/* Header Info */} | ||||||
| <TickerDetails ticker={selectedTicker} isDarkMode={isDarkMode} /> | ||||||
|
|
||||||
| {/* Stats Cards */} | ||||||
| <StatsGrid ticker={selectedTicker} isDarkMode={isDarkMode} /> | ||||||
|
|
||||||
| {/* Chart Section */} | ||||||
| <PriceChart | ||||||
| data={chartData} | ||||||
| chartDays={chartDays} | ||||||
| onChartDaysChange={setChartDays} | ||||||
| isDarkMode={isDarkMode} | ||||||
| isLoading={loading.chart} | ||||||
| /> | ||||||
| </> | ||||||
| ) : ( | ||||||
| <div | ||||||
| className={`flex flex-col justify-center items-center h-full ${ | ||||||
| isDarkMode ? "text-gray-400" : "text-gray-500" | ||||||
| }`} | ||||||
| > | ||||||
| <BarChart3 className="w-16 h-16 mb-4" aria-hidden="true" /> | ||||||
| <h2 className="text-xl font-semibold">No Ticker Selected</h2> | ||||||
| <p className="text-center max-w-xs"> | ||||||
| Please select a ticker from the list to view details. | ||||||
| </p> | ||||||
| </div> | ||||||
| )} | ||||||
| </main> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export default TradingDashboard; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| "use client"; | ||
|
|
||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||
| import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; | ||
| import { useState } from "react"; | ||
|
|
||
| export default function Providers({ children }: { children: React.ReactNode }) { | ||
| const [queryClient] = useState( | ||
| () => | ||
| new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| staleTime: 60 * 1000, | ||
| refetchOnWindowFocus: false, | ||
| }, | ||
| }, | ||
| }) | ||
| ); | ||
|
|
||
| return ( | ||
| <QueryClientProvider client={queryClient}> | ||
| {children} | ||
| <ReactQueryDevtools initialIsOpen={false} /> | ||
| </QueryClientProvider> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix theme toggle to update the global
darkclasstoggleThemeflipsisDarkModeand updateslocalStorage, but it never updatesdocument.documentElement.classList. Since components like the “Live” pill use Tailwind’sdark:variants, the globaldarkclass stays stuck at whatever the hook set on mount and won’t follow user toggles.Update the toggle to keep the html class in sync:
const toggleTheme = () => { setIsDarkMode((prev) => { - const newTheme = !prev; - localStorage.setItem("theme", newTheme ? "dark" : "light"); - return newTheme; + const newTheme = !prev; + if (typeof document !== "undefined") { + document.documentElement.classList.toggle("dark", newTheme); + } + localStorage.setItem("theme", newTheme ? "dark" : "light"); + return newTheme; }); };That way,
isDarkMode,localStorage, and the Tailwinddark:styles all stay consistent.Also applies to: 82-90