diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c54e9c7 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "root": true, + "env": { + "browser": true, + "es2020": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "react", + "react-hooks", + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b83e08d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Dockerfile for Hello World React+Vite application +FROM node:18-alpine + +WORKDIR /app + +# Copy package manifest first for layer caching +COPY package.json ./ + +# Install dependencies +RUN npm install + +# Copy the rest of the source code +COPY . . + +# Expose Vite dev server port +EXPOSE 5173 + +# Start the Vite dev server, binding to all interfaces +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/RUNNING.md b/RUNNING.md index 77896cf..4f3e653 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,84 @@ -# Running the Todo API +# Hello World React+Vite App + +## TEAM_BRIEF +stack: TypeScript/React+Vite +test_runner: npx vitest run +lint_tool: npx eslint src/ --ext .ts,.tsx +coverage_tool: vitest --coverage +coverage_threshold: 80 +coverage_applies: true ## Prerequisites -- Python 3.10 or later +- **Node.js** >= 18 +- **npm** >= 9 +- **Docker** and **Docker Compose** (optional, for containerised workflow) -## Install dependencies +## Local Setup (without Docker) ```bash -pip install fastapi uvicorn pydantic -``` +# Install dependencies +npm install -For running the test suite you will also need: +# Start the development server +npm run dev +# App available at http://localhost:5173 -```bash -pip install httpx pytest +# Run tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage + +# Run linter +npm run lint + +# Build for production +npm run build ``` -## Start the server +## Docker Setup ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 -``` +# Build and start the container +docker compose up --build -The API will be available at . +# App available at http://localhost:5173 -Interactive docs are served at . +# Run tests inside the container +docker compose exec app npm test -## Run the tests +# Run linter inside the container +docker compose exec app npm run lint + +# Stop and clean up +docker compose down +``` + +## Project Structure -```bash -pytest tests/ ``` +├── index.html # HTML entry point for Vite +├── src/ +│ ├── main.tsx # React DOM render entry +│ ├── App.tsx # Main App component (renders Hello World) +│ ├── App.test.tsx # Test suite for App component +│ ├── index.css # Global styles (centred layout) +│ └── setupTests.ts # Vitest setup (jest-dom matchers) +├── package.json # Dependencies and scripts +├── tsconfig.json # TypeScript configuration +├── vite.config.ts # Vite + Vitest configuration +├── .eslintrc.json # ESLint configuration +├── Dockerfile # Container image definition +└── docker-compose.yml # Docker Compose service definition +``` + +## Acceptance Criteria + +1. `npm test` passes — confirms "Hello World" renders correctly +2. The app displays "Hello World" in a centred `

` element +3. `npm run lint` completes with no errors +4. Styles from `src/index.css` are applied (flexbox centering) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7dd0234 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" + +services: + app: + build: . + ports: + - "5173:5173" + volumes: + - ./src:/app/src + - ./index.html:/app/index.html + - ./vite.config.ts:/app/vite.config.ts + environment: + - NODE_ENV=development diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 0000000..e432e0d --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "env": { + "browser": true, + "es2020": true, + "node": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": [ + "react", + "react-hooks", + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:@typescript-eslint/recommended" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off" + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..0e0ac71 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +# Stage 1: Development +FROM node:18-alpine AS dev + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package.json ./ + +# Install dependencies +RUN npm install + +# Copy remaining source code +COPY . . + +# Expose Vite dev server port +EXPOSE 5173 + +# Start dev server +CMD ["npm", "run", "dev"] diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml new file mode 100644 index 0000000..56ddc8d --- /dev/null +++ b/frontend/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.8" + +services: + app: + build: + context: . + dockerfile: Dockerfile + target: dev + ports: + - "5173:5173" + volumes: + - ./src:/app/src + - ./public:/app/public + - ./vite.config.ts:/app/vite.config.ts + - ./tsconfig.json:/app/tsconfig.json + - ./tsconfig.node.json:/app/tsconfig.node.json + environment: + - NODE_ENV=development diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..b280c7d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "hello-world-react-vite", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "A modern React+Vite+TypeScript Hello World application", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src/ --ext .ts,.tsx" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.2", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^23.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vitest": "^1.1.0" + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..f3deb93 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,12 @@ + + + + + + Hello World - React + Vite + + +
+ + + diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 0000000..97f24c3 --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import App from './App'; + +describe('App', () => { + it('renders Hello World', () => { + render(); + const heading = screen.getByText('Hello World'); + expect(heading).toBeInTheDocument(); + }); + + it('renders an h1 element', () => { + render(); + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toHaveTextContent('Hello World'); + }); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..4811e4c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +/** + * Root application component. + * Renders a simple "Hello World" heading. + */ +function App(): React.JSX.Element { + return ( +
+

Hello World

+
+ ); +} + +export default App; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..08a102c --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,31 @@ +/* Global styles */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f5f5f5; + color: #333; +} + +.app { + text-align: center; +} + +h1 { + font-size: 2.5rem; + font-weight: 700; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..5679abf --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +/** + * Application entry point. + * Mounts the root React component into the DOM element with id "root". + */ +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Root element not found. Ensure public/index.html contains
.'); +} + +ReactDOM.createRoot(rootElement).render( + + + , +); diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts new file mode 100644 index 0000000..b5d4010 --- /dev/null +++ b/frontend/src/setupTests.ts @@ -0,0 +1,5 @@ +/** + * Test setup file. + * Imports jest-dom matchers so they are available in all test files. + */ +import '@testing-library/jest-dom'; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..3c7f93a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "types": ["vitest/globals"] + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..603c994 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +/// + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5173, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + css: true, + }, +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..d5fc1f0 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Hello World + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..04dc39c --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "hello-world-app", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "Minimal React Hello World application with TypeScript and Vite", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint src/ --ext .ts,.tsx" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.2", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jsdom": "^23.0.1", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vitest": "^1.1.0" + } +} diff --git a/src/App.module.css b/src/App.module.css new file mode 100644 index 0000000..565ac6c --- /dev/null +++ b/src/App.module.css @@ -0,0 +1,45 @@ +/** + * CSS Module for the App component. + * + * Provides a centred, modern layout with gradient text and subtle animations. + */ + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + font-family: 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + text-align: center; + padding: 2rem; +} + +.heading { + font-size: 3.5rem; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + margin: 0 0 0.5rem; + animation: fadeIn 0.6s ease-out; +} + +.subtitle { + font-size: 1.25rem; + color: #6b7280; + margin: 0; + animation: fadeIn 0.8s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..976027c --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,50 @@ +/** + * Tests for the App component. + * + * Verifies that the main App component renders correctly with + * the expected "Hello World" heading. + */ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import App from './App'; + +describe('App', () => { + it('renders Hello World heading', () => { + render(); + expect(screen.getByText('Hello World')).toBeInTheDocument(); + }); + + it('renders inside an element with class "app"', () => { + const { container } = render(); + const appDiv = container.querySelector('.app'); + expect(appDiv).toBeInTheDocument(); + }); + + it('renders an h1 element', () => { + render(); + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent('Hello World'); + }); + + it('h1 is nested inside the .app container', () => { + const { container } = render(); + const appDiv = container.querySelector('.app'); + expect(appDiv).not.toBeNull(); + const h1 = appDiv!.querySelector('h1'); + expect(h1).not.toBeNull(); + expect(h1!.textContent).toBe('Hello World'); + }); + + it('renders exactly one h1 element', () => { + const { container } = render(); + const headings = container.querySelectorAll('h1'); + expect(headings).toHaveLength(1); + }); + + it('does not render unexpected text', () => { + render(); + expect(screen.queryByText('Goodbye World')).not.toBeInTheDocument(); + }); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..4d7e802 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,17 @@ +/** + * Main App component. + * + * Renders a centered "Hello World" heading inside a container + * with the class name "app". + */ +import './index.css'; + +function App(): JSX.Element { + return ( +
+

Hello World

+
+ ); +} + +export default App; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..c6674a9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,41 @@ +/** + * Global styles for the Hello World application. + * + * Centres content both horizontally and vertically using flexbox. + */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +#root { + width: 100%; +} + +.app { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + text-align: center; +} + +.app h1 { + font-size: 2.5rem; + color: #333; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..95c3f34 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,15 @@ +/** + * Application entry point. + * + * Renders the root App component into the DOM element with id "root". + */ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..437b786 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,7 @@ +/** + * Test setup file for Vitest. + * + * Imports @testing-library/jest-dom to extend Vitest matchers + * with DOM-specific assertions such as toBeInTheDocument(). + */ +import '@testing-library/jest-dom'; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..825f506 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +/** + * Type declarations for CSS modules. + * + * Allows TypeScript to understand default imports from .module.css files. + */ +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e05cb43 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest/globals"] + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..38f649e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +/** + * Vite configuration for the Hello World React application. + * + * Includes the React plugin for JSX transform and Vitest + * configuration for running tests with jsdom. + */ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/setupTests.ts'], + css: true, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2db1e2e --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +/** + * Vitest configuration. + * + * Configures the test environment to use jsdom for DOM APIs + * and sets up global test utilities. + */ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/setupTests.ts'], + include: ['src/**/*.test.{ts,tsx}'], + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.test.{ts,tsx}', 'src/setupTests.ts', 'src/main.tsx'], + }, + }, +});