Repository: johnpapa/heroes-react Branch: main Commit: ed1b29461392 Files: 70 Total size: 86.9 KB Directory structure: gitextract_voez_lfk/ ├── .dockerignore ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yml │ │ └── feature-request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── copilot-instructions.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── copilot-setup-steps.yml ├── .gitignore ├── .prettierrc ├── .vscode/ │ ├── launch.json │ └── settings.json ├── AGENTS.md ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── cypress/ │ ├── fixtures/ │ │ └── example.json │ ├── integration/ │ │ └── heroes.spec.js │ ├── plugins/ │ │ └── index.js │ └── support/ │ ├── commands.js │ └── index.js ├── cypress.json ├── db.js ├── db.json ├── docker-compose.debug.yml ├── docker-compose.yml ├── package.json ├── public/ │ ├── index.html │ └── manifest.json ├── routes.json ├── server.js └── src/ ├── About.js ├── App.css ├── App.js ├── components/ │ ├── ButtonFooter.js │ ├── CardContent.js │ ├── HeaderBar.js │ ├── HeaderBarBrand.js │ ├── HeaderBarLinks.js │ ├── InputDetail.js │ ├── ListHeader.js │ ├── Modal.js │ ├── ModalYesNo.js │ ├── NavBar.js │ ├── NotFound.js │ └── index.js ├── heroes/ │ ├── HeroDetail.js │ ├── HeroList.js │ ├── Heroes.js │ └── useHeroes.js ├── index.css ├── index.js ├── serviceWorker.js ├── store/ │ ├── action-utils.js │ ├── config.js │ ├── hero.actions.js │ ├── hero.api.js │ ├── hero.reducer.js │ ├── hero.saga.js │ ├── index.js │ ├── villain.actions.js │ ├── villain.api.js │ ├── villain.reducer.js │ └── villain.saga.js ├── styles.scss └── villains/ ├── VillainDetail.js ├── VillainList.js ├── Villains.js └── useVillains.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules npm-debug.log Dockerfile* docker-compose* .dockerignore .git .gitignore .env */bin */obj README.md LICENSE .vscode ================================================ FILE: .eslintrc.json ================================================ { "extends": [ "eslint:recommended", "plugin:react/recommended", "prettier" ], "rules": { "react/prop-types": 0, "jsx-a11y/label-has-for": 0, "jsx-a11y/anchor-is-valid": 0, "jsx-a11y/click-events-have-key-events": 0, "no-console": 1, "quotes": [ 2, "single", { "avoidEscape": true, "allowTemplateLiterals": true } ] }, "plugins": ["react"], "parserOptions": { "ecmaVersion": 2020, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "env": { "es6": true, "browser": true, "node": true }, "settings": { "react": { "version": "detect" } } } ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yml ================================================ name: Bug Report description: Report a bug in the Heroes React app labels: ["bug"] body: - type: markdown attributes: value: | Thank you for reporting a bug! Please fill out the details below. - type: textarea id: description attributes: label: Bug Description description: A clear description of what the bug is. validations: required: true - type: textarea id: steps attributes: label: Steps to Reproduce description: Steps to reproduce the behavior. placeholder: | 1. Run `npm run quick` 2. Navigate to /heroes 3. Click on a hero to edit 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected Behavior description: What you expected to happen. validations: required: true - type: textarea id: actual attributes: label: Actual Behavior description: What actually happened. validations: required: true - type: input id: node-version attributes: label: Node.js Version placeholder: "14.21.3" - type: dropdown id: browser attributes: label: Browser options: - Chrome - Firefox - Safari - Edge - Other - type: textarea id: additional attributes: label: Additional Context description: Any other context, screenshots, or console errors. ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: Feature Request description: Suggest a new feature or improvement labels: ["enhancement"] body: - type: markdown attributes: value: | Thank you for your suggestion! Please describe the feature you'd like to see. - type: textarea id: description attributes: label: Feature Description description: A clear description of the feature or improvement. validations: required: true - type: textarea id: motivation attributes: label: Motivation description: Why is this feature valuable? What problem does it solve? validations: required: true - type: textarea id: alternatives attributes: label: Alternatives Considered description: Any alternative solutions or features you've considered. - type: textarea id: additional attributes: label: Additional Context description: Any other context, mockups, or examples. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Changes - ## How to Test 1. `npm install` 2. `npm run quick` 3. Navigate to the affected feature (Heroes/Villains) 4. Verify the change works as expected 5. Run `npm run cypress` to confirm e2e tests pass ## Checklist - [ ] `npm run lint` passes - [ ] `npm run build` succeeds - [ ] Cypress e2e tests pass (`npm run cypress`) - [ ] Seed data updated in both `db.json` and `db.js` (if data shape changed) - [ ] Store barrel re-exports updated in `src/store/index.js` (if new store files added) - [ ] Component barrel updated in `src/components/index.js` (if new shared components added) ================================================ FILE: .github/copilot-instructions.md ================================================ # Copilot Instructions — heroes-react ## Project Type React 16 SPA (Create React App) with Redux + Redux-Saga for state management, json-server mock backend, and Cypress for e2e testing. JavaScript only (no TypeScript). ## JavaScript Conventions - Use ES2018+ syntax (arrow functions, destructuring, spread, async/await) - Single quotes for strings (enforced by Prettier and ESLint) - 2-space indentation, no tabs - No semicolons are not enforced — the project uses semicolons - Prefer `const` over `let`; never use `var` - Use template literals for string interpolation - Follow the existing ESLint config in `.eslintrc.json` (extends airbnb + prettier) - Prettier handles formatting — see `.prettierrc` for config - `console` usage triggers a warning (`no-console: 1`); use `const captains = console` alias pattern for intentional logging (see `server.js`, API files) ## React Patterns - **Class components** are used for the root `App.js` — do not convert to functional unless refactoring the whole app - **Functional components with hooks** are used for feature modules (heroes, villains) - **Custom hooks** (`useHeroes`, `useVillains`) wrap `useSelector` + `useDispatch` — always use hooks to connect to Redux, never `connect()` HOC - **Lazy loading** — feature route components use `React.lazy()` + `Suspense` in `App.js` - **Shared components** live in `src/components/` and are barrel-exported from `src/components/index.js` - **Props** — `react/prop-types` is disabled; no PropTypes validation is used ## Redux Conventions - **Action types** follow `'[Feature] VERB_NOUN'` format (e.g., `'[Heroes] LOAD_HERO'`) - Each feature has four store files: `*.actions.js`, `*.reducer.js`, `*.saga.js`, `*.api.js` - **Reducers** use switch/case statements (not createSlice or Redux Toolkit) - **Side effects** are handled by Redux-Saga (generator functions), not thunks - **API calls** use Axios with a base URL from `REACT_APP_API` env var - **Store barrel** — `src/store/index.js` re-exports everything and calls `combineReducers` ## Styling - Use Bulma CSS framework classes for layout and UI components - Custom styles go in `src/styles.scss` (global SCSS) - Component-specific CSS is minimal — prefer Bulma utility classes - Font Awesome for icons ## Testing - **E2E tests** use Cypress — specs live in `cypress/integration/` - Test against the running app on port 8626 - Use `cy.request('POST', '/api/reset', data)` in `beforeEach` to reset json-server data - Import seed data from `../../db` (the `db.js` module) - No unit tests currently exist — if adding unit tests, use the built-in CRA Jest setup ## File Naming - React components: `PascalCase.js` (e.g., `HeroDetail.js`, `NavBar.js`) - Hooks: `camelCase.js` prefixed with `use` (e.g., `useHeroes.js`) - Store files: `entity.type.js` (e.g., `hero.actions.js`, `villain.saga.js`) - Config/utility files: `camelCase.js` or `kebab-case.js` ## Environment Variables - `REACT_APP_API` — API base URL (proxied in dev, full URL in production) - `PORT` — app server port (default 8626) - Dev config in `.env.development`, production in `.env` ## Maintenance Matrix | When this changes... | Also update... | |---|---| | New entity/feature module added | `db.json`, `db.js` (seed data), `routes.json` (rewrites), `src/store/index.js` (register reducers + re-exports), `src/index.js` (run saga), `src/App.js` (route + lazy import), `src/components/NavBar.js` (nav link), `cypress/integration/` (add e2e spec) | | New shared component added | `src/components/index.js` (barrel re-export) | | Redux action types changed | `*.actions.js`, `*.reducer.js`, `*.saga.js` (all three must stay in sync per feature) | | API endpoint changed | `src/store/*.api.js` (HTTP calls), `db.json` (data shape), `routes.json` (rewrites), Cypress tests (assertions) | | Port changed | `.env`, `.env.development`, `cypress.json`, `.vscode/launch.json`, `Dockerfile` (EXPOSE) | | Styling or CSS framework changed | `src/styles.scss`, `src/App.js` (Bulma import), `package.json` (bulma dep), all component JSX | | Dependencies updated | `package.json`, verify `node-sass` compatibility with Node version, update Dockerfile base image if needed | | Docker config changed | `Dockerfile`, `docker-compose.yml`, `docker-compose.debug.yml` | ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: paths-ignore: - "**.md" - "docs/**" - ".github/ISSUE_TEMPLATE/**" - "LICENSE" - ".gitignore" push: branches: [main] paths-ignore: - "**.md" - "docs/**" - ".github/ISSUE_TEMPLATE/**" - "LICENSE" - ".gitignore" jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "14" cache: "npm" - name: Install dependencies run: npm install - name: Lint run: npm run lint - name: Build run: npm run build ================================================ FILE: .github/workflows/copilot-setup-steps.yml ================================================ name: "Copilot Setup Steps" on: workflow_dispatch jobs: setup: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "14" cache: "npm" - name: Install dependencies run: npm install ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: .prettierrc ================================================ { "tabWidth": 2, "useTabs": false, "singleQuote": true } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", "url": "http://localhost:8626", "webRoot": "${workspaceFolder}" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "eslint.enable": true, "peacock.color": "#61dafb", "workbench.colorCustomizations": { "activityBar.background": "#61dafb", "activityBar.activeBorder": "#f925cc", "activityBar.foreground": "#15202b", "activityBar.inactiveForeground": "#15202b99", "activityBarBadge.background": "#f925cc", "activityBarBadge.foreground": "#e7e7e7", "titleBar.activeBackground": "#61dafb", "titleBar.inactiveBackground": "#61dafb99", "titleBar.activeForeground": "#15202b", "titleBar.inactiveForeground": "#15202b99", "statusBar.background": "#61dafb", "statusBarItem.hoverBackground": "#2fcefa", "statusBar.foreground": "#15202b", "activityBar.activeBackground": "#61dafb" } } ================================================ FILE: AGENTS.md ================================================ # Heroes React — Agent Guide This is a **React single-page application** (SPA) — a demo/comparison app showing the Tour of Heroes concept in React. It uses a json-server mock backend, Redux with Redux-Saga for state management, and Cypress for end-to-end testing. Part of a family of comparative apps: [Angular](https://github.com/johnpapa/heroes-angular), [React](https://github.com/johnpapa/heroes-react), [Svelte](https://github.com/johnpapa/heroes-svelte), and [Vue](https://github.com/johnpapa/heroes-vue). ## Repository Structure ``` heroes-react/ ├── .github/ │ ├── copilot-instructions.md # Coding conventions for AI agents │ ├── workflows/ │ │ ├── ci.yml # PR validation (lint + build) │ │ └── copilot-setup-steps.yml # Cloud agent environment setup │ ├── ISSUE_TEMPLATE/ # Bug report and feature request forms │ ├── PULL_REQUEST_TEMPLATE.md # PR checklist │ └── dependabot.yml # Dependency update automation ├── cypress/ │ ├── fixtures/ # Test fixture data │ ├── integration/ # E2E test specs (heroes.spec.js) │ ├── plugins/ # Cypress plugins │ └── support/ # Cypress support files ├── public/ # Static assets served by CRA ├── src/ │ ├── components/ # Shared UI components (Modal, NavBar, HeaderBar, etc.) │ │ └── index.js # Barrel re-export for all shared components │ ├── heroes/ # Heroes feature module │ │ ├── Heroes.js # Route container (lazy-loaded) │ │ ├── HeroList.js # List display component │ │ ├── HeroDetail.js # Edit/add form component │ │ └── useHeroes.js # Custom hook bridging Redux to component │ ├── villains/ # Villains feature module (mirrors heroes/) │ │ ├── Villains.js │ │ ├── VillainList.js │ │ ├── VillainDetail.js │ │ └── useVillains.js │ ├── store/ # Redux store, actions, reducers, sagas, API calls │ │ ├── index.js # combineReducers + barrel re-exports │ │ ├── config.js # API base URL from env │ │ ├── action-utils.js # Shared response parsers │ │ ├── hero.actions.js # Action type constants + action creators │ │ ├── hero.reducer.js # Heroes + selectedHero reducers │ │ ├── hero.saga.js # Saga side-effects for hero CRUD │ │ ├── hero.api.js # Axios HTTP calls for heroes │ │ ├── villain.actions.js # Action type constants + action creators │ │ ├── villain.reducer.js # Villains + selectedVillain reducers │ │ ├── villain.saga.js # Saga side-effects for villain CRUD │ │ └── villain.api.js # Axios HTTP calls for villains │ ├── App.js # Root component with routing (class component) │ ├── About.js # About page │ ├── index.js # Entry point — Redux store + saga setup + ReactDOM.render │ └── styles.scss # Global SCSS styles ├── .vscode/ │ ├── launch.json # Chrome debug config (port 8626) │ └── settings.json # Peacock color + ESLint enabled ├── db.json # json-server seed data (heroes + villains) ├── db.js # Seed data as JS module (used by Cypress) ├── routes.json # json-server route rewrites (/api/* → /*) ├── server.js # Express production server (serves built app) ├── cypress.json # Cypress config (port 8626) ├── Dockerfile # Multi-stage Docker build (Node 10 → production) ├── docker-compose.yml # Docker compose for running the app ├── docker-compose.debug.yml # Docker compose debug override ├── .eslintrc.json # ESLint config (airbnb-based + prettier) ├── .prettierrc # Prettier config (single quotes, 2-space tabs) ├── .env # Production env (port 8626, Azure API URL) ├── .env.development # Dev env (port 8626, proxied API) ├── package.json # Dependencies, scripts, proxy config └── README.md # Project overview and getting started ``` ## Tech Stack - **Language:** JavaScript (ES2018, JSX) - **Framework:** React 16 (class components + hooks) - **State management:** Redux 4 + Redux-Saga - **Routing:** React Router v5 with lazy-loaded routes - **HTTP client:** Axios - **CSS:** Bulma + SASS + Font Awesome - **Mock backend:** json-server on port 8627 with route rewriting - **Bundler:** Create React App (react-scripts 3.x) - **Production server:** Express (server.js) - **Container:** Docker multi-stage build ## Build & Run ```bash # Install dependencies npm install # Run frontend + json-server backend concurrently npm run quick # Frontend only (requires separate API) npm start # Backend only (json-server on port 8627) npm run backend # Production build npm run build # Production server (serves built app on port 8626) node server.js ``` **Ports:** - `8626` — React dev server / production Express server - `8627` — json-server mock API The CRA proxy in `package.json` forwards `/api/*` requests from port 8626 to json-server on port 8627 during development. ## Testing **E2E tests (Cypress):** ```bash # Open Cypress test runner (interactive) npm run cypress # Run everything (app + backend + Cypress) npm run e2e ``` - Tests live in `cypress/integration/` - Cypress uses the same seed data from `db.js` and resets via `json-server-reset` - Configured in `cypress.json` with port 8626 **Unit tests (react-scripts):** ```bash npm test ``` Uses the built-in CRA test runner (Jest). No unit test files currently exist. ## Key Patterns and Conventions - **Feature modules** — heroes and villains each get their own directory under `src/` with a container component, list, detail, and a custom hook - **Redux pattern** — each feature has four store files: `*.actions.js` (constants + creators), `*.reducer.js`, `*.saga.js` (side effects), `*.api.js` (HTTP calls) - **Barrel exports** — `src/store/index.js` re-exports all actions, reducers, and sagas; `src/components/index.js` re-exports all shared components - **Action type naming** — `'[Feature] VERB_NOUN'` format (e.g., `'[Heroes] LOAD_HERO'`) - **Custom hooks** — `useHeroes` and `useVillains` encapsulate `useSelector` + `useDispatch`, providing a clean API to container components - **Lazy loading** — feature modules are code-split with `React.lazy()` and `Suspense` - **API config** — base URL comes from `REACT_APP_API` environment variable via `src/store/config.js` ## Adding a New Feature Module To add a new entity (e.g., "sidekicks"): 1. **Create seed data** — add a `"sidekicks"` array to `db.json` and `db.js` 2. **Add route rewrite** — add `"/sidekick": "/sidekicks"` to `routes.json` 3. **Create store files** in `src/store/`: - `sidekick.actions.js` — action types and creators following the `[Sidekicks] VERB_NOUN` pattern - `sidekick.api.js` — Axios CRUD functions using the API config - `sidekick.reducer.js` — reducer + selectedSidekickReducer - `sidekick.saga.js` — saga watchers and workers for each action 4. **Register in store** — update `src/store/index.js`: - Add `sidekicksReducer` and `selectedSidekickReducer` to `combineReducers` - Re-export all new modules (`export * from './sidekick.actions'`, etc.) 5. **Run the saga** — add `sagaMiddleware.run(sidekickSaga)` in `src/index.js` 6. **Create feature directory** `src/sidekicks/`: - `Sidekicks.js` — container component - `SidekickList.js` — list display - `SidekickDetail.js` — edit/add form - `useSidekicks.js` — custom hook 7. **Add route** — add `` in `App.js` with lazy import 8. **Add nav link** — update `src/components/NavBar.js` 9. **Add Cypress tests** — create `cypress/integration/sidekicks.spec.js` ## Screen Size / Responsive Rules - Bulma handles responsive layout via its grid system (`columns`, `column`) - The app uses a sidebar nav (`NavBar`) + main content area pattern - No custom breakpoints — relies on Bulma defaults ## Common Pitfalls - **Must run both servers** — `npm run quick` starts both the React dev server and json-server. Running `npm start` alone will fail API calls - **json-server reset** — Cypress tests reset data via `json-server-reset` POST to `/api/reset`. If tests fail with stale data, restart json-server - **Port conflicts** — the app expects 8626 (frontend) and 8627 (backend) to be free - **node-sass version** — `node-sass@4.x` requires Node.js < 15. Use Node 14 for local development - **Barrel re-exports** — when adding new shared components, update `src/components/index.js`; when adding new store modules, update `src/store/index.js` - **Docker image is stale** — the Dockerfile uses `node:10.13-alpine` which is very old; update if building Docker images ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Added - AI-ready configuration (AGENTS.md, copilot-instructions.md, CI, issue templates, PR template, dependabot) ================================================ FILE: Dockerfile ================================================ # Client App FROM node:10.13-alpine as client-app LABEL authors="John Papa" WORKDIR /usr/src/app COPY ["package.json", "npm-shrinkwrap.json*", "./"] RUN npm install --silent COPY . . ARG REACT_APP_API ENV REACT_APP_API $REACT_APP_API RUN npm run build # Node server FROM node:10.13-alpine as node-server WORKDIR /usr/src/app COPY ["package.json", "npm-shrinkwrap.json*", "./"] RUN npm install --production --silent && mv node_modules ../ COPY server.js . # Final image FROM node:10.13-alpine WORKDIR /usr/src/app # get the node_modules COPY --from=node-server /usr/src /usr/src # get the client app COPY --from=client-app /usr/src/app/build ./public EXPOSE 8626 CMD ["node", "server.js"] ================================================ FILE: README.md ================================================ # Tour of Heroes [![AI Ready](https://img.shields.io/badge/AI--Ready-yes-brightgreen?style=flat)](https://github.com/johnpapa/ai-ready) This project was created to help represent a fundamental app written with React. The heroes and villains theme is used throughout the app. by [John Papa](http://twitter.com/john_papa) Comparative apps can be found here with [Angular](https://github.com/johnpapa/heroes-angular), [Svelte](https://github.com/johnpapa/heroes-svelte), and [Vue](https://github.com/johnpapa/heroes-vue) ## Why I love JavaScript and the Web! One of the most common questions I hear is "which framework is best?". I like to flip this around and ask you "which is best for you?". The best way to know this is to try it for yourself. I'll follow up with some articles on my experiences with these frameworks but in the meantime, please try it for yourself to gain your own experience with each. ## Live Demos Hosted in [Azure](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) - [Tour of Heroes with Angular](https://papa-heroes-angular.azurewebsites.net) - [Tour of Heroes with React](https://papa-heroes-react.azurewebsites.net) - Tour of Heroes with Svelte - coming soon! - [Tour of Heroes with Vue](https://papa-heroes-vue.azurewebsites.net) ## Getting Started 1. Clone this repository ```bash git clone https://github.com/johnpapa/heroes-react.git cd heroes-react ``` 1. Install the npm packages ```bash npm install ``` 1. Run the app! ```bash npm run quick ``` ## Cypress Tests 1. You can execute all of the UI tests by running the following steps ```bash npm run cypress ``` ## What's in the App Each of these apps contain: Each of the apps written in the various frameworks/libraries has been designed to have similar features. While consistency is key, I want these apps to be comparable, yet done in an way authentic to each respective framework. Each project represents heroes and villains. The user can list them and edit them. Here is a list of those features: - [x] Start from the official quick-start and CLI - [x] Client side routing - [x] Three main routes Heroes, Villains, About - [x] Handles an erroneous route, leading to a PageNotFound component - [x] Active route is highlighted in the nav menu - [x] Routing should use html5 mode, not hash routes - [x] API - [x] JSON server as a backend - [x] App served on one port which can access API on another port proxy or CORS) - [x] HTTP - Uses most common client http libraries for each framework - [x] Styling - [x] Bulma - [x] SASS - [x] Font Awesome - [x] Same exact css in every app - [x] Editing - Heroes and Villains will be editable (add, update, delete) - [x] State/Store - Uses a store for state management - [x] Web development server handles fallback routing - [x] Generic components - [x] Modal - [x] Button Tool - [x] Card - [x] Header bar - [x] List header - [x] Nav bar - [x] Props in and emit events out - [x] Environment variable for the API location ### Why Cypress? Cypress.io makes it easy to run all three apps simultaneously in end to end tests, so you can watch the results while developing. ### Why abstracted CSS? The goal of the project was to show how each framework can be designed to create the same app. Each uses their own specific techniques in a way that is tuned to each framework. However the one caveat I wanted to achieve was to make sure all of them look the same. While I could have used specific styling for each with scoped and styled components, I chose to create a single global styles file that they all share. This allowed me to provide the same look and feel, run the same cypress tests, and focus more on the HTML and JavaScript/TypeScript. ### Why JSON Server? The app uses a JSON server for a backend by default. This allows you to run the code without needing any database engines or cloud accounts. Enjoy! ## Problems or Suggestions [Open an issue here](/issues) ## Thank You Thank you to [Sarah Drasner](https://twitter.com/), [Brian Holt](https://twitter.com/), [Chris Noring](https://twitter.com/), [Craig Shoemaker](https://twitter.com/), and [Ward Bell](https://twitter.com/wardbell) for providing input and reviewing the code in some of the repos for the Angular, React, Svelte, and Vue apps: - [heroes-angular](https://github.com/johnpapa/heroes-angular) - [heroes-react](https://github.com/johnpapa/heroes-react) - [heroes-svelte](https://github.com/johnpapa/heroes-svelte) - [heroes-vue](https://github.com/johnpapa/heroes-vue) ## Resources - [VS Code](https://code.visualstudio.com/?WT.mc_id=javascript-0000-jopapa) - [Azure Free Trial](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa) - [VS Code Extension for Node on Azure](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack&WT.mc_id=javascript-0000-jopapa) - [VS Code Extension Marketplace](https://marketplace.visualstudio.com/vscode?WT.mc_id=javascript-0000-jopapa) - [VS Code - macOS keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf?WT.mc_id=javascript-0000-jopapa) - [VS Code - Windows keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf?WT.mc_id=javascript-0000-jopapa) ### Debugging Resources - [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?WT.mc_id=javascript-0000-jopapa) - [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?WT.mc_id=javascript-0000-jopapa) - [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?WT.mc_id=javascript-0000-jopapa) ## Contributing Contributions are welcome! You can use [Copilot CLI](https://github.com/johnpapa/ai-ready) to help: ``` Add a new feature module called "sidekicks" with CRUD operations ``` ### Getting Started 1. Fork and clone the repo 2. Install dependencies: `npm install` 3. Start the app: `npm run quick` 4. Create a branch: `git checkout -b feat/my-feature` 5. Make changes, then run lint and e2e tests: ```bash npm run lint npm run cypress ``` 6. Push and open a PR See [AGENTS.md](AGENTS.md) for the full contributor guide, architecture, and conventions. ================================================ FILE: cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: cypress/integration/heroes.spec.js ================================================ /// /* eslint-env mocha */ /* global cy expect Cypress */ import data from '../../db'; const hero = data.heroes[3]; const heroCount = 6; const heroToDelete = data.heroes[5]; const newHero = { id: 'heroMadelyn', name: 'Madelyn', description: 'the cat whisperer' }; const port = Cypress.env('port'); const url = `http://localhost:${port}`; const resetData = () => cy.request('POST', `${url}/api/reset`, data); const containsHeroes = count => cy.get('.list .name').should('have.length', count); const detailsAreVisible = visible => { const val = visible ? '' : 'not.'; // return cy.get('.edit-detail input[name=name]').should(`${val}be.visible`) return cy.get('.edit-detail input[name=name]').should(`${val}exist`); }; context('Heroes', () => { beforeEach(() => { resetData().then(() => { cy.visit(url); cy.get('nav ul.menu-list a') .contains('Heroes') .click(); }); }); after(() => resetData()); specify(`Contains ${hero.name}`, () => { cy.get('.list .name').contains(hero.name); }); specify('Contains 6 heroes', () => { containsHeroes(heroCount); }); specify(`Deletes ${heroToDelete.name}`, () => { cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).click(); cy.get(`#modal .modal-yes`).click(); containsHeroes(heroCount - 1); cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should( 'not.exist' ); }); context(`${hero.name} has been Selected`, () => { beforeEach(() => { cy.get(`.list .edit-item[data-id=${hero.id}]`).click(); }); specify(`Shows Details for ${hero.name}`, () => { const match = new RegExp(hero.id); detailsAreVisible(true); cy.get('.edit-detail input[name=id]') .invoke('val') .should('match', match); }); specify(`Hero List is gone`, () => { containsHeroes(0); cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should( 'not.exist' ); cy.get(`.list .delete-item[data-id=${hero.id}]`).should('not.exist'); }); specify(`Saves changes to ${hero.name}`, () => { const newDescription = 'slayer of javascript'; detailsAreVisible(true); cy.get('.edit-detail input[name=description]') .clear() .type(newDescription); cy.get('.edit-detail input[name=description]') .invoke('val') .should('not.match', new RegExp(hero.description)) .and('match', new RegExp(newDescription)); cy.get('.edit-detail .save-button').click(); detailsAreVisible(false); cy.get('.list .description').contains(newDescription); containsHeroes(heroCount); }); specify(`Cancels changes to ${hero.name}`, () => { const newDescription = 'slayer of javascript'; detailsAreVisible(true); cy.get('.edit-detail input[name=description]') .clear() .type(newDescription); cy.get('.edit-detail input[name=description]') .invoke('val') .should('not.match', new RegExp(hero.description)) .and('match', new RegExp(newDescription)); cy.get('.edit-detail .cancel-button').click(); detailsAreVisible(false); cy.get('.list .description').contains(hero.description); containsHeroes(heroCount); }); }); context(`Add New Hero`, () => { beforeEach(() => { cy.get('.content-container .add-button').click(); }); specify(`Saves changes to ${newHero.name}`, () => { detailsAreVisible(true); cy.get('.edit-detail input[name=name]') .clear() .type(newHero.name); cy.get('.edit-detail input[name=description]') .clear() .type(newHero.description); cy.get('.edit-detail .save-button').click(); detailsAreVisible(false); cy.get('.list .description').contains(newHero.description); containsHeroes(heroCount + 1); }); }); context(`Direct Routing`, () => { specify(`Routes to /heroes directly and see hero list`, () => { cy.visit(url); cy.wait(1000); cy.location().should(loc => { expect(loc.host).to.eq(`localhost:${port}`); expect(loc.hostname).to.eq('localhost'); expect(loc.href).to.eq(`${url}/heroes`); expect(loc.origin).to.eq(url); expect(loc.port).to.eq(port); expect(loc.protocol).to.eq('http:'); expect(loc.toString()).to.eq(`${url}/heroes`); }); detailsAreVisible(false); containsHeroes(heroCount); }); }); }); ================================================ FILE: cypress/plugins/index.js ================================================ // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config } ================================================ FILE: cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) ================================================ FILE: cypress/support/index.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: cypress.json ================================================ { "ignoreTestFiles": "/**/examples/*", "env": { "port": "8626" } } ================================================ FILE: db.js ================================================ const heroes = [ { id: 'HeroAslaug', name: 'Aslaug', description: 'warrior queen', }, { id: 'HeroBjorn', name: 'Bjorn Ironside', description: 'king of 9th century Sweden', }, { id: 'HeroIvar', name: 'Ivar the Boneless', description: 'commander of the Great Heathen Army', }, { id: 'HeroLagertha', name: 'Lagertha the Shieldmaiden', description: 'aka Hlaðgerðr', }, { id: 'HeroRagnar', name: 'Ragnar Lothbrok', description: 'aka Ragnar Sigurdsson', }, { id: 'HeroThora', name: 'Thora Town-hart', description: 'daughter of Earl Herrauðr of Götaland', }, ]; const villains = [ { id: 'VillainMadelyn', name: 'Madelyn', description: 'the cat whisperer', }, { id: 'VillainHaley', name: 'Haley', description: 'pen wielder', }, { id: 'VillainElla', name: 'Ella', description: 'fashionista', }, { id: 'VillainLandon', name: 'Landon', description: 'Mandalorian mauler', }, ]; const data = { heroes: heroes, villains: villains }; module.exports = data; ================================================ FILE: db.json ================================================ { "heroes": [ { "id": "HeroAslaug", "name": "Aslaug", "description": "warrior queen" }, { "id": "HeroBjorn", "name": "Bjorn Ironside", "description": "king of 9th century Sweden" }, { "id": "HeroIvar", "name": "Ivar the Boneless", "description": "commander of the Great Heathen Army" }, { "id": "HeroLagertha", "name": "Lagertha the Shieldmaiden", "description": "aka Hlaðgerðr" }, { "id": "HeroRagnar", "name": "Ragnar Lothbrok", "description": "aka Ragnar Sigurdsson" }, { "id": "HeroThora", "name": "Thora Town-hart", "description": "daughter of Earl Herrauðr of Götaland" } ], "villains": [ { "id": "VillainMadelyn", "name": "Madelyn", "description": "the cat whisperer" }, { "id": "VillainHaley", "name": "Haley", "description": "pen wielder" }, { "id": "VillainElla", "name": "Ella", "description": "fashionista" }, { "id": "VillainLandon", "name": "Landon", "description": "Mandalorian mauler" } ] } ================================================ FILE: docker-compose.debug.yml ================================================ version: '2.1' services: heroes-react: image: heroes-react build: . context: . args: REACT_APP_API: ${REACT_APP_API} env_file: - .env.development ports: - 8626:8626 - 9229:9229 command: node --inspect=0.0.0.0:9229 server.js ================================================ FILE: docker-compose.yml ================================================ version: '2.1' services: heroes-react: image: heroes-react build: context: . args: REACT_APP_API: ${REACT_APP_API} env_file: - .env.development ports: - 8626:8626 ================================================ FILE: package.json ================================================ { "name": "heroes-react", "version": "0.2.0", "private": true, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "format": "prettier --write \"src/**/*.{js,jsx}\"", "lint": "eslint \"src/**/*.{js,jsx}\" --quiet", "quick": "concurrently \"npm run backend\" \"npm start\"", "cypress": "npx cypress open", "e2e": "concurrently \"npm run quick\" \"npm run cypress\"", "backend": "json-server --watch db.json --routes routes.json --port 8627 --middlewares ./node_modules/json-server-reset" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "proxy": "http://localhost:8627/", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "axios": "^1.7.9", "bulma": "^0.9.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^8.1.3", "react-router-dom": "^7.14.2", "react-scripts": "5.0.1", "redux": "^4.2.1", "redux-saga": "^1.3.0", "redux-thunk": "^2.4.2", "sass": "^1.83.0" }, "devDependencies": { "concurrently": "^9.1.2", "cypress": "^13.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.3", "json-server": "^0.17.4", "json-server-reset": "^1.6.0", "prettier": "^3.4.2" } } ================================================ FILE: public/index.html ================================================ Heroes React
================================================ FILE: public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: routes.json ================================================ { "/api/*": "/$1", "/hero": "/heroes", "/reset": "/reset" } ================================================ FILE: server.js ================================================ const express = require('express'); const app = express(); const port = process.env.PORT || 8626; const publicweb = process.env.PUBLICWEB || './public'; const captains = console; const start = () => { app.use(express.static(publicweb)); captains.log(`serving ${publicweb}`); app.get('*', (req, res) => { res.sendFile(`index.html`, { root: publicweb }); }); app.listen(port, () => captains.log(`listening on http://localhost:${port}`)); }; start(); ================================================ FILE: src/About.js ================================================ import React from 'react'; const About = () => (

Tour of Heroes

This project was created to help represent a fundamental app written with React. The heroes and villains theme is used throughout the app.

by John Papa


Why

I love JavaScript and the Web! One of the most common questions I hear is “which framework is best?”. I like to flip this around and ask you “which is best for you?”. The best way to know this is to try it for yourself. I'll follow up with some articles on my experiences with these frameworks but in the meantime, please try it for yourself to gain your own experience with each.


Comparative Apps


Live Demos

Hosted in Azure

); export default About; ================================================ FILE: src/App.css ================================================ ================================================ FILE: src/App.js ================================================ import React, { lazy, Suspense } from 'react'; import 'bulma/css/bulma.css'; import './styles.scss'; import { Navigate, Route, Routes } from 'react-router-dom'; import { HeaderBar, NavBar, NotFound } from './components'; import About from './About'; const Heroes = lazy(() => import(/* webpackChunkName: "heroes" */ './heroes/Heroes')); const Villains = lazy(() => import(/* webpackChunkName: "villains" */ './villains/Villains')); function App() { return (
Loading...
}> } /> } /> } /> } /> } />
); } export default App; ================================================ FILE: src/components/ButtonFooter.js ================================================ import React from 'react'; const ButtonFooter = ({ label, className, iconClasses, onClick, dataIndex, dataId }) => { return ( ); }; export default ButtonFooter; ================================================ FILE: src/components/CardContent.js ================================================ import React from 'react'; const CardContent = ({ name, description }) => (
{name}
{description}
); export default CardContent; ================================================ FILE: src/components/HeaderBar.js ================================================ import React from 'react'; import HeaderBarBrand from './HeaderBarBrand'; import HeaderBarLinks from './HeaderBarLinks'; const HeaderBar = () => (
); export default HeaderBar; ================================================ FILE: src/components/HeaderBarBrand.js ================================================ import React from 'react'; import { NavLink } from 'react-router-dom'; const HeaderBarBrand = () => (