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
83 changes: 83 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: CI

on:
push:
branches: [main, master, feature/*]
pull_request:
branches: [main, master]

jobs:
server-tests:
name: Server tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: server/market-trading-service
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache server node modules
uses: actions/cache@v4
with:
path: server/market-trading-service/node_modules
key: ${{ runner.os }}-server-${{ hashFiles('server/market-trading-service/package-lock.json') }}
restore-keys: |
${{ runner.os }}-server-

- name: Install server dependencies
run: npm ci

- name: Run server tests
run: npm test --silent

client-tests:
name: Client tests (conditional)
runs-on: ubuntu-latest
needs: server-tests
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Detect client test script
id: detect_client_tests
run: |
set -e
PKG=client/trading-dashboard/package.json
if [ -f "$PKG" ] && grep -q '"test"' "$PKG"; then
echo "has_tests=true" >> $GITHUB_OUTPUT
else
echo "has_tests=false" >> $GITHUB_OUTPUT
fi

- name: Use Node.js 20
if: steps.detect_client_tests.outputs.has_tests == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache client node modules
if: steps.detect_client_tests.outputs.has_tests == 'true'
uses: actions/cache@v4
with:
path: client/trading-dashboard/node_modules
key: ${{ runner.os }}-client-${{ hashFiles('client/trading-dashboard/package-lock.json') }}
restore-keys: |
${{ runner.os }}-client-

- name: Install client dependencies
if: steps.detect_client_tests.outputs.has_tests == 'true'
run: |
cd client/trading-dashboard
npm ci

- name: Run client tests
if: steps.detect_client_tests.outputs.has_tests == 'true'
run: |
cd client/trading-dashboard
npm test --silent
44 changes: 44 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: E2E (Cypress)

on:
push:
branches: [main, master, feature/*]
pull_request:
branches: [main, master]

jobs:
cypress-run:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Start client & run Cypress (no backend)
uses: cypress-io/github-action@v6
with:
working-directory: client/trading-dashboard
build: npm run build
start: npm start
wait-on: "http://localhost:3000"
wait-on-timeout: 120
browser: chrome
spec: cypress/e2e/**/*.cy.ts
env:
# Point to an unreachable host so fetch fails and client falls back to MOCK_TICKERS
NEXT_PUBLIC_MARKET_TRADING_URL: http://127.0.0.1:59999
CYPRESS_BASE_URL: http://localhost:3000

- name: Upload Cypress videos/screenshots (if failed)
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
path: |
client/trading-dashboard/cypress/videos/**
client/trading-dashboard/cypress/screenshots/**
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,56 @@
# multi-bank-coding-challenge
Full Stack real-time trading dashboard

Full-stack real-time trading dashboard used for the Multi Bank coding challenge. This repository contains two main projects and supporting documentation.

Project layout

- `client/trading-dashboard` — Next.js frontend (UI, E2E + unit tests). See: `client/trading-dashboard/README.md` and `client/trading-dashboard/README.md` for developer instructions.
- `server/market-trading-service` — Node + TypeScript microservice that simulates market data and exposes a small REST API. See: `server/market-trading-service/README.md`.
- `docs/` — architecture and project-scope documentation.
- `docker-compose.yml` & `README.DOCKER.md` — Docker setup for running both services together.

Quick start (recommended: Docker)

1. From the repository root, build and start both services with docker-compose:

```bash
docker-compose build
docker-compose up
```

This will start:

- Frontend: http://localhost:3000
- Backend API: http://localhost:3005 (health endpoint: `/health`)

Local development (without Docker)

- Frontend:

```bash
cd client/trading-dashboard
npm install
npm run dev
```

- Backend:

```bash
cd server/market-trading-service
npm install
npm run dev
```

Tests

- Frontend unit tests: run in `client/trading-dashboard` with `npm run test` (Jest)
- Frontend E2E: run Cypress after starting the frontend (see `client/trading-dashboard/cypress.config.ts`)

Notes

- Each subproject includes its own README with more detailed developer and testing instructions. If you plan to contribute, please follow the subproject README before opening a PR.
- If you want CI/CD integration, I can add GitHub Actions workflows that run unit tests and optional Cypress E2E tests.

License

This project is available under the repository LICENSE file.
32 changes: 32 additions & 0 deletions client/trading-dashboard/app/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { render, screen } from "@testing-library/react";
import Index from "../page";
// IMPORTANT: mock child components BEFORE importing the page so the page uses the mocked versions.
jest.mock("@/components/Hero", () => ({
__esModule: true,
default: () => <div data-testid="hero">Hero Component</div>,
}));

jest.mock("@/components/TickerGrid", () => ({
__esModule: true,
default: () => <div data-testid="ticker-grid">TickerGrid Component</div>,
}));

jest.mock("@/components/NewsFeed", () => ({
__esModule: true,
default: () => <div data-testid="news-feed">NewsFeed Component</div>,
}));

describe("Homepage (Index)", () => {
it("renders all main sections: Hero, TickerGrid, and NewsFeed", () => {
render(<Index />);
expect(screen.getByTestId("hero")).toBeInTheDocument();
expect(screen.getByTestId("ticker-grid")).toBeInTheDocument();
expect(screen.getByTestId("news-feed")).toBeInTheDocument();
});

it("has correct layout structure with min-h-screen and bg-background", () => {
const { container } = render(<Index />);
const mainDiv = container.querySelector(".min-h-screen.bg-background");
expect(mainDiv).toBeInTheDocument();
});
});
54 changes: 54 additions & 0 deletions client/trading-dashboard/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AlertTriangle, Home } from "lucide-react";

const NotFound = () => {
const pathname = usePathname();

// Log the error to the console for debugging purposes
useEffect(() => {
console.error(
`404 Not Found: User attempted to access a non-existent route: ${pathname}`
);
}, [pathname]);

return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="mx-4 w-full max-w-md rounded-2xl p-8 text-center shadow-xl bg-gray-800/60 dark:backdrop-blur-sm">
Comment on lines +18 to +19
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix formatting issues and standardize dark mode handling.

Multiple className strings contain double spaces and inconsistent dark mode class usage:

  1. Double spaces (lines 21, 22, 32): Remove extra whitespace for cleaner code
  2. Inconsistent dark mode (lines 22, 36): The component uses a dark theme by default (bg-gray-900, bg-gray-800/60), but some elements conditionally apply dark: prefix classes. Either commit to a dark-only theme or implement proper light/dark mode support consistently across all elements.
  3. Long className strings (lines 28, 44): Consider using a utility like clsx or cn helper for better readability.

Example fixes:

-    <div className="flex min-h-screen items-center justify-center  bg-gray-900">
+    <div className="flex min-h-screen items-center justify-center bg-gray-900">
-      <div className="mx-4 w-full max-w-md rounded-2xl  p-8 text-center shadow-xl bg-gray-800/60 dark:backdrop-blur-sm">
+      <div className="mx-4 w-full max-w-md rounded-2xl p-8 text-center shadow-xl bg-gray-800/60 backdrop-blur-sm">
-        <h2 className="mt-4 text-2xl font-bold  text-gray-100">
+        <h2 className="mt-4 text-2xl font-bold text-gray-100">

Also applies to: 28-28, 32-32, 36-36, 44-44

🤖 Prompt for AI Agents
In client/trading-dashboard/app/not-found.tsx around lines 21-22 (also affecting
lines 28, 32, 36, 44), clean up extra double spaces in className strings, and
standardize dark mode handling: either treat the component as dark-only (remove
inconsistent dark: prefixes and use dark color classes consistently) or
implement proper light/dark support by adding explicit light-mode classes and
using dark: prefixes consistently for dark overrides; also break very long
className strings into a cn/clsx helper call to improve readability and
maintainability.

<div className="mb-6 flex justify-center">
<AlertTriangle
aria-label="Warning: Page not found"
className="h-16 w-16 text-yellow-500"
/>
</div>

<h1 className="bg-gradient-to-r from-purple-500 to-pink-500 bg-clip-text text-8xl font-extrabold text-transparent">
404
</h1>

<h2 className="mt-4 text-2xl font-bold text-gray-100">
Page Not Found
</h2>

<p className="mt-2 text-gray-500 dark:text-gray-400">
Sorry, the page you are looking for doesn&apos;t exist or has been
moved.
</p>

<div className="mt-8">
<Link
href="/"
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 font-semibold text-white shadow-md transition-all duration-300 hover:bg-blue-700 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
<Home className="h-5 w-5" />
Return to Homepage
</Link>
</div>
</div>
</div>
);
};

export default NotFound;
7 changes: 3 additions & 4 deletions client/trading-dashboard/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import Hero from "@/components/Hero";
import NewsFeed from "@/components/NewsFeed";
import TickerGrid from "@/components/TickerGrid";

const Index = async () => {
// Converted to a synchronous Server Component (no data fetching here) so Jest can render it.
export default function Index() {
return (
<div className="min-h-screen bg-background">
<Hero />
<TickerGrid />
<NewsFeed />
</div>
);
};

export default Index;
}
2 changes: 1 addition & 1 deletion client/trading-dashboard/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Header = () => {
const pathname = usePathname();
return (
<header className="sticky top-0 z-50 w-full border-b border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto max-w-7xl flex h-16 items-center justify-between">
<div className="container px-6 mx-auto max-w-7xl flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary">
<span className="text-lg font-bold text-primary-foreground">
Expand Down
5 changes: 4 additions & 1 deletion client/trading-dashboard/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { TrendingUp, Activity, BarChart3 } from "lucide-react";

const Hero = () => {
return (
<section className="relative overflow-hidden border-b bg-gradient-to-br from-background via-background to-secondary py-20 px-4">
<section
data-testid="hero"
className="relative overflow-hidden border-b bg-gradient-to-br from-background via-background to-secondary py-20 px-4"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,hsl(210_100%_56%/0.1),transparent_50%)]" />

<div className="container mx-auto max-w-7xl relative z-10">
Expand Down
4 changes: 2 additions & 2 deletions client/trading-dashboard/components/NewsFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const NewsFeed = () => {
},
{
title: "Energy Sector Stocks Rise on Supply Chain Improvements",
source: "Financial Times",
source: "The Guardian",
time: "10 hours ago",
category: "Energy",
url: "#",
Expand All @@ -62,7 +62,7 @@ const NewsFeed = () => {
setTimeout(() => {
setNews((prevNews) => [...prevNews].sort(() => Math.random() - 0.5));
}, 500); // optional internal visual delay
}, 2000); // Auto-refresh every 2 seconds
}, 5000); // Auto-refresh every 5 seconds

return () => clearInterval(interval); // Clean up on unmount
}, []);
Expand Down
Loading