[
  {
    "path": ".dockerignore",
    "content": "node_modules\nnpm-debug.log\nDockerfile*\ndocker-compose*\n.dockerignore\n.git\n.gitignore\n.env\n*/bin\n*/obj\nREADME.md\nLICENSE\n.vscode"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:react/recommended\",\n    \"prettier\"\n  ],\n  \"rules\": {\n    \"react/prop-types\": 0,\n    \"jsx-a11y/label-has-for\": 0,\n    \"jsx-a11y/anchor-is-valid\": 0,\n    \"jsx-a11y/click-events-have-key-events\": 0,\n    \"no-console\": 1,\n    \"quotes\": [\n      2,\n      \"single\",\n      {\n        \"avoidEscape\": true,\n        \"allowTemplateLiterals\": true\n      }\n    ]\n  },\n  \"plugins\": [\"react\"],\n  \"parserOptions\": {\n    \"ecmaVersion\": 2020,\n    \"sourceType\": \"module\",\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    }\n  },\n  \"env\": {\n    \"es6\": true,\n    \"browser\": true,\n    \"node\": true\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"detect\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: Report a bug in the Heroes React app\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for reporting a bug! Please fill out the details below.\n  - type: textarea\n    id: description\n    attributes:\n      label: Bug Description\n      description: A clear description of what the bug is.\n    validations:\n      required: true\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to Reproduce\n      description: Steps to reproduce the behavior.\n      placeholder: |\n        1. Run `npm run quick`\n        2. Navigate to /heroes\n        3. Click on a hero to edit\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behavior\n      description: What you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual Behavior\n      description: What actually happened.\n    validations:\n      required: true\n  - type: input\n    id: node-version\n    attributes:\n      label: Node.js Version\n      placeholder: \"14.21.3\"\n  - type: dropdown\n    id: browser\n    attributes:\n      label: Browser\n      options:\n        - Chrome\n        - Firefox\n        - Safari\n        - Edge\n        - Other\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional Context\n      description: Any other context, screenshots, or console errors.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: Feature Request\ndescription: Suggest a new feature or improvement\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thank you for your suggestion! Please describe the feature you'd like to see.\n  - type: textarea\n    id: description\n    attributes:\n      label: Feature Description\n      description: A clear description of the feature or improvement.\n    validations:\n      required: true\n  - type: textarea\n    id: motivation\n    attributes:\n      label: Motivation\n      description: Why is this feature valuable? What problem does it solve?\n    validations:\n      required: true\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered\n      description: Any alternative solutions or features you've considered.\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional Context\n      description: Any other context, mockups, or examples.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Description\n\n<!-- What does this PR do? One or two sentences. -->\n\n## Changes\n\n<!-- List the files changed and why. -->\n\n- \n\n## How to Test\n\n1. `npm install`\n2. `npm run quick`\n3. Navigate to the affected feature (Heroes/Villains)\n4. Verify the change works as expected\n5. Run `npm run cypress` to confirm e2e tests pass\n\n## Checklist\n\n- [ ] `npm run lint` passes\n- [ ] `npm run build` succeeds\n- [ ] Cypress e2e tests pass (`npm run cypress`)\n- [ ] Seed data updated in both `db.json` and `db.js` (if data shape changed)\n- [ ] Store barrel re-exports updated in `src/store/index.js` (if new store files added)\n- [ ] Component barrel updated in `src/components/index.js` (if new shared components added)\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# Copilot Instructions — heroes-react\n\n## Project Type\n\nReact 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).\n\n## JavaScript Conventions\n\n- Use ES2018+ syntax (arrow functions, destructuring, spread, async/await)\n- Single quotes for strings (enforced by Prettier and ESLint)\n- 2-space indentation, no tabs\n- No semicolons are not enforced — the project uses semicolons\n- Prefer `const` over `let`; never use `var`\n- Use template literals for string interpolation\n- Follow the existing ESLint config in `.eslintrc.json` (extends airbnb + prettier)\n- Prettier handles formatting — see `.prettierrc` for config\n- `console` usage triggers a warning (`no-console: 1`); use `const captains = console` alias pattern for intentional logging (see `server.js`, API files)\n\n## React Patterns\n\n- **Class components** are used for the root `App.js` — do not convert to functional unless refactoring the whole app\n- **Functional components with hooks** are used for feature modules (heroes, villains)\n- **Custom hooks** (`useHeroes`, `useVillains`) wrap `useSelector` + `useDispatch` — always use hooks to connect to Redux, never `connect()` HOC\n- **Lazy loading** — feature route components use `React.lazy()` + `Suspense` in `App.js`\n- **Shared components** live in `src/components/` and are barrel-exported from `src/components/index.js`\n- **Props** — `react/prop-types` is disabled; no PropTypes validation is used\n\n## Redux Conventions\n\n- **Action types** follow `'[Feature] VERB_NOUN'` format (e.g., `'[Heroes] LOAD_HERO'`)\n- Each feature has four store files: `*.actions.js`, `*.reducer.js`, `*.saga.js`, `*.api.js`\n- **Reducers** use switch/case statements (not createSlice or Redux Toolkit)\n- **Side effects** are handled by Redux-Saga (generator functions), not thunks\n- **API calls** use Axios with a base URL from `REACT_APP_API` env var\n- **Store barrel** — `src/store/index.js` re-exports everything and calls `combineReducers`\n\n## Styling\n\n- Use Bulma CSS framework classes for layout and UI components\n- Custom styles go in `src/styles.scss` (global SCSS)\n- Component-specific CSS is minimal — prefer Bulma utility classes\n- Font Awesome for icons\n\n## Testing\n\n- **E2E tests** use Cypress — specs live in `cypress/integration/`\n- Test against the running app on port 8626\n- Use `cy.request('POST', '/api/reset', data)` in `beforeEach` to reset json-server data\n- Import seed data from `../../db` (the `db.js` module)\n- No unit tests currently exist — if adding unit tests, use the built-in CRA Jest setup\n\n## File Naming\n\n- React components: `PascalCase.js` (e.g., `HeroDetail.js`, `NavBar.js`)\n- Hooks: `camelCase.js` prefixed with `use` (e.g., `useHeroes.js`)\n- Store files: `entity.type.js` (e.g., `hero.actions.js`, `villain.saga.js`)\n- Config/utility files: `camelCase.js` or `kebab-case.js`\n\n## Environment Variables\n\n- `REACT_APP_API` — API base URL (proxied in dev, full URL in production)\n- `PORT` — app server port (default 8626)\n- Dev config in `.env.development`, production in `.env`\n\n## Maintenance Matrix\n\n| When this changes... | Also update... |\n|---|---|\n| 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) |\n| New shared component added | `src/components/index.js` (barrel re-export) |\n| Redux action types changed | `*.actions.js`, `*.reducer.js`, `*.saga.js` (all three must stay in sync per feature) |\n| API endpoint changed | `src/store/*.api.js` (HTTP calls), `db.json` (data shape), `routes.json` (rewrites), Cypress tests (assertions) |\n| Port changed | `.env`, `.env.development`, `cypress.json`, `.vscode/launch.json`, `Dockerfile` (EXPOSE) |\n| Styling or CSS framework changed | `src/styles.scss`, `src/App.js` (Bulma import), `package.json` (bulma dep), all component JSX |\n| Dependencies updated | `package.json`, verify `node-sass` compatibility with Node version, update Dockerfile base image if needed |\n| Docker config changed | `Dockerfile`, `docker-compose.yml`, `docker-compose.debug.yml` |\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    paths-ignore:\n      - \"**.md\"\n      - \"docs/**\"\n      - \".github/ISSUE_TEMPLATE/**\"\n      - \"LICENSE\"\n      - \".gitignore\"\n  push:\n    branches: [main]\n    paths-ignore:\n      - \"**.md\"\n      - \"docs/**\"\n      - \".github/ISSUE_TEMPLATE/**\"\n      - \"LICENSE\"\n      - \".gitignore\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"14\"\n          cache: \"npm\"\n\n      - name: Install dependencies\n        run: npm install\n\n      - name: Lint\n        run: npm run lint\n\n      - name: Build\n        run: npm run build\n"
  },
  {
    "path": ".github/workflows/copilot-setup-steps.yml",
    "content": "name: \"Copilot Setup Steps\"\non: workflow_dispatch\n\njobs:\n  setup:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"14\"\n          cache: \"npm\"\n\n      - name: Install dependencies\n        run: npm install\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"chrome\",\n      \"request\": \"launch\",\n      \"name\": \"Launch Chrome against localhost\",\n      \"url\": \"http://localhost:8626\",\n      \"webRoot\": \"${workspaceFolder}\"\n    }\n  ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.enable\": true,\n  \"peacock.color\": \"#61dafb\",\n  \"workbench.colorCustomizations\": {\n    \"activityBar.background\": \"#61dafb\",\n    \"activityBar.activeBorder\": \"#f925cc\",\n    \"activityBar.foreground\": \"#15202b\",\n    \"activityBar.inactiveForeground\": \"#15202b99\",\n    \"activityBarBadge.background\": \"#f925cc\",\n    \"activityBarBadge.foreground\": \"#e7e7e7\",\n    \"titleBar.activeBackground\": \"#61dafb\",\n    \"titleBar.inactiveBackground\": \"#61dafb99\",\n    \"titleBar.activeForeground\": \"#15202b\",\n    \"titleBar.inactiveForeground\": \"#15202b99\",\n    \"statusBar.background\": \"#61dafb\",\n    \"statusBarItem.hoverBackground\": \"#2fcefa\",\n    \"statusBar.foreground\": \"#15202b\",\n    \"activityBar.activeBackground\": \"#61dafb\"\n  }\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Heroes React — Agent Guide\n\nThis 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.\n\nPart 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).\n\n## Repository Structure\n\n```\nheroes-react/\n├── .github/\n│   ├── copilot-instructions.md     # Coding conventions for AI agents\n│   ├── workflows/\n│   │   ├── ci.yml                  # PR validation (lint + build)\n│   │   └── copilot-setup-steps.yml # Cloud agent environment setup\n│   ├── ISSUE_TEMPLATE/             # Bug report and feature request forms\n│   ├── PULL_REQUEST_TEMPLATE.md    # PR checklist\n│   └── dependabot.yml              # Dependency update automation\n├── cypress/\n│   ├── fixtures/                   # Test fixture data\n│   ├── integration/                # E2E test specs (heroes.spec.js)\n│   ├── plugins/                    # Cypress plugins\n│   └── support/                    # Cypress support files\n├── public/                         # Static assets served by CRA\n├── src/\n│   ├── components/                 # Shared UI components (Modal, NavBar, HeaderBar, etc.)\n│   │   └── index.js                # Barrel re-export for all shared components\n│   ├── heroes/                     # Heroes feature module\n│   │   ├── Heroes.js               # Route container (lazy-loaded)\n│   │   ├── HeroList.js             # List display component\n│   │   ├── HeroDetail.js           # Edit/add form component\n│   │   └── useHeroes.js            # Custom hook bridging Redux to component\n│   ├── villains/                   # Villains feature module (mirrors heroes/)\n│   │   ├── Villains.js\n│   │   ├── VillainList.js\n│   │   ├── VillainDetail.js\n│   │   └── useVillains.js\n│   ├── store/                      # Redux store, actions, reducers, sagas, API calls\n│   │   ├── index.js                # combineReducers + barrel re-exports\n│   │   ├── config.js               # API base URL from env\n│   │   ├── action-utils.js         # Shared response parsers\n│   │   ├── hero.actions.js         # Action type constants + action creators\n│   │   ├── hero.reducer.js         # Heroes + selectedHero reducers\n│   │   ├── hero.saga.js            # Saga side-effects for hero CRUD\n│   │   ├── hero.api.js             # Axios HTTP calls for heroes\n│   │   ├── villain.actions.js      # Action type constants + action creators\n│   │   ├── villain.reducer.js      # Villains + selectedVillain reducers\n│   │   ├── villain.saga.js         # Saga side-effects for villain CRUD\n│   │   └── villain.api.js          # Axios HTTP calls for villains\n│   ├── App.js                      # Root component with routing (class component)\n│   ├── About.js                    # About page\n│   ├── index.js                    # Entry point — Redux store + saga setup + ReactDOM.render\n│   └── styles.scss                 # Global SCSS styles\n├── .vscode/\n│   ├── launch.json                 # Chrome debug config (port 8626)\n│   └── settings.json               # Peacock color + ESLint enabled\n├── db.json                         # json-server seed data (heroes + villains)\n├── db.js                           # Seed data as JS module (used by Cypress)\n├── routes.json                     # json-server route rewrites (/api/* → /*)\n├── server.js                       # Express production server (serves built app)\n├── cypress.json                    # Cypress config (port 8626)\n├── Dockerfile                      # Multi-stage Docker build (Node 10 → production)\n├── docker-compose.yml              # Docker compose for running the app\n├── docker-compose.debug.yml        # Docker compose debug override\n├── .eslintrc.json                  # ESLint config (airbnb-based + prettier)\n├── .prettierrc                     # Prettier config (single quotes, 2-space tabs)\n├── .env                            # Production env (port 8626, Azure API URL)\n├── .env.development                # Dev env (port 8626, proxied API)\n├── package.json                    # Dependencies, scripts, proxy config\n└── README.md                       # Project overview and getting started\n```\n\n## Tech Stack\n\n- **Language:** JavaScript (ES2018, JSX)\n- **Framework:** React 16 (class components + hooks)\n- **State management:** Redux 4 + Redux-Saga\n- **Routing:** React Router v5 with lazy-loaded routes\n- **HTTP client:** Axios\n- **CSS:** Bulma + SASS + Font Awesome\n- **Mock backend:** json-server on port 8627 with route rewriting\n- **Bundler:** Create React App (react-scripts 3.x)\n- **Production server:** Express (server.js)\n- **Container:** Docker multi-stage build\n\n## Build & Run\n\n```bash\n# Install dependencies\nnpm install\n\n# Run frontend + json-server backend concurrently\nnpm run quick\n\n# Frontend only (requires separate API)\nnpm start\n\n# Backend only (json-server on port 8627)\nnpm run backend\n\n# Production build\nnpm run build\n\n# Production server (serves built app on port 8626)\nnode server.js\n```\n\n**Ports:**\n- `8626` — React dev server / production Express server\n- `8627` — json-server mock API\n\nThe CRA proxy in `package.json` forwards `/api/*` requests from port 8626 to json-server on port 8627 during development.\n\n## Testing\n\n**E2E tests (Cypress):**\n\n```bash\n# Open Cypress test runner (interactive)\nnpm run cypress\n\n# Run everything (app + backend + Cypress)\nnpm run e2e\n```\n\n- Tests live in `cypress/integration/`\n- Cypress uses the same seed data from `db.js` and resets via `json-server-reset`\n- Configured in `cypress.json` with port 8626\n\n**Unit tests (react-scripts):**\n\n```bash\nnpm test\n```\n\nUses the built-in CRA test runner (Jest). No unit test files currently exist.\n\n## Key Patterns and Conventions\n\n- **Feature modules** — heroes and villains each get their own directory under `src/` with a container component, list, detail, and a custom hook\n- **Redux pattern** — each feature has four store files: `*.actions.js` (constants + creators), `*.reducer.js`, `*.saga.js` (side effects), `*.api.js` (HTTP calls)\n- **Barrel exports** — `src/store/index.js` re-exports all actions, reducers, and sagas; `src/components/index.js` re-exports all shared components\n- **Action type naming** — `'[Feature] VERB_NOUN'` format (e.g., `'[Heroes] LOAD_HERO'`)\n- **Custom hooks** — `useHeroes` and `useVillains` encapsulate `useSelector` + `useDispatch`, providing a clean API to container components\n- **Lazy loading** — feature modules are code-split with `React.lazy()` and `Suspense`\n- **API config** — base URL comes from `REACT_APP_API` environment variable via `src/store/config.js`\n\n## Adding a New Feature Module\n\nTo add a new entity (e.g., \"sidekicks\"):\n\n1. **Create seed data** — add a `\"sidekicks\"` array to `db.json` and `db.js`\n2. **Add route rewrite** — add `\"/sidekick\": \"/sidekicks\"` to `routes.json`\n3. **Create store files** in `src/store/`:\n   - `sidekick.actions.js` — action types and creators following the `[Sidekicks] VERB_NOUN` pattern\n   - `sidekick.api.js` — Axios CRUD functions using the API config\n   - `sidekick.reducer.js` — reducer + selectedSidekickReducer\n   - `sidekick.saga.js` — saga watchers and workers for each action\n4. **Register in store** — update `src/store/index.js`:\n   - Add `sidekicksReducer` and `selectedSidekickReducer` to `combineReducers`\n   - Re-export all new modules (`export * from './sidekick.actions'`, etc.)\n5. **Run the saga** — add `sagaMiddleware.run(sidekickSaga)` in `src/index.js`\n6. **Create feature directory** `src/sidekicks/`:\n   - `Sidekicks.js` — container component\n   - `SidekickList.js` — list display\n   - `SidekickDetail.js` — edit/add form\n   - `useSidekicks.js` — custom hook\n7. **Add route** — add `<Route path=\"/sidekicks\" component={Sidekicks} />` in `App.js` with lazy import\n8. **Add nav link** — update `src/components/NavBar.js`\n9. **Add Cypress tests** — create `cypress/integration/sidekicks.spec.js`\n\n## Screen Size / Responsive Rules\n\n- Bulma handles responsive layout via its grid system (`columns`, `column`)\n- The app uses a sidebar nav (`NavBar`) + main content area pattern\n- No custom breakpoints — relies on Bulma defaults\n\n## Common Pitfalls\n\n- **Must run both servers** — `npm run quick` starts both the React dev server and json-server. Running `npm start` alone will fail API calls\n- **json-server reset** — Cypress tests reset data via `json-server-reset` POST to `/api/reset`. If tests fail with stale data, restart json-server\n- **Port conflicts** — the app expects 8626 (frontend) and 8627 (backend) to be free\n- **node-sass version** — `node-sass@4.x` requires Node.js < 15. Use Node 14 for local development\n- **Barrel re-exports** — when adding new shared components, update `src/components/index.js`; when adding new store modules, update `src/store/index.js`\n- **Docker image is stale** — the Dockerfile uses `node:10.13-alpine` which is very old; update if building Docker images\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).\n\n## [Unreleased]\n\n### Added\n\n- AI-ready configuration (AGENTS.md, copilot-instructions.md, CI, issue templates, PR template, dependabot)\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Client App\nFROM node:10.13-alpine as client-app\nLABEL authors=\"John Papa\"\nWORKDIR /usr/src/app\nCOPY [\"package.json\", \"npm-shrinkwrap.json*\", \"./\"]\nRUN npm install --silent\nCOPY . .\nARG REACT_APP_API\nENV REACT_APP_API $REACT_APP_API\nRUN npm run build\n\n# Node server\nFROM node:10.13-alpine as node-server\nWORKDIR /usr/src/app\nCOPY [\"package.json\", \"npm-shrinkwrap.json*\", \"./\"]\nRUN npm install --production --silent && mv node_modules ../\nCOPY server.js .\n\n# Final image\nFROM node:10.13-alpine\nWORKDIR /usr/src/app\n# get the node_modules\nCOPY --from=node-server /usr/src /usr/src\n# get the client app\nCOPY --from=client-app /usr/src/app/build ./public\nEXPOSE 8626\nCMD [\"node\", \"server.js\"]\n"
  },
  {
    "path": "README.md",
    "content": "# Tour of Heroes\n\n[![AI Ready](https://img.shields.io/badge/AI--Ready-yes-brightgreen?style=flat)](https://github.com/johnpapa/ai-ready)\n\nThis project was created to help represent a fundamental app written with React. The heroes and villains theme is used throughout the app.\n\nby [John Papa](http://twitter.com/john_papa)\n\nComparative 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)\n\n## Why\n\nI 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.\n\n## Live Demos\n\nHosted in [Azure](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa)\n\n- [Tour of Heroes with Angular](https://papa-heroes-angular.azurewebsites.net)\n- [Tour of Heroes with React](https://papa-heroes-react.azurewebsites.net)\n- Tour of Heroes with Svelte - coming soon!\n- [Tour of Heroes with Vue](https://papa-heroes-vue.azurewebsites.net)\n\n## Getting Started\n\n1. Clone this repository\n\n   ```bash\n   git clone https://github.com/johnpapa/heroes-react.git\n   cd heroes-react\n   ```\n\n1. Install the npm packages\n\n   ```bash\n   npm install\n   ```\n\n1. Run the app!\n\n   ```bash\n   npm run quick\n   ```\n\n## Cypress Tests\n\n1. You can execute all of the UI tests by running the following steps\n\n   ```bash\n   npm run cypress\n   ```\n\n## What's in the App\n\nEach of these apps contain:\n\nEach 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.\n\nEach project represents heroes and villains. The user can list them and edit them.\n\nHere is a list of those features:\n\n- [x] Start from the official quick-start and CLI\n- [x] Client side routing\n  - [x] Three main routes Heroes, Villains, About\n  - [x] Handles an erroneous route, leading to a PageNotFound component\n  - [x] Active route is highlighted in the nav menu\n  - [x] Routing should use html5 mode, not hash routes\n- [x] API\n  - [x] JSON server as a backend\n  - [x] App served on one port which can access API on another port proxy or CORS)\n  - [x] HTTP - Uses most common client http libraries for each framework\n- [x] Styling\n  - [x] Bulma\n  - [x] SASS\n  - [x] Font Awesome\n  - [x] Same exact css in every app\n- [x] Editing - Heroes and Villains will be editable (add, update, delete)\n- [x] State/Store - Uses a store for state management\n- [x] Web development server handles fallback routing\n- [x] Generic components\n  - [x] Modal\n  - [x] Button Tool\n  - [x] Card\n  - [x] Header bar\n  - [x] List header\n  - [x] Nav bar\n- [x] Props in and emit events out\n- [x] Environment variable for the API location\n\n### Why Cypress?\n\nCypress.io makes it easy to run all three apps simultaneously in end to end tests, so you can watch the results while developing.\n\n### Why abstracted CSS?\n\nThe 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.\n\n### Why JSON Server?\n\nThe 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!\n\n## Problems or Suggestions\n\n[Open an issue here](/issues)\n\n## Thank You\n\nThank 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:\n\n- [heroes-angular](https://github.com/johnpapa/heroes-angular)\n- [heroes-react](https://github.com/johnpapa/heroes-react)\n- [heroes-svelte](https://github.com/johnpapa/heroes-svelte)\n- [heroes-vue](https://github.com/johnpapa/heroes-vue)\n\n## Resources\n\n- [VS Code](https://code.visualstudio.com/?WT.mc_id=javascript-0000-jopapa)\n- [Azure Free Trial](https://azure.microsoft.com/free/?WT.mc_id=javascript-0000-jopapa)\n- [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)\n- [VS Code Extension Marketplace](https://marketplace.visualstudio.com/vscode?WT.mc_id=javascript-0000-jopapa)\n- [VS Code - macOS keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf?WT.mc_id=javascript-0000-jopapa)\n- [VS Code - Windows keys](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf?WT.mc_id=javascript-0000-jopapa)\n\n### Debugging Resources\n\n- [Debugging Angular in VS Code](https://code.visualstudio.com/docs/nodejs/angular-tutorial?WT.mc_id=javascript-0000-jopapa)\n- [Debugging React in VS Code](https://code.visualstudio.com/docs/nodejs/reactjs-tutorial?WT.mc_id=javascript-0000-jopapa)\n- [Debugging Vue in VS Code](https://code.visualstudio.com/docs/nodejs/vuejs-tutorial?WT.mc_id=javascript-0000-jopapa)\n\n## Contributing\n\nContributions are welcome! You can use [Copilot CLI](https://github.com/johnpapa/ai-ready) to help:\n\n```\nAdd a new feature module called \"sidekicks\" with CRUD operations\n```\n\n### Getting Started\n\n1. Fork and clone the repo\n2. Install dependencies: `npm install`\n3. Start the app: `npm run quick`\n4. Create a branch: `git checkout -b feat/my-feature`\n5. Make changes, then run lint and e2e tests:\n   ```bash\n   npm run lint\n   npm run cypress\n   ```\n6. Push and open a PR\n\nSee [AGENTS.md](AGENTS.md) for the full contributor guide, architecture, and conventions.\n"
  },
  {
    "path": "cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}"
  },
  {
    "path": "cypress/integration/heroes.spec.js",
    "content": "/// <reference types=\"cypress\" />\n/* eslint-env mocha */\n/* global cy expect Cypress */\n\nimport data from '../../db';\n\nconst hero = data.heroes[3];\nconst heroCount = 6;\nconst heroToDelete = data.heroes[5];\nconst newHero = {\n  id: 'heroMadelyn',\n  name: 'Madelyn',\n  description: 'the cat whisperer'\n};\n\nconst port = Cypress.env('port');\nconst url = `http://localhost:${port}`;\n\nconst resetData = () => cy.request('POST', `${url}/api/reset`, data);\n\nconst containsHeroes = count =>\n  cy.get('.list .name').should('have.length', count);\n\nconst detailsAreVisible = visible => {\n  const val = visible ? '' : 'not.';\n  // return cy.get('.edit-detail input[name=name]').should(`${val}be.visible`)\n  return cy.get('.edit-detail input[name=name]').should(`${val}exist`);\n};\n\ncontext('Heroes', () => {\n  beforeEach(() => {\n    resetData().then(() => {\n      cy.visit(url);\n      cy.get('nav ul.menu-list a')\n        .contains('Heroes')\n        .click();\n    });\n  });\n\n  after(() => resetData());\n\n  specify(`Contains ${hero.name}`, () => {\n    cy.get('.list .name').contains(hero.name);\n  });\n\n  specify('Contains 6 heroes', () => {\n    containsHeroes(heroCount);\n  });\n\n  specify(`Deletes ${heroToDelete.name}`, () => {\n    cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).click();\n    cy.get(`#modal .modal-yes`).click();\n\n    containsHeroes(heroCount - 1);\n    cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should(\n      'not.exist'\n    );\n  });\n\n  context(`${hero.name} has been Selected`, () => {\n    beforeEach(() => {\n      cy.get(`.list .edit-item[data-id=${hero.id}]`).click();\n    });\n\n    specify(`Shows Details for ${hero.name}`, () => {\n      const match = new RegExp(hero.id);\n      detailsAreVisible(true);\n      cy.get('.edit-detail input[name=id]')\n        .invoke('val')\n        .should('match', match);\n    });\n\n    specify(`Hero List is gone`, () => {\n      containsHeroes(0);\n      cy.get(`.list .delete-item[data-id=${heroToDelete.id}]`).should(\n        'not.exist'\n      );\n      cy.get(`.list .delete-item[data-id=${hero.id}]`).should('not.exist');\n    });\n\n    specify(`Saves changes to ${hero.name}`, () => {\n      const newDescription = 'slayer of javascript';\n      detailsAreVisible(true);\n      cy.get('.edit-detail input[name=description]')\n        .clear()\n        .type(newDescription);\n      cy.get('.edit-detail input[name=description]')\n        .invoke('val')\n        .should('not.match', new RegExp(hero.description))\n        .and('match', new RegExp(newDescription));\n      cy.get('.edit-detail .save-button').click();\n      detailsAreVisible(false);\n      cy.get('.list .description').contains(newDescription);\n      containsHeroes(heroCount);\n    });\n\n    specify(`Cancels changes to ${hero.name}`, () => {\n      const newDescription = 'slayer of javascript';\n      detailsAreVisible(true);\n      cy.get('.edit-detail input[name=description]')\n        .clear()\n        .type(newDescription);\n      cy.get('.edit-detail input[name=description]')\n        .invoke('val')\n        .should('not.match', new RegExp(hero.description))\n        .and('match', new RegExp(newDescription));\n      cy.get('.edit-detail .cancel-button').click();\n      detailsAreVisible(false);\n      cy.get('.list .description').contains(hero.description);\n      containsHeroes(heroCount);\n    });\n  });\n\n  context(`Add New Hero`, () => {\n    beforeEach(() => {\n      cy.get('.content-container .add-button').click();\n    });\n\n    specify(`Saves changes to ${newHero.name}`, () => {\n      detailsAreVisible(true);\n      cy.get('.edit-detail input[name=name]')\n        .clear()\n        .type(newHero.name);\n      cy.get('.edit-detail input[name=description]')\n        .clear()\n        .type(newHero.description);\n      cy.get('.edit-detail .save-button').click();\n      detailsAreVisible(false);\n      cy.get('.list .description').contains(newHero.description);\n      containsHeroes(heroCount + 1);\n    });\n  });\n\n  context(`Direct Routing`, () => {\n    specify(`Routes to /heroes directly and see hero list`, () => {\n      cy.visit(url);\n      cy.wait(1000);\n      cy.location().should(loc => {\n        expect(loc.host).to.eq(`localhost:${port}`);\n        expect(loc.hostname).to.eq('localhost');\n        expect(loc.href).to.eq(`${url}/heroes`);\n        expect(loc.origin).to.eq(url);\n        expect(loc.port).to.eq(port);\n        expect(loc.protocol).to.eq('http:');\n        expect(loc.toString()).to.eq(`${url}/heroes`);\n      });\n      detailsAreVisible(false);\n      containsHeroes(heroCount);\n    });\n  });\n});\n"
  },
  {
    "path": "cypress/plugins/index.js",
    "content": "// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\nmodule.exports = (on, config) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n}\n"
  },
  {
    "path": "cypress/support/commands.js",
    "content": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom commands and overwrite\n// existing commands.\n//\n// For more comprehensive examples of custom\n// commands please read more here:\n// https://on.cypress.io/custom-commands\n// ***********************************************\n//\n//\n// -- This is a parent command --\n// Cypress.Commands.add(\"login\", (email, password) => { ... })\n//\n//\n// -- This is a child command --\n// Cypress.Commands.add(\"drag\", { prevSubject: 'element'}, (subject, options) => { ... })\n//\n//\n// -- This is a dual command --\n// Cypress.Commands.add(\"dismiss\", { prevSubject: 'optional'}, (subject, options) => { ... })\n//\n//\n// -- This is will overwrite an existing command --\n// Cypress.Commands.overwrite(\"visit\", (originalFn, url, options) => { ... })\n"
  },
  {
    "path": "cypress/support/index.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands'\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "cypress.json",
    "content": "{\n  \"ignoreTestFiles\": \"/**/examples/*\",\n  \"env\": {\n    \"port\": \"8626\"\n  }\n}\n"
  },
  {
    "path": "db.js",
    "content": "const heroes = [\n  {\n    id: 'HeroAslaug',\n    name: 'Aslaug',\n    description: 'warrior queen',\n  },\n  {\n    id: 'HeroBjorn',\n    name: 'Bjorn Ironside',\n    description: 'king of 9th century Sweden',\n  },\n  {\n    id: 'HeroIvar',\n    name: 'Ivar the Boneless',\n    description: 'commander of the Great Heathen Army',\n  },\n  {\n    id: 'HeroLagertha',\n    name: 'Lagertha the Shieldmaiden',\n    description: 'aka Hlaðgerðr',\n  },\n  {\n    id: 'HeroRagnar',\n    name: 'Ragnar Lothbrok',\n    description: 'aka Ragnar Sigurdsson',\n  },\n  {\n    id: 'HeroThora',\n    name: 'Thora Town-hart',\n    description: 'daughter of Earl Herrauðr of Götaland',\n  },\n];\n\nconst villains = [\n  {\n    id: 'VillainMadelyn',\n    name: 'Madelyn',\n    description: 'the cat whisperer',\n  },\n  {\n    id: 'VillainHaley',\n    name: 'Haley',\n    description: 'pen wielder',\n  },\n  {\n    id: 'VillainElla',\n    name: 'Ella',\n    description: 'fashionista',\n  },\n  {\n    id: 'VillainLandon',\n    name: 'Landon',\n    description: 'Mandalorian mauler',\n  },\n];\n\nconst data = { heroes: heroes, villains: villains };\n\nmodule.exports = data;\n"
  },
  {
    "path": "db.json",
    "content": "{\n  \"heroes\": [\n    {\n      \"id\": \"HeroAslaug\",\n      \"name\": \"Aslaug\",\n      \"description\": \"warrior queen\"\n    },\n    {\n      \"id\": \"HeroBjorn\",\n      \"name\": \"Bjorn Ironside\",\n      \"description\": \"king of 9th century Sweden\"\n    },\n    {\n      \"id\": \"HeroIvar\",\n      \"name\": \"Ivar the Boneless\",\n      \"description\": \"commander of the Great Heathen Army\"\n    },\n    {\n      \"id\": \"HeroLagertha\",\n      \"name\": \"Lagertha the Shieldmaiden\",\n      \"description\": \"aka Hlaðgerðr\"\n    },\n    {\n      \"id\": \"HeroRagnar\",\n      \"name\": \"Ragnar Lothbrok\",\n      \"description\": \"aka Ragnar Sigurdsson\"\n    },\n    {\n      \"id\": \"HeroThora\",\n      \"name\": \"Thora Town-hart\",\n      \"description\": \"daughter of Earl Herrauðr of Götaland\"\n    }\n  ],\n  \"villains\": [\n    {\n      \"id\": \"VillainMadelyn\",\n      \"name\": \"Madelyn\",\n      \"description\": \"the cat whisperer\"\n    },\n    {\n      \"id\": \"VillainHaley\",\n      \"name\": \"Haley\",\n      \"description\": \"pen wielder\"\n    },\n    {\n      \"id\": \"VillainElla\",\n      \"name\": \"Ella\",\n      \"description\": \"fashionista\"\n    },\n    {\n      \"id\": \"VillainLandon\",\n      \"name\": \"Landon\",\n      \"description\": \"Mandalorian mauler\"\n    }\n  ]\n}"
  },
  {
    "path": "docker-compose.debug.yml",
    "content": "version: '2.1'\n\nservices:\n  heroes-react:\n    image: heroes-react\n    build: .\n      context: .\n      args:\n        REACT_APP_API: ${REACT_APP_API}\n    env_file:\n      - .env.development\n    ports:\n      - 8626:8626\n      - 9229:9229\n    command: node --inspect=0.0.0.0:9229 server.js"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '2.1'\n\nservices:\n  heroes-react:\n    image: heroes-react\n    build:\n      context: .\n      args:\n        REACT_APP_API: ${REACT_APP_API}\n    env_file:\n      - .env.development\n    ports:\n      - 8626:8626\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"heroes-react\",\n  \"version\": \"0.2.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n    \"format\": \"prettier --write \\\"src/**/*.{js,jsx}\\\"\",\n    \"lint\": \"eslint \\\"src/**/*.{js,jsx}\\\" --quiet\",\n    \"quick\": \"concurrently \\\"npm run backend\\\" \\\"npm start\\\"\",\n    \"cypress\": \"npx cypress open\",\n    \"e2e\": \"concurrently \\\"npm run quick\\\" \\\"npm run cypress\\\"\",\n    \"backend\": \"json-server --watch db.json --routes routes.json --port 8627 --middlewares ./node_modules/json-server-reset\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"react-app\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"proxy\": \"http://localhost:8627/\",\n  \"dependencies\": {\n    \"@fortawesome/fontawesome-free\": \"^5.15.4\",\n    \"axios\": \"^1.7.9\",\n    \"bulma\": \"^0.9.4\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-redux\": \"^8.1.3\",\n    \"react-router-dom\": \"^7.14.2\",\n    \"react-scripts\": \"5.0.1\",\n    \"redux\": \"^4.2.1\",\n    \"redux-saga\": \"^1.3.0\",\n    \"redux-thunk\": \"^2.4.2\",\n    \"sass\": \"^1.83.0\"\n  },\n  \"devDependencies\": {\n    \"concurrently\": \"^9.1.2\",\n    \"cypress\": \"^13.17.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-prettier\": \"^5.2.3\",\n    \"json-server\": \"^0.17.4\",\n    \"json-server-reset\": \"^1.6.0\",\n    \"prettier\": \"^3.4.2\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <meta name=\"theme-color\" content=\"#000000\">\n  <!--\n      manifest.json provides metadata used when your web app is added to the\n      homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n  <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\">\n  <link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.4.1/css/all.css\" integrity=\"sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz\"\n    crossorigin=\"anonymous\">\n  <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n  <title>Heroes React</title>\n</head>\n\n<body>\n  <noscript>\n    You need to enable JavaScript to run this app.\n  </noscript>\n  <div id=\"modal\"></div>\n  <div id=\"root\"></div>\n  <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n</body>\n\n</html>"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "routes.json",
    "content": "{\n  \"/api/*\": \"/$1\",\n  \"/hero\": \"/heroes\",\n  \"/reset\": \"/reset\"\n}\n"
  },
  {
    "path": "server.js",
    "content": "const express = require('express');\nconst app = express();\nconst port = process.env.PORT || 8626;\nconst publicweb = process.env.PUBLICWEB || './public';\n\nconst captains = console;\n\nconst start = () => {\n  app.use(express.static(publicweb));\n  captains.log(`serving ${publicweb}`);\n  app.get('*', (req, res) => {\n    res.sendFile(`index.html`, { root: publicweb });\n  });\n  app.listen(port, () => captains.log(`listening on http://localhost:${port}`));\n};\n\nstart();\n"
  },
  {
    "path": "src/About.js",
    "content": "import React from 'react';\n\nconst About = () => (\n  <div className=\"content-container\">\n    <div className=\"content-title-group not-found\">\n      <h2 className=\"title\">Tour of Heroes</h2>\n      <p>\n        This project was created to help represent a fundamental app written\n        with React. The heroes and villains theme is used throughout the app.\n      </p>\n      <p>\n        by\n        <a href=\"http://twitter.com/john_papa\">John Papa</a>\n      </p>\n      <br />\n      <h2 className=\"title\">Why</h2>\n      <p>\n        I love JavaScript and the Web! One of the most common questions I hear\n        is &ldquo;which framework is best?&rdquo;. I like to flip this around and ask you\n        &ldquo;which is best for you?&rdquo;. The best way to know this is to try it for\n        yourself. I&apos;ll follow up with some articles on my experiences with these\n        frameworks but in the meantime, please try it for yourself to gain your\n        own experience with each.\n      </p>\n      <br />\n      <h2 className=\"title\">Comparative Apps</h2>\n      <ul>\n        <li>\n          <a href=\"https://github.com/johnpapa/heroes-angular\">\n            github.com/johnpapa/heroes-angular\n          </a>\n        </li>\n        <li>\n          <a href=\"https://github.com/johnpapa/heroes-react\">\n            github.com/johnpapa/heroes-react\n          </a>\n        </li>\n        <li>\n          <a href=\"https://github.com/johnpapa/heroes-vue\">\n            github.com/johnpapa/heroes-vue\n          </a>\n        </li>\n      </ul>\n      <br />\n      <h2 className=\"title\">Live Demos</h2>\n      <p>\n        Hosted in\n        <a href=\"https://aka.ms/jp-free\">Azure</a>\n      </p>\n      <ul>\n        <li>\n          <a href=\"https://papa-heroes-angular.azurewebsites.net\">\n            Tour of Heroes with Angular\n          </a>\n        </li>\n        <li>\n          <a href=\"https://papa-heroes-react.azurewebsites.net\">\n            Tour of Heroes with React\n          </a>\n        </li>\n        <li>\n          <a href=\"https://papa-heroes-vue.azurewebsites.net\">\n            Tour of Heroes with Vue\n          </a>\n        </li>\n      </ul>\n    </div>\n  </div>\n);\n\nexport default About;\n"
  },
  {
    "path": "src/App.css",
    "content": ""
  },
  {
    "path": "src/App.js",
    "content": "import React, { lazy, Suspense } from 'react';\nimport 'bulma/css/bulma.css';\nimport './styles.scss';\nimport { Navigate, Route, Routes } from 'react-router-dom';\nimport { HeaderBar, NavBar, NotFound } from './components';\nimport About from './About';\n\nconst Heroes = lazy(() => import(/* webpackChunkName: \"heroes\" */ './heroes/Heroes'));\nconst Villains = lazy(() => import(/* webpackChunkName: \"villains\" */ './villains/Villains'));\n\nfunction App() {\n  return (\n    <div>\n      <HeaderBar />\n      <div className=\"section columns\">\n        <NavBar />\n        <main className=\"column\">\n          <Suspense fallback={<div>Loading...</div>}>\n            <Routes>\n              <Route path=\"/\" element={<Navigate to=\"/heroes\" replace />} />\n              <Route path=\"/heroes/*\" element={<Heroes />} />\n              <Route path=\"/villains/*\" element={<Villains />} />\n              <Route path=\"/about\" element={<About />} />\n              <Route path=\"*\" element={<NotFound />} />\n            </Routes>\n          </Suspense>\n        </main>\n      </div>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "src/components/ButtonFooter.js",
    "content": "import React from 'react';\n\nconst ButtonFooter = ({\n  label,\n  className,\n  iconClasses,\n  onClick,\n  dataIndex,\n  dataId\n}) => {\n  return (\n    <button\n      className={'link card-footer-item ' + className}\n      aria-label={label}\n      tabIndex={0}\n      onClick={onClick}\n      data-index={dataIndex}\n      data-id={dataId}\n    >\n      <i className={iconClasses} />\n      <span>{label}</span>\n    </button>\n  );\n};\n\nexport default ButtonFooter;\n"
  },
  {
    "path": "src/components/CardContent.js",
    "content": "import React from 'react';\n\nconst CardContent = ({ name, description }) => (\n  <div className=\"card-content\">\n    <div className=\"content\">\n      <div className=\"name\">{name}</div>\n      <div className=\"description\">{description}</div>\n    </div>\n  </div>\n);\n\nexport default CardContent;\n"
  },
  {
    "path": "src/components/HeaderBar.js",
    "content": "import React from 'react';\nimport HeaderBarBrand from './HeaderBarBrand';\nimport HeaderBarLinks from './HeaderBarLinks';\n\nconst HeaderBar = () => (\n  <header>\n    <nav\n      className=\"navbar has-background-dark is-dark\"\n      role=\"navigation\"\n      aria-label=\"main navigation\"\n    >\n      <HeaderBarBrand />\n      <HeaderBarLinks />\n    </nav>\n  </header>\n);\n\nexport default HeaderBar;\n"
  },
  {
    "path": "src/components/HeaderBarBrand.js",
    "content": "import React from 'react';\nimport { NavLink } from 'react-router-dom';\n\nconst HeaderBarBrand = () => (\n  <div className=\"navbar-brand\">\n    <a\n      className=\"navbar-item\"\n      href=\"https://reactjs.org/\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n    >\n      <i className=\"fab js-logo fa-react fa-2x\" aria-hidden=\"true\" />\n    </a>\n    <NavLink to=\"/\" className=\"navbar-item nav-home\">\n      <span className=\"tour\">TOUR</span>\n      <span className=\"of\">OF</span>\n      <span className=\"heroes\">HEROES</span>\n    </NavLink>\n    <button\n      className=\"link navbar-burger burger\"\n      aria-label=\"menu\"\n      aria-expanded=\"false\"\n      data-target=\"navbarBasicExample\"\n    >\n      <span aria-hidden=\"true\" />\n      <span aria-hidden=\"true\" />\n      <span aria-hidden=\"true\" />\n    </button>\n  </div>\n);\n\nexport default HeaderBarBrand;\n"
  },
  {
    "path": "src/components/HeaderBarLinks.js",
    "content": "import React from 'react';\n\nconst HeaderBarLinks = () => (\n  <div className=\"navbar-menu\">\n    <div className=\"navbar-end\">\n      <div className=\"navbar-item\">\n        <div className=\"buttons\">\n          <a\n            href=\"https://github.com/johnpapa/heroes-react\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <i className=\"fab fa-github fa-2x\" aria-hidden=\"true\" />\n          </a>\n          <a\n            href=\"https://twitter.com/john_papa\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <i className=\"fab fa-twitter fa-2x\" aria-hidden=\"true\" />\n          </a>\n        </div>\n      </div>\n    </div>\n  </div>\n);\n\nexport default HeaderBarLinks;\n"
  },
  {
    "path": "src/components/InputDetail.js",
    "content": "import React from 'react';\n\nconst InputDetail = ({ name, value, placeholder, onChange, readOnly }) => (\n  <div className=\"field\">\n    <label className=\"label\" htmlFor={name}>\n      {name}\n    </label>\n    <input\n      name={name}\n      className=\"input\"\n      type=\"text\"\n      defaultValue={value}\n      placeholder={placeholder}\n      readOnly={!!readOnly}\n      onChange={onChange}\n    />\n  </div>\n);\n\nexport default InputDetail;\n"
  },
  {
    "path": "src/components/ListHeader.js",
    "content": "import React from 'react';\nimport { NavLink } from 'react-router-dom';\n\nconst ListHeader = ({ title, handleAdd, handleRefresh, routePath }) => {\n  return (\n    <div className=\"content-title-group\">\n      <NavLink to={routePath}>\n        <h2 className=\"title\">{title}</h2>\n      </NavLink>\n      <button\n        className=\"button add-button\"\n        onClick={handleAdd}\n        aria-label=\"add\"\n      >\n        <i className=\"fas fa-plus\" aria-hidden=\"true\" />\n      </button>\n      <button\n        className=\"button refresh-button\"\n        onClick={handleRefresh}\n        aria-label=\"refresh\"\n      >\n        <i className=\"fas fa-sync\" aria-hidden=\"true\" />\n      </button>\n    </div>\n  );\n};\n\nexport default ListHeader;\n"
  },
  {
    "path": "src/components/Modal.js",
    "content": "import { Component } from 'react';\nimport { createPortal } from 'react-dom';\n\nconst modalRoot = document.getElementById('modal');\n\nclass Modal extends Component {\n  constructor(props) {\n    super(props);\n    this.el = document.createElement('div');\n  }\n\n  componentDidMount() {\n    modalRoot.appendChild(this.el);\n  }\n\n  componentWillUnmount() {\n    modalRoot.removeChild(this.el);\n  }\n\n  render() {\n    return createPortal(this.props.children, this.el);\n  }\n}\n\nexport default Modal;\n"
  },
  {
    "path": "src/components/ModalYesNo.js",
    "content": "import React from 'react';\n\nimport Modal from './Modal';\n\nconst ModalYesNo = ({ message, onYes, onNo }) => (\n  <Modal>\n    <div className=\"modal is-active\">\n      <div className=\"modal-background\" />\n      <div className=\"modal-card\">\n        <header className=\"modal-card-head\">\n          <p className=\"modal-card-title\">Confirm</p>\n        </header>\n        <section className=\"modal-card-body\">{message}</section>\n        <footer className=\"modal-card-foot card-footer\">\n          <button className=\"button modal-no\" onClick={onNo}>\n            No\n          </button>\n          <button className=\"button is-primary modal-yes\" onClick={onYes}>\n            Yes\n          </button>\n        </footer>\n      </div>\n    </div>\n  </Modal>\n);\n\nexport default ModalYesNo;\n"
  },
  {
    "path": "src/components/NavBar.js",
    "content": "import React from 'react';\nimport { NavLink } from 'react-router-dom';\n\nconst NavBar = props => (\n  <nav className=\"column is-2 menu\">\n    <p className=\"menu-label\">Menu</p>\n    <ul className=\"menu-list\">\n      <NavLink to=\"/heroes\" className={({ isActive }) => isActive ? 'active-link' : ''}>\n        Heroes\n      </NavLink>\n      <NavLink to=\"/villains\" className={({ isActive }) => isActive ? 'active-link' : ''}>\n        Villains\n      </NavLink>\n      <NavLink to=\"/about\" className={({ isActive }) => isActive ? 'active-link' : ''}>\n        About\n      </NavLink>\n    </ul>\n    {props.children}\n  </nav>\n);\n\nexport default NavBar;\n"
  },
  {
    "path": "src/components/NotFound.js",
    "content": "import React from 'react';\n\nconst NotFound = () => (\n  <div className=\"content-container\">\n    <div className=\"content-title-group not-found\">\n      <i className=\"fas fa-exclamation-triangle\" aria-hidden=\"true\" />\n      &nbsp;\n      <span className=\"title\">{`These aren't the bits you're looking for`}</span>\n    </div>\n  </div>\n);\n\nexport default NotFound;\n"
  },
  {
    "path": "src/components/index.js",
    "content": "import ButtonFooter from './ButtonFooter';\nimport CardContent from './CardContent';\nimport HeaderBar from './HeaderBar';\nimport InputDetail from './InputDetail';\nimport ListHeader from './ListHeader';\nimport ModalYesNo from './ModalYesNo';\nimport NavBar from './NavBar';\nimport NotFound from './NotFound';\n\nexport {\n  ButtonFooter,\n  CardContent,\n  HeaderBar,\n  InputDetail,\n  ListHeader,\n  NavBar,\n  NotFound,\n  ModalYesNo\n};\n"
  },
  {
    "path": "src/heroes/HeroDetail.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\n\nimport { ButtonFooter, InputDetail } from '../components';\n\nfunction HeroDetail({\n  hero: initHero,\n  handleCancelHero,\n  handleSaveHero,\n}) {\n  const navigate = useNavigate();\n  const [hero, setHero] = useState(Object.assign({}, initHero));\n\n  useEffect(() => {\n    if (!hero) {\n      navigate('/'); // no hero, bail out of Details\n    }\n  }, [hero, navigate]);\n\n  function handleSave() {\n    const chgHero = { ...hero, id: hero.id || null };\n    handleSaveHero(chgHero);\n  }\n\n  function handleNameChange(e) {\n    setHero({ ...hero, name: e.target.value });\n  }\n\n  function handleDescriptionChange(e) {\n    setHero({ ...hero, description: e.target.value });\n  }\n\n  return (\n    <div className=\"card edit-detail\">\n      <header className=\"card-header\">\n        <p className=\"card-header-title\">\n          {hero.name}\n          &nbsp;\n        </p>\n      </header>\n      <div className=\"card-content\">\n        <div className=\"content\">\n          {hero.id && <InputDetail name=\"id\" value={hero.id} readOnly=\"true\" />}\n          <InputDetail\n            name=\"name\"\n            value={hero.name}\n            placeholder=\"e.g Colleen\"\n            onChange={handleNameChange}\n          />\n          <InputDetail\n            name=\"description\"\n            value={hero.description}\n            placeholder=\"e.g dance fight!\"\n            onChange={handleDescriptionChange}\n          />\n        </div>\n      </div>\n      <footer className=\"card-footer \">\n        <ButtonFooter\n          className=\"cancel-button\"\n          iconClasses=\"fas fa-undo\"\n          onClick={handleCancelHero}\n          label=\"Cancel\"\n        />\n        <ButtonFooter\n          className=\"save-button\"\n          iconClasses=\"fas fa-save\"\n          onClick={handleSave}\n          label=\"Save\"\n        />\n      </footer>\n    </div>\n  );\n}\n\nexport default HeroDetail;\n"
  },
  {
    "path": "src/heroes/HeroList.js",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\n\nimport { ButtonFooter, CardContent } from '../components';\n\nfunction HeroList({ handleDeleteHero, handleSelectHero, heroes }) {\n  const navigate = useNavigate();\n\n  function selectHero(e) {\n    const hero = getSelectedHero(e);\n    handleSelectHero(hero);\n    navigate(`/heroes/${hero.id}`);\n  }\n\n  function deleteHero(e) {\n    const hero = getSelectedHero(e);\n    handleDeleteHero(hero);\n  }\n\n  function getSelectedHero(e) {\n    const index = +e.currentTarget.dataset.index;\n    return heroes[index];\n  }\n\n  return (\n    <ul className=\"list\">\n      {heroes.map((hero, index) => (\n        <li key={hero.id} role=\"presentation\">\n          <div className=\"card\">\n            <CardContent name={hero.name} description={hero.description} />\n            <footer className=\"card-footer\">\n              <ButtonFooter\n                className=\"delete-item\"\n                iconClasses=\"fas fa-trash\"\n                onClick={deleteHero}\n                label=\"Delete\"\n                dataIndex={index}\n                dataId={hero.id}\n              />\n              <ButtonFooter\n                className=\"edit-item\"\n                iconClasses=\"fas fa-edit\"\n                onClick={selectHero}\n                label=\"Edit\"\n                dataIndex={index}\n                dataId={hero.id}\n              />\n            </footer>\n          </div>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport default HeroList;\n"
  },
  {
    "path": "src/heroes/Heroes.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Route, Routes, useNavigate } from 'react-router-dom';\n\nimport { ListHeader, ModalYesNo } from '../components';\nimport HeroDetail from './HeroDetail';\nimport HeroList from './HeroList';\nimport useHeroes from './useHeroes';\n\nconst captains = console;\n\nfunction Heroes() {\n  const navigate = useNavigate();\n  const [heroToDelete, setHeroToDelete] = useState(null);\n  const [showModal, setShowModal] = useState(false);\n  const {\n    addHero,\n    deleteHero,\n    getHeroes,\n    heroes,\n    selectHero,\n    selectedHero,\n    updateHero\n  } = useHeroes();\n\n  useEffect(() => {\n    getHeroes();\n  }, [getHeroes]);\n\n  function addNewHero() {\n    selectHero({});\n    navigate('/heroes/0');\n  }\n\n  function handleCancelHero() {\n    navigate('/');\n    selectHero(null);\n    setHeroToDelete(null);\n  }\n\n  function handleDeleteHero(hero) {\n    selectHero(null);\n    setHeroToDelete(hero);\n    setShowModal(true);\n  }\n\n  function handleSaveHero(hero) {\n    if (selectedHero && selectedHero.name) {\n      captains.log(hero);\n      updateHero(hero);\n    } else {\n      addHero(hero);\n    }\n    handleCancelHero();\n  }\n\n  function handleCloseModal() {\n    setShowModal(false);\n  }\n\n  function handleDeleteFromModal() {\n    setShowModal(false);\n    deleteHero(heroToDelete);\n    handleCancelHero();\n  }\n\n  function handleSelectHero(selectedHero) {\n    selectHero(selectedHero);\n    captains.log(`you selected ${selectedHero.name}`);\n  }\n\n  function handleRefresh() {\n    handleCancelHero();\n    getHeroes();\n  }\n\n  return (\n    <div className=\"content-container\">\n      <ListHeader\n        title=\"Heroes\"\n        handleAdd={addNewHero}\n        handleRefresh={handleRefresh}\n        routePath=\"/heroes\"\n      />\n      <div className=\"columns is-multiline is-variable\">\n        <div className=\"column is-8\">\n          <Routes>\n            <Route\n              index\n              element={\n                <HeroList\n                  heroes={heroes}\n                  selectedHero={selectedHero}\n                  handleSelectHero={handleSelectHero}\n                  handleDeleteHero={handleDeleteHero}\n                />\n              }\n            />\n            <Route\n              path=\":id\"\n              element={\n                <HeroDetail\n                  hero={selectedHero}\n                  handleCancelHero={handleCancelHero}\n                  handleSaveHero={handleSaveHero}\n                />\n              }\n            />\n          </Routes>\n        </div>\n      </div>\n\n      {showModal && (\n        <ModalYesNo\n          message={`Would you like to delete ${heroToDelete.name}?`}\n          onNo={handleCloseModal}\n          onYes={handleDeleteFromModal}\n        />\n      )}\n    </div>\n  );\n}\n\nexport default Heroes;\n"
  },
  {
    "path": "src/heroes/useHeroes.js",
    "content": "import { useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport {\n  addHeroAction,\n  deleteHeroAction,\n  loadHeroesAction,\n  selectHeroAction,\n  updateHeroAction\n} from '../store';\n\n/** Custom hook for accessing Hero state in redux store */\nfunction useHeroes() {\n  const dispatch = useDispatch();\n\n  return {\n    // Selectors\n    heroes: useSelector(state => state.heroes.data),\n    selectedHero: useSelector(state => state.selectedHero),\n\n    // Dispatchers\n    // Wrap any dispatcher that could be called within a useEffect() in a useCallback()\n    addHero: hero => dispatch(addHeroAction(hero)),\n    deleteHero: hero => dispatch(deleteHeroAction(hero)),\n    getHeroes: useCallback(() => dispatch(loadHeroesAction()), [dispatch]), // called within a useEffect()\n    selectHero: hero => dispatch(selectHeroAction(hero)),\n    updateHero: hero => dispatch(updateHeroAction(hero))\n  };\n}\n\nexport default useHeroes;\n"
  },
  {
    "path": "src/index.css",
    "content": "body {\n  margin: 0;\n  padding: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { Provider } from 'react-redux';\nimport { BrowserRouter } from 'react-router-dom';\nimport { applyMiddleware, compose, createStore } from 'redux';\nimport createSagaMiddleware from 'redux-saga';\nimport App from './App';\nimport './index.css';\nimport app, { heroSaga, villainSaga } from './store';\n\n// create and configure redux middleware (saga is a middleware)\nconst sagaMiddleware = createSagaMiddleware();\n\nconst composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;\nconst store = createStore(\n  app,\n  composeEnhancers(applyMiddleware(sagaMiddleware))\n);\n\nsagaMiddleware.run(heroSaga);\nsagaMiddleware.run(villainSaga);\n\nconst root = createRoot(document.getElementById('root'));\nroot.render(\n  <Provider store={store}>\n    <BrowserRouter>\n      <App />\n    </BrowserRouter>\n  </Provider>\n);\n"
  },
  {
    "path": "src/serviceWorker.js",
    "content": "// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on subsequent visits to a page, after all the\n// existing tabs open on the page have been closed, since previously cached\n// resources are updated in the background.\n\n// To learn more about the benefits of this model and instructions on how to\n// opt-in, read http://bit.ly/CRA-PWA.\n\nconst isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.1/8 is considered localhost for IPv4.\n    window.location.hostname.match(\n      /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n    )\n);\n\nexport function register(config) {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    // The URL constructor is available in all browsers that support SW.\n    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);\n    if (publicUrl.origin !== window.location.origin) {\n      // Our service worker won't work if PUBLIC_URL is on a different origin\n      // from what our page is served on. This might happen if a CDN is used to\n      // serve assets; see https://github.com/facebook/create-react-app/issues/2374\n      return;\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n      if (isLocalhost) {\n        // This is running on localhost. Let's check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl, config);\n\n        // Add some additional logging to localhost, pointing developers to the\n        // service worker/PWA documentation.\n        navigator.serviceWorker.ready.then(() => {\n          console.log(\n            'This web app is being served cache-first by a service ' +\n              'worker. To learn more, visit http://bit.ly/CRA-PWA'\n          );\n        });\n      } else {\n        // Is not localhost. Just register service worker\n        registerValidSW(swUrl, config);\n      }\n    });\n  }\n}\n\nfunction registerValidSW(swUrl, config) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then(registration => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing;\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (navigator.serviceWorker.controller) {\n              // At this point, the updated precached content has been fetched,\n              // but the previous service worker will still serve the older\n              // content until all client tabs are closed.\n              console.log(\n                'New content is available and will be used when all ' +\n                  'tabs for this page are closed. See http://bit.ly/CRA-PWA.'\n              );\n\n              // Execute callback\n              if (config && config.onUpdate) {\n                config.onUpdate(registration);\n              }\n            } else {\n              // At this point, everything has been precached.\n              // It's the perfect time to display a\n              // \"Content is cached for offline use.\" message.\n              console.log('Content is cached for offline use.');\n\n              // Execute callback\n              if (config && config.onSuccess) {\n                config.onSuccess(registration);\n              }\n            }\n          }\n        };\n      };\n    })\n    .catch(error => {\n      console.error('Error during service worker registration:', error);\n    });\n}\n\nfunction checkValidServiceWorker(swUrl, config) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl)\n    .then(response => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      if (\n        response.status === 404 ||\n        response.headers.get('content-type').indexOf('javascript') === -1\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then(registration => {\n          registration.unregister().then(() => {\n            window.location.reload();\n          });\n        });\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl, config);\n      }\n    })\n    .catch(() => {\n      console.log(\n        'No internet connection found. App is running in offline mode.'\n      );\n    });\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then(registration => {\n      registration.unregister();\n    });\n  }\n}\n"
  },
  {
    "path": "src/store/action-utils.js",
    "content": "export const parseList = response => {\n  if (response.status !== 200) throw Error(response.message);\n  let list = response.data;\n  if (typeof list !== 'object') {\n    list = [];\n  }\n  return list;\n};\n\nexport const parseItem = (response, code) => {\n  if (response.status !== code) throw Error(response.message);\n  let item = response.data;\n  if (typeof item !== 'object') {\n    item = undefined;\n  }\n  return item;\n};\n"
  },
  {
    "path": "src/store/config.js",
    "content": "const API = process.env.REACT_APP_API;\n\nexport { API as default };\n"
  },
  {
    "path": "src/store/hero.actions.js",
    "content": "export const LOAD_HERO = '[Heroes] LOAD_HERO';\nexport const LOAD_HERO_SUCCESS = '[Heroes] LOAD_HERO_SUCCESS';\nexport const LOAD_HERO_ERROR = '[Heroes] LOAD_HERO_ERROR';\n\nexport const UPDATE_HERO = '[Heroes] UPDATE_HERO';\nexport const UPDATE_HERO_SUCCESS = '[Heroes] UPDATE_HERO_SUCCESS';\nexport const UPDATE_HERO_ERROR = '[Heroes] UPDATE_HERO_ERROR';\n\nexport const DELETE_HERO = '[Heroes] DELETE_HERO';\nexport const DELETE_HERO_SUCCESS = '[Heroes] DELETE_HERO_SUCCESS';\nexport const DELETE_HERO_ERROR = '[Heroes] DELETE_HERO_ERROR';\n\nexport const ADD_HERO = '[Heroes] ADD_HERO';\nexport const ADD_HERO_SUCCESS = '[Heroes] ADD_HERO_SUCCESS';\nexport const ADD_HERO_ERROR = '[Heroes] ADD_HERO_ERROR';\n\nexport const SELECT_HERO = '[Hero] SELECT_HERO';\n\nexport const selectHeroAction = hero => ({ type: SELECT_HERO, payload: hero });\nexport const loadHeroesAction = () => ({ type: LOAD_HERO });\n\nexport const updateHeroAction = hero => ({ type: UPDATE_HERO, payload: hero });\nexport const deleteHeroAction = hero => ({ type: DELETE_HERO, payload: hero });\nexport const addHeroAction = hero => ({ type: ADD_HERO, payload: hero });\n"
  },
  {
    "path": "src/store/hero.api.js",
    "content": "import axios from 'axios';\nimport { parseItem, parseList } from './action-utils';\nimport API from './config';\n\nconst captains = console;\n\nexport const deleteHeroApi = async hero => {\n  const response = await axios.delete(`${API}/heroes/${hero.id}`);\n  return parseItem(response, 200);\n};\n\nexport const updateHeroApi = async hero => {\n  captains.log(hero.id);\n  const response = await axios.put(`${API}/heroes/${hero.id}`, hero);\n  return parseItem(response, 200);\n};\n\nexport const addHeroApi = async hero => {\n  const response = await axios.post(`${API}/heroes`, hero);\n  return parseItem(response, 201);\n};\n\nexport const loadHeroesApi = async () => {\n  const response = await axios.get(`${API}/heroes`);\n  return parseList(response, 200);\n};\n"
  },
  {
    "path": "src/store/hero.reducer.js",
    "content": "import {\n  SELECT_HERO,\n  LOAD_HERO_SUCCESS,\n  LOAD_HERO,\n  LOAD_HERO_ERROR,\n  UPDATE_HERO,\n  UPDATE_HERO_SUCCESS,\n  UPDATE_HERO_ERROR,\n  DELETE_HERO,\n  DELETE_HERO_SUCCESS,\n  DELETE_HERO_ERROR,\n  ADD_HERO,\n  ADD_HERO_SUCCESS,\n  ADD_HERO_ERROR\n} from './hero.actions';\n\nlet initState = {\n  loading: false,\n  data: [],\n  error: void 0\n};\n\nexport const heroesReducer = (state = initState, action) => {\n  switch (action.type) {\n    case LOAD_HERO:\n      return { ...state, loading: true, error: '' };\n    case LOAD_HERO_SUCCESS:\n      return { ...state, loading: false, data: [...action.payload] };\n    case LOAD_HERO_ERROR:\n      return { ...state, loading: false, error: action.payload };\n\n    case UPDATE_HERO:\n      return {\n        ...state,\n        data: state.data.map(h => {\n          if (h.id === action.payload.id) {\n            state.loading = true;\n          }\n          return h;\n        })\n      };\n    case UPDATE_HERO_SUCCESS:\n      return modifyHeroState(state, action.payload);\n    case UPDATE_HERO_ERROR:\n      return { ...state, loading: false, error: action.payload };\n\n    case DELETE_HERO: {\n      return {\n        ...state,\n        loading: true,\n        data: state.data.filter(h => h !== action.payload)\n      };\n    }\n\n    case DELETE_HERO_SUCCESS: {\n      const result = { ...state, loading: false };\n      return result;\n    }\n\n    case DELETE_HERO_ERROR: {\n      return {\n        ...state,\n        data: [...state.data, action.payload.requestData],\n        loading: false\n      };\n    }\n\n    case ADD_HERO: {\n      return { ...state, loading: true };\n    }\n\n    case ADD_HERO_SUCCESS: {\n      return {\n        ...state,\n        loading: false,\n        data: [...state.data, { ...action.payload }]\n      };\n    }\n\n    case ADD_HERO_ERROR: {\n      return { ...state, loading: false };\n    }\n\n    default:\n      return state;\n  }\n};\n\nconst modifyHeroState = (heroState, heroChanges) => {\n  return {\n    ...heroState,\n    loading: false,\n    data: heroState.data.map(h => {\n      if (h.id === heroChanges.id) {\n        return { ...h, ...heroChanges };\n      } else {\n        return h;\n      }\n    })\n  };\n};\n\nlet initialSelectedHero = null;\n\nexport const selectedHeroReducer = (state = initialSelectedHero, action) => {\n  switch (action.type) {\n    case SELECT_HERO:\n      return action.payload ? { ...action.payload } : null;\n    default:\n      return state;\n  }\n};\n"
  },
  {
    "path": "src/store/hero.saga.js",
    "content": "import { put, takeEvery, call, all } from 'redux-saga/effects';\nimport {\n  LOAD_HERO,\n  LOAD_HERO_SUCCESS,\n  LOAD_HERO_ERROR,\n  UPDATE_HERO,\n  UPDATE_HERO_SUCCESS,\n  UPDATE_HERO_ERROR,\n  DELETE_HERO,\n  DELETE_HERO_SUCCESS,\n  DELETE_HERO_ERROR,\n  ADD_HERO,\n  ADD_HERO_SUCCESS,\n  ADD_HERO_ERROR\n} from './hero.actions';\nimport {\n  addHeroApi,\n  deleteHeroApi,\n  loadHeroesApi,\n  updateHeroApi\n} from './hero.api';\n\n// Our worker Saga: will perform the async increment task\nexport function* loadingHeroesAsync() {\n  try {\n    const data = yield call(loadHeroesApi);\n    const heroes = [...data];\n\n    yield put({ type: LOAD_HERO_SUCCESS, payload: heroes });\n  } catch (err) {\n    yield put({ type: LOAD_HERO_ERROR, payload: err.message });\n  }\n}\n\n// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC\nexport function* watchLoadingHeroesAsync() {\n  yield takeEvery(LOAD_HERO, loadingHeroesAsync);\n}\n\nexport function* updatingHeroAsync({ payload }) {\n  try {\n    const data = yield call(updateHeroApi, payload);\n    const updatedHero = data;\n\n    yield put({ type: UPDATE_HERO_SUCCESS, payload: updatedHero });\n  } catch (err) {\n    yield put({ type: UPDATE_HERO_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchUpdatingHeroAsync() {\n  yield takeEvery(UPDATE_HERO, updatingHeroAsync);\n}\n\nexport function* deletingHeroAsync({ payload }) {\n  try {\n    yield call(deleteHeroApi, payload);\n\n    yield put({ type: DELETE_HERO_SUCCESS, payload: null });\n  } catch (err) {\n    yield put({ type: DELETE_HERO_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchDeletingHeroAsync() {\n  yield takeEvery(DELETE_HERO, deletingHeroAsync);\n}\n\nexport function* addingHeroAsync({ payload }) {\n  try {\n    const data = yield call(addHeroApi, payload);\n    const addedHero = data;\n\n    yield put({ type: ADD_HERO_SUCCESS, payload: addedHero });\n  } catch (err) {\n    yield put({ type: ADD_HERO_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchAddingHeroAsync() {\n  yield takeEvery(ADD_HERO, addingHeroAsync);\n}\n\nexport function* heroSaga() {\n  yield all([\n    watchLoadingHeroesAsync(),\n    watchUpdatingHeroAsync(),\n    watchDeletingHeroAsync(),\n    watchAddingHeroAsync()\n  ]);\n}\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import { combineReducers } from 'redux';\nimport { heroesReducer, selectedHeroReducer } from './hero.reducer';\nimport { selectedVillainReducer, villainsReducer } from './villain.reducer';\n\nexport * from './hero.actions';\nexport * from './hero.reducer';\nexport * from './hero.saga';\nexport * from './villain.actions';\nexport * from './villain.reducer';\nexport * from './villain.saga';\n\nconst store = combineReducers({\n  villains: villainsReducer,\n  heroes: heroesReducer,\n  selectedHero: selectedHeroReducer,\n  selectedVillain: selectedVillainReducer\n});\n\nexport default store;\n"
  },
  {
    "path": "src/store/villain.actions.js",
    "content": "export const LOAD_VILLAIN = '[Villains] LOAD_VILLAIN';\nexport const LOAD_VILLAIN_SUCCESS = '[Villains] LOAD_VILLAIN_SUCCESS';\nexport const LOAD_VILLAIN_ERROR = '[Villains] LOAD_VILLAIN_ERROR';\n\nexport const UPDATE_VILLAIN = '[Villains] UPDATE_VILLAIN';\nexport const UPDATE_VILLAIN_SUCCESS = '[Villains] UPDATE_VILLAIN_SUCCESS';\nexport const UPDATE_VILLAIN_ERROR = '[Villains] UPDATE_VILLAIN_ERROR';\n\nexport const DELETE_VILLAIN = '[Villains] DELETE_VILLAIN';\nexport const DELETE_VILLAIN_SUCCESS = '[Villains] DELETE_VILLAIN_SUCCESS';\nexport const DELETE_VILLAIN_ERROR = '[Villains] DELETE_VILLAIN_ERROR';\n\nexport const ADD_VILLAIN = '[Villains] ADD_VILLAIN';\nexport const ADD_VILLAIN_SUCCESS = '[Villains] ADD_VILLAIN_SUCCESS';\nexport const ADD_VILLAIN_ERROR = '[Villains] ADD_VILLAIN_ERROR';\n\nexport const SELECT_VILLAIN = '[Villain] SELECT_VILLAIN';\n\nexport const selectVillainAction = villain => ({\n  type: SELECT_VILLAIN,\n  payload: villain\n});\nexport const loadVillainsAction = () => ({ type: LOAD_VILLAIN });\n\nexport const updateVillainAction = villain => ({\n  type: UPDATE_VILLAIN,\n  payload: villain\n});\nexport const deleteVillainAction = villain => ({\n  type: DELETE_VILLAIN,\n  payload: villain\n});\nexport const addVillainAction = villain => ({\n  type: ADD_VILLAIN,\n  payload: villain\n});\n"
  },
  {
    "path": "src/store/villain.api.js",
    "content": "import axios from 'axios';\nimport { parseItem, parseList } from './action-utils';\nimport API from './config';\n\nconst captains = console;\n\nexport const deleteVillainApi = async villain => {\n  const response = await axios.delete(`${API}/villains/${villain.id}`);\n  return parseItem(response, 200);\n};\n\nexport const updateVillainApi = async villain => {\n  captains.log(villain.id);\n  const response = await axios.put(`${API}/villains/${villain.id}`, villain);\n  return parseItem(response, 200);\n};\n\nexport const addVillainApi = async villain => {\n  const response = await axios.post(`${API}/villains`, villain);\n  return parseItem(response, 201);\n};\n\nexport const loadVillainsApi = async () => {\n  const response = await axios.get(`${API}/villains`);\n  return parseList(response, 200);\n};\n"
  },
  {
    "path": "src/store/villain.reducer.js",
    "content": "import {\n  SELECT_VILLAIN,\n  LOAD_VILLAIN_SUCCESS,\n  LOAD_VILLAIN,\n  LOAD_VILLAIN_ERROR,\n  UPDATE_VILLAIN,\n  UPDATE_VILLAIN_SUCCESS,\n  UPDATE_VILLAIN_ERROR,\n  DELETE_VILLAIN,\n  DELETE_VILLAIN_SUCCESS,\n  DELETE_VILLAIN_ERROR,\n  ADD_VILLAIN,\n  ADD_VILLAIN_SUCCESS,\n  ADD_VILLAIN_ERROR\n} from './villain.actions';\n\nlet initState = {\n  loading: false,\n  data: [],\n  error: void 0\n};\n\nexport const villainsReducer = (state = initState, action) => {\n  switch (action.type) {\n    case LOAD_VILLAIN:\n      return { ...state, loading: true, error: '' };\n    case LOAD_VILLAIN_SUCCESS:\n      return { ...state, loading: false, data: [...action.payload] };\n    case LOAD_VILLAIN_ERROR:\n      return { ...state, loading: false, error: action.payload };\n\n    case UPDATE_VILLAIN:\n      return {\n        ...state,\n        data: state.data.map(h => {\n          if (h.id === action.payload.id) {\n            state.loading = true;\n          }\n          return h;\n        })\n      };\n    case UPDATE_VILLAIN_SUCCESS:\n      return modifyVillainState(state, action.payload);\n    case UPDATE_VILLAIN_ERROR:\n      return { ...state, loading: false, error: action.payload };\n\n    case DELETE_VILLAIN: {\n      return {\n        ...state,\n        loading: true,\n        data: state.data.filter(h => h !== action.payload)\n      };\n    }\n\n    case DELETE_VILLAIN_SUCCESS: {\n      const result = { ...state, loading: false };\n      return result;\n    }\n\n    case DELETE_VILLAIN_ERROR: {\n      return {\n        ...state,\n        data: [...state.data, action.payload.requestData],\n        loading: false\n      };\n    }\n\n    case ADD_VILLAIN: {\n      return { ...state, loading: true };\n    }\n\n    case ADD_VILLAIN_SUCCESS: {\n      return {\n        ...state,\n        loading: false,\n        data: [...state.data, { ...action.payload }]\n      };\n    }\n\n    case ADD_VILLAIN_ERROR: {\n      return { ...state, loading: false };\n    }\n\n    default:\n      return state;\n  }\n};\n\nconst modifyVillainState = (villainState, villainChanges) => {\n  return {\n    ...villainState,\n    loading: false,\n    data: villainState.data.map(h => {\n      if (h.id === villainChanges.id) {\n        return { ...h, ...villainChanges };\n      } else {\n        return h;\n      }\n    })\n  };\n};\n\nlet initialSelectedVillain = null;\n\nexport const selectedVillainReducer = (\n  state = initialSelectedVillain,\n  action\n) => {\n  switch (action.type) {\n    case SELECT_VILLAIN:\n      return action.payload ? { ...action.payload } : null;\n    default:\n      return state;\n  }\n};\n"
  },
  {
    "path": "src/store/villain.saga.js",
    "content": "import { put, takeEvery, call, all } from 'redux-saga/effects';\nimport {\n  LOAD_VILLAIN,\n  LOAD_VILLAIN_SUCCESS,\n  LOAD_VILLAIN_ERROR,\n  UPDATE_VILLAIN,\n  UPDATE_VILLAIN_SUCCESS,\n  UPDATE_VILLAIN_ERROR,\n  DELETE_VILLAIN,\n  DELETE_VILLAIN_SUCCESS,\n  DELETE_VILLAIN_ERROR,\n  ADD_VILLAIN,\n  ADD_VILLAIN_SUCCESS,\n  ADD_VILLAIN_ERROR\n} from './villain.actions';\nimport {\n  addVillainApi,\n  deleteVillainApi,\n  loadVillainsApi,\n  updateVillainApi\n} from './villain.api';\n\nexport function* loadingVillainsAsync() {\n  try {\n    const data = yield call(loadVillainsApi);\n    const villaines = [...data];\n\n    yield put({ type: LOAD_VILLAIN_SUCCESS, payload: villaines });\n  } catch (err) {\n    yield put({ type: LOAD_VILLAIN_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchLoadingVillainsAsync() {\n  yield takeEvery(LOAD_VILLAIN, loadingVillainsAsync);\n}\n\nexport function* updatingVillainAsync({ payload }) {\n  try {\n    const data = yield call(updateVillainApi, payload);\n    const updatedVillain = data;\n\n    yield put({ type: UPDATE_VILLAIN_SUCCESS, payload: updatedVillain });\n  } catch (err) {\n    yield put({ type: UPDATE_VILLAIN_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchUpdatingVillainAsync() {\n  yield takeEvery(UPDATE_VILLAIN, updatingVillainAsync);\n}\n\nexport function* deletingVillainAsync({ payload }) {\n  try {\n    yield call(deleteVillainApi, payload);\n\n    yield put({ type: DELETE_VILLAIN_SUCCESS, payload: null });\n  } catch (err) {\n    yield put({ type: DELETE_VILLAIN_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchDeletingVillainAsync() {\n  yield takeEvery(DELETE_VILLAIN, deletingVillainAsync);\n}\n\nexport function* addingVillainAsync({ payload }) {\n  try {\n    const data = yield call(addVillainApi, payload);\n    const addedVillain = data;\n\n    yield put({ type: ADD_VILLAIN_SUCCESS, payload: addedVillain });\n  } catch (err) {\n    yield put({ type: ADD_VILLAIN_ERROR, payload: err.message });\n  }\n}\n\nexport function* watchAddingVillainAsync() {\n  yield takeEvery(ADD_VILLAIN, addingVillainAsync);\n}\n\nexport function* villainSaga() {\n  yield all([\n    watchLoadingVillainsAsync(),\n    watchUpdatingVillainAsync(),\n    watchDeletingVillainAsync(),\n    watchAddingVillainAsync()\n  ]);\n}\n"
  },
  {
    "path": "src/styles.scss",
    "content": "$vue: #42b883;\n$vue-light: #42b883;\n$angular: #b52e31;\n$angular-light: #eb7a7c;\n$react: #00b3e6;\n$react-light: #61dafb;\n$primary: $react;\n$primary-light: $react-light;\n$link: $primary; // #00b3e6; // #ff4081;\n\n$shade-light: #fafafa;\n\n@import 'bulma/bulma.sass';\n\n.menu-list .active-link,\n.menu-list .router-link-active {\n  color: #fff;\n  background-color: $link;\n}\n\n.not-found {\n  i {\n    font-size: 20px;\n    margin-right: 8px;\n  }\n  .title {\n    letter-spacing: 0px;\n    font-weight: normal;\n    font-size: 24px;\n    text-transform: none;\n  }\n}\n\nheader {\n  font-weight: bold;\n  font-family: Arial;\n  span {\n    letter-spacing: 0px;\n    &.tour {\n      color: #fff;\n    }\n    &.of {\n      color: #ccc;\n    }\n    &.heroes {\n      color: $primary-light;\n    }\n  }\n  .navbar-item.nav-home {\n    border: 3px solid transparent;\n    border-radius: 0%;\n    &:hover {\n      border-right: 3px solid $primary-light;\n      border-left: 3px solid $primary-light;\n    }\n  }\n  .fab {\n    font-size: 24px;\n    &.js-logo {\n      color: $primary-light;\n    }\n  }\n  .buttons {\n    i.fab {\n      color: #fff;\n      margin-left: 20px;\n      margin-right: 10px;\n      &:hover {\n        color: $primary-light;\n      }\n    }\n  }\n}\n\n.edit-detail {\n  .input[readonly] {\n    background-color: $shade-light;\n  }\n}\n\n.content-title-group {\n  margin-bottom: 16px;\n  h2 {\n    border-left: 16px solid $primary;\n    border-bottom: 2px solid $primary;\n    padding-left: 8px;\n    padding-right: 16px;\n    display: inline-block;\n    text-transform: uppercase;\n    color: #555;\n    letter-spacing: 0px;\n    &:hover {\n      color: $link;\n    }\n  }\n  button.button {\n    border: 0;\n    color: #999;\n    &:hover {\n      color: $link;\n    }\n  }\n}\nul.list {\n  box-shadow: none;\n}\ndiv.card-content {\n  background-color: $shade-light;\n  .name {\n    font-size: 28px;\n    color: #000;\n  }\n  .description {\n    font-size: 20px;\n    color: #999;\n  }\n  background-color: $shade-light;\n}\n.card {\n  margin-bottom: 2em;\n}\n\nlabel.label {\n  font-weight: normal;\n}\n\np.card-header-title {\n  background-color: $primary;\n  text-transform: uppercase;\n  letter-spacing: 4px;\n  color: #fff;\n  display: block;\n  padding-left: 24px;\n}\n.card-footer button {\n  font-size: 16px;\n  i {\n    margin-right: 10px;\n  }\n  color: #888;\n  &:hover {\n    color: $link;\n  }\n}\n\n.modal-card-foot button {\n  display: inline-block;\n  width: 80px;\n}\n\n.modal-card-head,\n.modal-card-body {\n  text-align: center;\n}\n\n.field {\n  margin-bottom: 0.75rem;\n}\n\n.navbar-burger {\n  margin-left: auto;\n}\n\nbutton.link {\n  background: none;\n  border: none;\n  cursor: pointer;\n}\n"
  },
  {
    "path": "src/villains/VillainDetail.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\n\nimport { ButtonFooter, InputDetail } from '../components';\n\nfunction VillainDetail({\n  villain: initVillain,\n  handleCancelVillain,\n  handleSaveVillain,\n}) {\n  const navigate = useNavigate();\n  const [villain, setVillain] = useState(Object.assign({}, initVillain));\n\n  useEffect(() => {\n    if (!villain) {\n      navigate('/villains'); // no villain, bail out of Details\n    }\n  }, [villain, navigate]);\n\n  function handleSave() {\n    const chgVillain = { ...villain, id: villain.id || null };\n    handleSaveVillain(chgVillain);\n  }\n\n  function handleNameChange(e) {\n    setVillain({ ...villain, name: e.target.value });\n  }\n\n  function handleDescriptionChange(e) {\n    setVillain({ ...villain, description: e.target.value });\n  }\n\n  return (\n    <div className=\"card edit-detail\">\n      <header className=\"card-header\">\n        <p className=\"card-header-title\">\n          {villain.name}\n          &nbsp;\n        </p>\n      </header>\n      <div className=\"card-content\">\n        <div className=\"content\">\n          {villain.id && (\n            <InputDetail name=\"id\" value={villain.id} readOnly=\"true\" />\n          )}\n          <InputDetail\n            name=\"name\"\n            value={villain.name}\n            placeholder=\"e.g Colleen\"\n            onChange={handleNameChange}\n          />\n          <InputDetail\n            name=\"description\"\n            value={villain.description}\n            placeholder=\"e.g dance fight!\"\n            onChange={handleDescriptionChange}\n          />\n        </div>\n      </div>\n      <footer className=\"card-footer \">\n        <ButtonFooter\n          className=\"cancel-button\"\n          iconClasses=\"fas fa-undo\"\n          onClick={handleCancelVillain}\n          label=\"Cancel\"\n        />\n        <ButtonFooter\n          className=\"save-button\"\n          iconClasses=\"fas fa-save\"\n          onClick={handleSave}\n          label=\"Save\"\n        />\n      </footer>\n    </div>\n  );\n}\n\nexport default VillainDetail;\n"
  },
  {
    "path": "src/villains/VillainList.js",
    "content": "import React from 'react';\nimport { useNavigate } from 'react-router-dom';\n\nimport { ButtonFooter, CardContent } from '../components';\n\nfunction VillainList({\n  handleDeleteVillain,\n  handleSelectVillain,\n  villains,\n}) {\n  const navigate = useNavigate();\n\n  function selectVillain(e) {\n    const villain = getSelectedVillain(e);\n    handleSelectVillain(villain);\n    navigate(`/villains/${villain.id}`);\n  }\n\n  function deleteVillain(e) {\n    const villain = getSelectedVillain(e);\n    handleDeleteVillain(villain);\n  }\n\n  function getSelectedVillain(e) {\n    const index = +e.currentTarget.dataset.index;\n    return villains[index];\n  }\n\n  return (\n    <ul className=\"list\">\n      {villains.map((villain, index) => (\n        <li key={villain.id} role=\"presentation\">\n          <div className=\"card\">\n            <CardContent\n              name={villain.name}\n              description={villain.description}\n            />\n            <footer className=\"card-footer\">\n              <ButtonFooter\n                className=\"delete-item\"\n                iconClasses=\"fas fa-trash\"\n                onClick={deleteVillain}\n                label=\"Delete\"\n                dataIndex={index}\n                dataId={villain.id}\n              />\n              <ButtonFooter\n                className=\"edit-item\"\n                iconClasses=\"fas fa-edit\"\n                onClick={selectVillain}\n                label=\"Edit\"\n                dataIndex={index}\n                dataId={villain.id}\n              />\n            </footer>\n          </div>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport default VillainList;\n"
  },
  {
    "path": "src/villains/Villains.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Route, Routes, useNavigate } from 'react-router-dom';\n\nimport { ListHeader, ModalYesNo } from '../components';\nimport VillainDetail from './VillainDetail';\nimport VillainList from './VillainList';\nimport useVillains from './useVillains';\n\nconst captains = console;\n\nfunction Villains() {\n  const navigate = useNavigate();\n  const [villainToDelete, setVillainToDelete] = useState(null);\n  const [showModal, setShowModal] = useState(false);\n  const {\n    addVillain,\n    deleteVillain,\n    getVillains,\n    villains,\n    selectVillain,\n    selectedVillain,\n    updateVillain\n  } = useVillains();\n\n  useEffect(() => {\n    getVillains();\n  }, [getVillains]);\n\n  function addNewVillain() {\n    selectVillain({});\n    navigate('/villains/0');\n  }\n\n  function handleCancelVillain() {\n    navigate('/villains');\n    selectVillain(null);\n    setVillainToDelete(null);\n  }\n\n  function handleDeleteVillain(villain) {\n    selectVillain(null);\n    setVillainToDelete(villain);\n    setShowModal(true);\n  }\n\n  function handleSaveVillain(villain) {\n    if (selectedVillain && selectedVillain.name) {\n      captains.log(villain);\n      updateVillain(villain);\n    } else {\n      addVillain(villain);\n    }\n    handleCancelVillain();\n  }\n\n  function handleCloseModal() {\n    setShowModal(false);\n  }\n\n  function handleDeleteFromModal() {\n    setShowModal(false);\n    deleteVillain(villainToDelete);\n    handleCancelVillain();\n  }\n\n  function handleSelectVillain(selectedVillain) {\n    selectVillain(selectedVillain);\n    captains.log(`you selected ${selectedVillain.name}`);\n  }\n\n  function handleRefresh() {\n    handleCancelVillain();\n    getVillains();\n  }\n\n  return (\n    <div className=\"content-container\">\n      <ListHeader\n        title=\"Villains\"\n        handleAdd={addNewVillain}\n        handleRefresh={handleRefresh}\n        routePath=\"/villains\"\n      />\n      <div className=\"columns is-multiline is-variable\">\n        <div className=\"column is-8\">\n          <Routes>\n            <Route\n              index\n              element={\n                <VillainList\n                  villains={villains}\n                  selectedVillain={selectedVillain}\n                  handleSelectVillain={handleSelectVillain}\n                  handleDeleteVillain={handleDeleteVillain}\n                />\n              }\n            />\n            <Route\n              path=\":id\"\n              element={\n                <VillainDetail\n                  villain={selectedVillain}\n                  handleCancelVillain={handleCancelVillain}\n                  handleSaveVillain={handleSaveVillain}\n                />\n              }\n            />\n          </Routes>\n        </div>\n      </div>\n\n      {showModal && (\n        <ModalYesNo\n          message={`Would you like to delete ${villainToDelete.name}?`}\n          onNo={handleCloseModal}\n          onYes={handleDeleteFromModal}\n        />\n      )}\n    </div>\n  );\n}\n\nexport default Villains;\n"
  },
  {
    "path": "src/villains/useVillains.js",
    "content": "import { useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport {\n  addVillainAction,\n  deleteVillainAction,\n  loadVillainsAction,\n  selectVillainAction,\n  updateVillainAction\n} from '../store';\n\n/** Custom hook for accessing Villain state in redux store */\nfunction useVillains() {\n  const dispatch = useDispatch();\n\n  return {\n    // Selectors\n    villains: useSelector(state => state.villains.data),\n    selectedVillain: useSelector(state => state.selectedVillain),\n\n    // Dispatchers\n    // Wrap any dispatcher that could be called within a useEffect() in a useCallback()\n    addVillain: villain => dispatch(addVillainAction(villain)),\n    deleteVillain: villain => dispatch(deleteVillainAction(villain)),\n    getVillains: useCallback(() => dispatch(loadVillainsAction()), [dispatch]), // called within a useEffect()\n    selectVillain: villain => dispatch(selectVillainAction(villain)),\n    updateVillain: villain => dispatch(updateVillainAction(villain))\n  };\n}\n\nexport default useVillains;\n"
  }
]