Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.DOCKER.md
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.
8 changes: 8 additions & 0 deletions client/trading-dashboard/.dockerignore
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
41 changes: 41 additions & 0 deletions client/trading-dashboard/.gitignore
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
25 changes: 25 additions & 0 deletions client/trading-dashboard/Dockerfile
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"]
39 changes: 39 additions & 0 deletions client/trading-dashboard/README.md
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).
190 changes: 190 additions & 0 deletions client/trading-dashboard/app/(dashboard)/dashboard/page.tsx
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;
});
};
Comment on lines +31 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix theme toggle to update the global dark class

toggleTheme flips isDarkMode and updates localStorage, but it never updates document.documentElement.classList. Since components like the “Live” pill use Tailwind’s dark: variants, the global dark class 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 Tailwind dark: styles all stay consistent.

Also applies to: 82-90


const handleSelectTicker = (ticker: Ticker) => {
setSelectedTicker(ticker);
setSidebarOpen(false); // Close sidebar on mobile after selection
};

return (
<div className="min-h-screen bg-gray-900">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Background color should respect theme state.

The root div uses a hardcoded bg-gray-900 class, which doesn't respond to the isDarkMode state. This causes the background to remain dark even in light mode.

-    <div className="min-h-screen bg-gray-900">
+    <div className={`min-h-screen ${isDarkMode ? "bg-gray-900" : "bg-gray-50"}`}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="min-h-screen bg-gray-900">
<div className={`min-h-screen ${isDarkMode ? "bg-gray-900" : "bg-gray-50"}`}>
🤖 Prompt for AI Agents
In client/trading-dashboard/app/(dashboard)/dashboard/page.tsx around line 45,
the root div currently uses a hardcoded "bg-gray-900" which ignores the
isDarkMode theme state; change it to apply background classes conditionally
based on isDarkMode (e.g., choose dark class when true and light class when
false) or use a theme-aware utility (like classNames or template literal) to
toggle between the dark and light background classes so the UI respects the
current theme state.

{/* 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;
26 changes: 26 additions & 0 deletions client/trading-dashboard/app/Providers.tsx
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>
);
}
Binary file added client/trading-dashboard/app/favicon.ico
Binary file not shown.
Loading