├── .dockerignore
├── public
└── favicon.ico
├── .gitignore
├── app
├── interfaces.ts
├── routes.ts
├── app.css
├── routes
│ ├── home.tsx
│ └── dashboard.tsx
├── root.tsx
├── Login
│ └── login.tsx
└── UserPage
│ └── userPage.tsx
├── react-router.config.ts
├── vite.config.ts
├── Dockerfile
├── tsconfig.json
├── package.json
├── README.md
└── README copy.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | .react-router
2 | build
3 | node_modules
4 | README.md
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Big-Silver/ortex-technical-test/main/public/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | /node_modules/
4 |
5 | # React Router
6 | /.react-router/
7 | /build/
8 |
--------------------------------------------------------------------------------
/app/interfaces.ts:
--------------------------------------------------------------------------------
1 | interface IUser {
2 | id: number;
3 | email: string;
4 | password: string;
5 | }
6 | export { type IUser };
7 |
--------------------------------------------------------------------------------
/app/routes.ts:
--------------------------------------------------------------------------------
1 | import { type RouteConfig, index, route } from "@react-router/dev/routes";
2 |
3 | export default [index("routes/home.tsx"), route("dashboard", "routes/dashboard.tsx"),] satisfies RouteConfig;
4 |
--------------------------------------------------------------------------------
/react-router.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@react-router/dev/config";
2 |
3 | export default {
4 | // Config options...
5 | // Server-side render by default, to enable SPA mode set this to `false`
6 | ssr: true,
7 | } satisfies Config;
8 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { reactRouter } from "@react-router/dev/vite";
2 | import tailwindcss from "@tailwindcss/vite";
3 | import { defineConfig } from "vite";
4 | import tsconfigPaths from "vite-tsconfig-paths";
5 |
6 | export default defineConfig({
7 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
8 | });
9 |
--------------------------------------------------------------------------------
/app/app.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @theme {
4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
6 | }
7 |
8 | html,
9 | body {
10 | @apply bg-white dark:bg-gray-950;
11 |
12 | @media (prefers-color-scheme: dark) {
13 | color-scheme: dark;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/routes/home.tsx:
--------------------------------------------------------------------------------
1 | import type { Route } from "./+types/home";
2 | import { Login } from "../Login/login";
3 |
4 | export function meta({}: Route.MetaArgs) {
5 | return [
6 | { title: "New React Router App" },
7 | { name: "description", content: "Welcome to React Router!" },
8 | ];
9 | }
10 |
11 | export default function Home() {
12 | return ;
13 | }
14 |
--------------------------------------------------------------------------------
/app/routes/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import type { Route } from "./+types/home";
2 | import { UserPage } from "../UserPage/userPage";
3 |
4 | export function meta({ }: Route.MetaArgs) {
5 | return [
6 | { title: "New React Router App" },
7 | { name: "description", content: "Welcome to React Router!" },
8 | ];
9 | }
10 |
11 | export default function Dashboard() {
12 | return ;
13 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS development-dependencies-env
2 | COPY . /app
3 | WORKDIR /app
4 | RUN npm ci
5 |
6 | FROM node:20-alpine AS production-dependencies-env
7 | COPY ./package.json package-lock.json /app/
8 | WORKDIR /app
9 | RUN npm ci --omit=dev
10 |
11 | FROM node:20-alpine AS build-env
12 | COPY . /app/
13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules
14 | WORKDIR /app
15 | RUN npm run build
16 |
17 | FROM node:20-alpine
18 | COPY ./package.json package-lock.json /app/
19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules
20 | COPY --from=build-env /app/build /app/build
21 | WORKDIR /app
22 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "**/*",
4 | "**/.server/**/*",
5 | "**/.client/**/*",
6 | ".react-router/types/**/*"
7 | ],
8 | "compilerOptions": {
9 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
10 | "types": ["node", "vite/client"],
11 | "target": "ES2022",
12 | "module": "ES2022",
13 | "moduleResolution": "bundler",
14 | "jsx": "react-jsx",
15 | "rootDirs": [".", "./.react-router/types"],
16 | "baseUrl": ".",
17 | "paths": {
18 | "~/*": ["./app/*"]
19 | },
20 | "esModuleInterop": true,
21 | "verbatimModuleSyntax": true,
22 | "noEmit": true,
23 | "resolveJsonModule": true,
24 | "skipLibCheck": true,
25 | "strict": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ortex-technical-test",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "react-router build",
7 | "dev": "react-router dev --host",
8 | "start": "react-router-serve ./build/server/index.js",
9 | "typecheck": "react-router typegen && tsc"
10 | },
11 | "dependencies": {
12 | "@react-router/node": "7.10.1",
13 | "@react-router/serve": "7.10.1",
14 | "isbot": "^5.1.31",
15 | "react": "^19.2.3",
16 | "react-dom": "^19.2.3",
17 | "react-router": "7.10.1",
18 | "react-router-dom": "^7.10.1"
19 | },
20 | "devDependencies": {
21 | "@react-router/dev": "7.10.1",
22 | "@tailwindcss/vite": "^4.1.13",
23 | "@types/node": "^22",
24 | "@types/react": "^19.2.7",
25 | "@types/react-dom": "^19.2.3",
26 | "tailwindcss": "^4.1.13",
27 | "typescript": "^5.9.2",
28 | "vite": "^7.1.7",
29 | "vite-tsconfig-paths": "^5.1.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ortex Technical Test
2 |
3 | A modern, production-ready template for building full-stack React applications using React Router.
4 |
5 | ## Getting Started
6 |
7 | ### Installation
8 |
9 | Install the dependencies:
10 |
11 | ```bash
12 | npm install
13 | ```
14 |
15 | ### Development
16 |
17 | Start the development server with HMR:
18 |
19 | ```bash
20 | npm run dev
21 | ```
22 |
23 | Your application will be available at `http://localhost:5173`.
24 |
25 | ## Building for Production
26 |
27 | Create a production build:
28 |
29 | ```bash
30 | npm run build
31 | ```
32 |
33 | ## Deployment
34 |
35 | ### Docker Deployment
36 |
37 | To build and run using Docker:
38 |
39 | ```bash
40 | docker build -t ortex-technical-test .
41 |
42 | # Run the container
43 | docker run -p 3000:3000 ortex-technical-test
44 | ```
45 |
46 | The containerized application can be deployed to any platform that supports Docker, including:
47 |
48 | - AWS ECS
49 | - Google Cloud Run
50 | - Azure Container Apps
51 | - Digital Ocean App Platform
52 | - Fly.io
53 | - Railway
54 |
55 | ### DIY Deployment
56 |
57 | If you're familiar with deploying Node applications, the built-in app server is production-ready.
58 |
59 | Make sure to deploy the output of `npm run build`
60 |
61 | ```
62 | ├── package.json
63 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
64 | ├── build/
65 | │ ├── client/ # Static assets
66 | │ └── server/ # Server-side code
67 | ```
68 |
69 | ## Styling
70 |
71 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
72 |
73 | ---
74 |
75 | Built with ❤️ using React Router.
76 |
--------------------------------------------------------------------------------
/README copy.md:
--------------------------------------------------------------------------------
1 | # Ortex Technical Test
2 |
3 | A modern, production-ready template for building full-stack React applications using React Router.
4 |
5 | ## Getting Started
6 |
7 | ### Installation
8 |
9 | Install the dependencies:
10 |
11 | ```bash
12 | npm install
13 | ```
14 |
15 | ### Development
16 |
17 | Start the development server with HMR:
18 |
19 | ```bash
20 | npm run dev
21 | ```
22 |
23 | Your application will be available at `http://localhost:5173`.
24 |
25 | ## Building for Production
26 |
27 | Create a production build:
28 |
29 | ```bash
30 | npm run build
31 | ```
32 |
33 | ## Deployment
34 |
35 | ### Docker Deployment
36 |
37 | To build and run using Docker:
38 |
39 | ```bash
40 | docker build -t ortex-technical-test .
41 |
42 | # Run the container
43 | docker run -p 3000:3000 ortex-technical-test
44 | ```
45 |
46 | The containerized application can be deployed to any platform that supports Docker, including:
47 |
48 | - AWS ECS
49 | - Google Cloud Run
50 | - Azure Container Apps
51 | - Digital Ocean App Platform
52 | - Fly.io
53 | - Railway
54 |
55 | ### DIY Deployment
56 |
57 | If you're familiar with deploying Node applications, the built-in app server is production-ready.
58 |
59 | Make sure to deploy the output of `npm run build`
60 |
61 | ```
62 | ├── package.json
63 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
64 | ├── build/
65 | │ ├── client/ # Static assets
66 | │ └── server/ # Server-side code
67 | ```
68 |
69 | ## Styling
70 |
71 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
72 |
73 | ---
74 |
75 | Built with ❤️ using React Router.
76 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | isRouteErrorResponse,
3 | Links,
4 | Meta,
5 | Outlet,
6 | Scripts,
7 | ScrollRestoration,
8 | } from "react-router";
9 |
10 | import type { Route } from "./+types/root";
11 | import "./app.css";
12 |
13 | export const links: Route.LinksFunction = () => [
14 | { rel: "preconnect", href: "https://fonts.googleapis.com" },
15 | {
16 | rel: "preconnect",
17 | href: "https://fonts.gstatic.com",
18 | crossOrigin: "anonymous",
19 | },
20 | {
21 | rel: "stylesheet",
22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
23 | },
24 | ];
25 |
26 | export function Layout({ children }: { children: React.ReactNode }) {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {children}
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | export default function App() {
45 | return ;
46 | }
47 |
48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
49 | let message = "Oops!";
50 | let details = "An unexpected error occurred.";
51 | let stack: string | undefined;
52 |
53 | if (isRouteErrorResponse(error)) {
54 | message = error.status === 404 ? "404" : "Error";
55 | details =
56 | error.status === 404
57 | ? "The requested page could not be found."
58 | : error.statusText || details;
59 | } else if (import.meta.env.DEV && error && error instanceof Error) {
60 | details = error.message;
61 | stack = error.stack;
62 | }
63 |
64 | return (
65 |
66 | {message}
67 | {details}
68 | {stack && (
69 |
70 | {stack}
71 |
72 | )}
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/app/Login/login.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export function Login() {
5 | const [email, setEmail] = useState("");
6 | const [password, setPassword] = useState("");
7 | const navigate = useNavigate();
8 |
9 | const handleSubmit = (e: any) => {
10 | e.preventDefault();
11 |
12 | // Save credentials to browser cache (localStorage)
13 | localStorage.setItem(
14 | "ortex_user",
15 | JSON.stringify({ id: 1,email, password })
16 | );
17 | navigate("/dashboard");
18 | };
19 |
20 | return (
21 |
22 |
23 |
ORTEX
24 |
25 |
26 |
56 |
57 |
58 | );
59 | }
--------------------------------------------------------------------------------
/app/UserPage/userPage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { type IUser } from "~/interfaces";
4 |
5 | export function UserPage() {
6 | const [users, setUsers] = useState([] as IUser[]);
7 | const [showReset, setShowReset] = useState(false);
8 | const [selectedUser, setSelectedUser] = useState(null as IUser | null);
9 | const [currentPassword, setCurrentPassword] = useState("");
10 | const [newPassword, setNewPassword] = useState("");
11 | const [errorMessage, setErrorMessage] = useState("");
12 | const [price, setPrice] = useState(null);
13 | const [timestamp, setTimestamp] = useState(null as Date | null);
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | // Check if user is logged in
18 | const storedUser = localStorage.getItem("ortex_user");
19 | if (!storedUser) {
20 | navigate("/");
21 | return;
22 | }
23 |
24 | const userObj = JSON.parse(storedUser) as IUser;
25 | setUsers([{ ...userObj }]);
26 |
27 | const ws = new WebSocket("ws://stream.tradingeconomics.com/?client=guest:guest");
28 | ws.onopen = () => {
29 | ws.send(JSON.stringify({ topic: "subscribe", to: "EURUSD:CUR" }));
30 | };
31 | ws.onmessage = (event) => {
32 | const data = JSON.parse(event.data);
33 | if (data.price && data.dt) {
34 | setPrice(data.price);
35 | setTimestamp(new Date(data.dt));
36 | }
37 | };
38 |
39 | return () => ws.close();
40 | }, [navigate]);
41 |
42 | const handleResetClick = (user: IUser) => {
43 | setSelectedUser(user);
44 | setShowReset(true);
45 | };
46 |
47 | const handleModalClose = () => {
48 | setShowReset(false);
49 | setSelectedUser(null);
50 | };
51 |
52 | const handlePasswordReset = () => {
53 | if (selectedUser) {
54 | if (currentPassword !== selectedUser.password) {
55 | setErrorMessage("Original password is incorrect.");
56 | return;
57 | }
58 |
59 | const updatedUsers = users.map(u =>
60 | u.id === selectedUser.id ? { ...u, password: newPassword } : u
61 | );
62 | setUsers(updatedUsers);
63 |
64 | const storedUser = localStorage.getItem("ortex_user");
65 | if (storedUser) {
66 | const currentUser = JSON.parse(storedUser) as IUser;
67 | if (currentUser.id === selectedUser.id) {
68 | localStorage.setItem("ortex_user", JSON.stringify({ ...currentUser, password: newPassword }));
69 | }
70 | }
71 | handleModalClose();
72 | }
73 | };
74 |
75 | const handleLogout = () => {
76 | localStorage.removeItem("ortex_user");
77 | navigate("/");
78 | };
79 |
80 | return (
81 |
82 |
83 |
{/* placeholder to balance header */}
84 |
85 |
EUR / USD
86 |
{price ?? "Loading..."}
87 | {timestamp &&
Updated: {timestamp.toLocaleString()}
}
88 |
89 |
95 |
96 |
97 |
98 |
Users
99 |
100 |
101 |
102 | | ID |
103 | Email |
104 | Actions |
105 |
106 |
107 |
108 | {users.map((user) => (
109 |
110 | | {user.id} |
111 | {user.email} |
112 |
113 |
119 | |
120 |
121 | ))}
122 |
123 |
124 |
125 | {showReset && (
126 |
127 |
128 |
Reset Password
129 |
Reset password for: {selectedUser?.email}
130 | {errorMessage &&
{errorMessage}
}
131 |
setCurrentPassword(e.target.value)}
135 | placeholder="Original Password"
136 | className="w-full border p-2 rounded mb-2"
137 | />
138 |
setNewPassword(e.target.value)}
142 | placeholder="New Password"
143 | className="w-full border p-2 rounded mb-4"
144 | />
145 |
146 |
152 |
158 |
159 |
160 |
161 | )}
162 |
163 | );
164 | }
165 |
--------------------------------------------------------------------------------