Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
1 result

Target

Select target project
  • cpilkington3 / React Redux Starter Code
  • jherman5 / React Redux Starter Code
  • SOFT Core / SOFT 260 / React Redux Starter Code
  • jadengoter / React Redux Starter Code
  • Andrew Herold / React Redux Starter Code
  • jackmnolley / Unit Conversion App
  • Muhammad Usama / React Redux Starter Code
  • sfarahmand2 / Homework 3
  • Ethan Yehl / ethan-hw3
  • gseagren2 / Algorithm Explorer
  • ihopp2 / SOFT260_HW4_Fall2024
11 results
Select Git revision
  • main
1 result
Show changes

Commits on Source 27

33 files
+ 1153
0
Compare changes
  • Side-by-side
  • Inline

Files

.gitattributes

0 → 100644
+2 −0
Original line number Original line Diff line number Diff line
# Disable line-ending conversions for this repository.
* -text

.gitignore

0 → 100644
+23 −0
Original line number Original line Diff line number Diff line
# dependencies
/node_modules

# testing
/coverage

# production
/build

# environments
.env.local
.env.development.local
.env.test.local
.env.production.local

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# misc
*~
.DS_Store

.gitmodules

0 → 100644
+6 −0
Original line number Original line Diff line number Diff line
[submodule "stylelint-config"]
	path = stylelint-config
	url = git@git.unl.edu:soft-core/soft-260/stylelint-config.git
[submodule "eslint-config"]
	path = eslint-config
	url = git@git.unl.edu:soft-core/soft-260/eslint-config.git

README.md

0 → 100644
+190 −0
Original line number Original line Diff line number Diff line
# React Redux Starter Code

A minimal app to be used as starter code for labs and homework in the SOFT 260
course at UNL.  The starter code demonstrates basic usage of the React Hooks
API, the Redux Toolkit (RTK), React Router, Jest, and the React Testing Library
(RTL) in the context of a React Redux progressive web app (PWA).

# Quick Start

Recursively clone this repository and `cd` into the root folder:

```
$ git clone --recursive git@git.unl.edu:soft-core/soft-260/react-redux-starter-code.git
$ cd react-redux-starter-code
```

(If you forget `--recursive` when cloning, you can `cd` into your clone and run
`git submodule update --init --recursive` instead.)

Install dependencies:

```
$ npm install
```

(Near the end you may see some warnings because `create-react-app` transitively
depends on some deprecated packages.)

Optionally run the linter and the test suite:

```
$ npm run lint
$ npm run test
```

And then serve the application locally:

```
$ npm start
```

When you are done, press control-c to stop the server.

# Folders and Files

The folders and files in the starter code are briefly described below.  React
Redux applications use a model-view-controller (MVC) architecture (see
<https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller>), so in
these descriptions the terms "model", "view", and "controller" refer to those
roles from MVC.

## Submodules

*   The Git submodule `stylelint-config` contains the stylelint configuration
    for the coding style used in the `minimal-app` project.

*   The Git submodule `eslint-config` contains the ESLint configuration for the
    coding style used in the `minimal-app` project.  Per `create-react-app`
    convention, in a development build of the main app, a separate, weaker
    coding style also warns at runtime about likely bugs.

## General Configuration

*   The file `minimal-app/.gitignore` prevents non-source-code files from being
    accidentally committed to the repository.

*   The file `minimal-app/package.json` describes the project and its
    dependencies.  It can be edited to customize the inputs and processes used
    in various software lifecyle tasks like linting, testing, or deploying.

*   The file `minimal-app/package-lock.json` records the exact set of
    dependencies used to satisfy the requirements in `minimal-app/package.json`.
    It should not be hand-edited; use commands like `npm install` or `npm
    uninstall` from the `minimal-app` directory to make changes.

*   The folder `minimal-app/node_modules` contains the dependencies installed by
    `npm`.

## Application Infrastructure

*   The file `minimal-app/public/manifest.json` provides data needed to run the
    web application as a PWA.  (See
    <https://developer.mozilla.org/en-US/docs/Web/Manifest> for more
    information.)

*   The file `minimal-app/public/logo.svg` is the image used for the app's icon.
    (A maskable icon is recommended; see <https://web.dev/maskable-icon/> for
    more information and <https://maskable.app/> to test an image.)

*   The file `minimal-app/public/index.html` contains the skeleton document in
    which the app's HTML is embedded.

*   The file `minimal-app/src/index.js` specifies other wrappers around the
    application proper than cannot be given in `index.html`, usually because
    they involve React components, not pure HTML.  In this case, the wrappers:

    *   Enable extra checks at development time using React strict mode.  (See
        <https://reactjs.org/docs/strict-mode.html> for more information.)

    *   Make the Redux store from `minimal-app/src/app/store.js` available to
        the app's React components.

    *   Enable routing with React Router.  (See
        <https://reacttraining.com/react-router/web> for more information.)

    *   Apply the letterboxed portrait layout from `index.css`, which is
        described next.

*   The file `minimal-app/src/index.css` contains the CSS associated with
    `index.html`.  In this case the CSS forces a letterboxed portrait layout and
    specifies a sans-serif font.

*   The file `minimal-app/src/service-worker.js` acts as a proxy server, which
    among other things makes it possible to run a PWA while offline.  (See
    <https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API> for
    more information.)

*   The file `minimal-app/src/serviceWorkerRegistration.js` registers the
    service worker so that the browser knows about it.

## Model and Controller Code

*   The file `minimal-app/src/app/store.js` combines the models provided by the
    app's features to create a model for the whole app.

*   The file `minimal-app/src/features/counter/counterSlice.js` implements a
    Redux slice, the model and associated controllers for a particular feature,
    which in this case is a simple counter.  (See
    <https://redux-toolkit.js.org/> for more information.)

## View Code

*   The file `minimal-app/src/app.js` implements the React component
    representing the entire app.  In this case only one route, `/`, is
    implemented, but the returned fragment is intentionally written so that it
    is easy to add additional routes.

*   The file `minimal-app/src/features/counter/counter.js` implements a React
    component that counts the number of times a user has tapped a button.  This
    component uses the slice from `counterSlice.js`.

*   The file `minimal-app/src/features/counter/counter.module.css` provides
    styles for the React component in `counter.js`.

## Test Infrastructure

*   The file `minimal-app/src/setupTests.js` provides setup code that applies to
    every test case.  In this case that code imports RTL's custom matchers from
    testing React components.

*   The file `minimal-app/src/testing/mockRedux.js` provides the ability to mock
    the part of Redux used by React components under the React Hooks API so that
    those view components can be tested independently of model and controller
    code.

## Test Code

*   The file `minimal-app/app.test.js` demonstrates a snapshot regression test
    of the app component with mocked selectors.  (See
    <https://jestjs.io/docs/en/snapshot-testing> for more information.)

*   The file `minimal-app/src/__snapshots__/app.test.js.snap` contains the
    oracles for the snapshot tests in `app.test.js`.  When the tests are run in
    watch mode (using the command `npm test`), these oracles can be updated
    interactively if a snapshot test fails due to changed requirements.

*   The file `minimal-app/src/features/counter/counter.test.js` demonstrates
    non-snapshot tests of the view code in the counter component.

*   The file `minimal-app/src/features/counter/counterSlice.test.js`
    demonstrates tests of model and controller code.

# Adaptation Checklist

When adapting this code for a new project, make sure to do at least the
following:

*  Change the project name, version number, and description in
   `minimal-app/package.json`.

*  Change the short name, name, description, colors, and other settings in
   `minimal-app/public/manifest.json`.

*  Change the title, description, and theme color in
   `minimal-app/public/index.html`.

*  Rename the folder from `minimal-app` to something descriptive and change the
   corresponding entries in the outer `package.json`.

*  Rerun `npm install` to update `package-lock.json` based on the above changes.
Original line number Original line Diff line number Diff line
Subproject commit 24df42fb655d234b83c93b0fb24d012e4d9ecb58

package-lock.json

0 → 100644
+0 −0

File added.

Preview size limit exceeded, changes collapsed.

package.json

0 → 100644
+30 −0
Original line number Original line Diff line number Diff line
{
  "name": "unit-conversion",
  "version": "1.1.0",
  "description": "An app that takes units and converts them to other units",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "postinstall:stylelint-config": "cd stylelint-config && npm install",
    "postinstall:eslint-config": "cd eslint-config && npm install",
    "postinstall:app": "cd unit-conversion && npm install",
    "postinstall": "run-s postinstall:**",
    "lint:app": "cd unit-conversion && npm run lint",
    "lint": "run-s --continue-on-error lint:**",
    "test-once:app": "cd unit-conversion && npm run test-once",
    "test-once": "run-s --continue-on-error test-once:**",
    "test": "run-s test-once",
    "start": "cd unit-conversion && npm run start",
    "build:app": "cd unit-conversion && npm run build",
    "build": "run-s build:**"
  },
  "devDependencies": {
    "ghooks": "^2.0.4",
    "npm-run-all": "^4.1.5"
  },
  "config": {
    "ghooks": {
      "pre-commit": "npm run lint"
    }
  }
}
+15 −0
Original line number Original line Diff line number Diff line
{
	"folders": [
		{
			"path": "."
		}
	],
	"settings": {
		"files.eol": "LF",
		"files.exclude": {
			"**/node_modules": true
		},
		"files.trimFinalNewlines": true,
		"files.trimTrailingWhitespace": true
	}
}
Original line number Original line Diff line number Diff line
Subproject commit 40b0b09694d1fa983446ff2768211c23f71f0f78
+23 −0
Original line number Original line Diff line number Diff line
# dependencies
/node_modules

# testing
/coverage

# production
/build

# environments
.env.local
.env.development.local
.env.test.local
.env.production.local

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# misc
*~
.DS_Store
+0 −0

File added.

Preview size limit exceeded, changes collapsed.

+78 −0
Original line number Original line Diff line number Diff line
{
  "name": "unit-conversion",
  "version": "1.1.0",
  "description": "An app that takes units and converts them to other units",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "lint:css": "stylelint \"**/*.css\" \"**/*.module.css\" \"!coverage/**\"",
    "lint:js": "eslint --max-warnings 0 ./src",
    "lint": "run-s --continue-on-error lint:**",
    "test-once": "react-scripts test --watchAll=false --coverage",
    "test": "react-scripts test --watchAll --coverage",
    "start": "react-scripts start",
    "build": "react-scripts build",
    "eject": "react-scripts eject"
  },
  "homepage": ".",
  "dependencies": {
    "@reduxjs/toolkit": "^1.6.0",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^12.0.0",
    "@testing-library/user-event": "^13.1.9",
    "classnames": "^2.3.1",
    "npm-run-all": "^4.1.5",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.4",
    "react-router-dom": "^5.2.0",
    "react-scripts": "^4.0.3",
    "workbox-background-sync": "^5.1.3",
    "workbox-broadcast-update": "^5.1.3",
    "workbox-cacheable-response": "^5.1.3",
    "workbox-core": "^5.1.3",
    "workbox-expiration": "^5.1.3",
    "workbox-google-analytics": "^5.1.3",
    "workbox-navigation-preload": "^5.1.3",
    "workbox-precaching": "^5.1.3",
    "workbox-range-requests": "^5.1.3",
    "workbox-routing": "^5.1.3",
    "workbox-strategies": "^5.1.3",
    "workbox-streams": "^5.1.3"
  },
  "devDependencies": {
    "@unlsoft/eslint-config": "file:../eslint-config",
    "@unlsoft/stylelint-config": "file:../stylelint-config",
    "eslint-plugin-jest-dom": "^3.9.0",
    "stylelint": "^13.13.1"
  },
  "stylelint": {
    "extends": "@unlsoft/stylelint-config"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "@unlsoft/eslint-config/react"
    ]
  },
  "jest": {
    "clearMocks": true,
    "collectCoverageFrom": [
      "src/features/**/*.js"
    ],
    "resetMocks": false,
    "restoreMocks": false
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
+20 −0
Original line number Original line Diff line number Diff line
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <meta
      name="description"
      content="An app that takes units and converts them to other units"
    />
    <meta name="theme-color" content="rgba(208 0 0 / 100%)" />
    <link rel="icon" href="u.svg" />
    <link rel="apple-touch-icon" href="u.svg" />
    <title>Unit Conversion</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
+18 −0
Original line number Original line Diff line number Diff line
{
  "short_name": "Conversion App",
  "name": "Unit Conversion",
  "description": "An app that takes units and converts them to other units",
  "icons": [
    {
      "src": "u.svg",
      "type": "image/svg+xml",
      "sizes": "192x192 512x512",
      "purpose": "any maskable"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "rgba(208 0 0 / 100%)",
  "background_color": "rgba(211 211 211 / 100%)"
}
+2 −0
Original line number Original line Diff line number Diff line
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g id="U"><path d="m337.255 281.038c.015 67.588-1.025 87.876-24.229 105.894-22.163 17.227-82.017 19.79-110.537-1.201-27.246-20.039-28.315-48.384-27.847-96.782l.103-288.949h-128.745v268.557c0 93.472 0 181.758 103.638 226.406 47.595 20.546 154.453 24.243 211.816 1.465 104.546-41.528 104.546-129.477 104.546-231.738v-264.69h-128.745z"/></g></svg>
 No newline at end of file
+29 −0
Original line number Original line Diff line number Diff line
// import { Route } from 'react-router-dom';
import { EndingNumberOutput } from './features/ending-number-output/endingNumberOutput.js';
import { NumberInputField } from './features/number-input-field/numberInputField.js';
import { UnitSelector } from './features/unit-selector/unitSelector.js';
import { Card } from './features/card/card.js';

/*  What is this used for????
      <Route exact path={'/'}>
      </Route>
*/

export function App() {
  return (
    <>
      <Card title="Input Unit Selector">
        <UnitSelector type="Input"/>
      </Card>
      <Card title="Number Input">
        <NumberInputField />
      </Card>
      <Card title="Output Unit Selector">
        <UnitSelector type="Output"/>
      </Card>
      <Card title="Number Output">
        <EndingNumberOutput />
      </Card>
    </>
  );
}
+10 −0
Original line number Original line Diff line number Diff line
import { configureStore } from '@reduxjs/toolkit';
import unitSelectorSlice from '../features/unit-selector/unitSelectorSlice.js';
import numberInputFieldSlice from '../features/number-input-field/numberInputFieldSlice.js';

export const store = configureStore({
  reducer: {
    [unitSelectorSlice.name]: unitSelectorSlice.reducer,
    [numberInputFieldSlice.name]: numberInputFieldSlice.reducer,
  },
});
+51 −0
Original line number Original line Diff line number Diff line
* {
  box-sizing: border-box;
}

.card {
  display: flex;
  justify-content: space-around;
  margin: 7%;
  min-height: 20%;
  border-radius: 10px;
  background-color: rgba(255 255 255 / 40%);
  box-shadow:
    0 2.8px 2.2px rgba(0 0 0 / 3.4%),
    0 6.7px 5.3px rgba(0 0 0 / 4.8%),
    0 12.5px 10px rgba(0 0 0 / 6%),
    0 22.3px 17.9px rgba(0 0 0 / 7.2%),
    0 41.8px 33.4px rgba(0 0 0 / 8.6%),
    0 100px 80px rgba(0 0 0 / 12%);
  padding-left: 20px;
  padding-right: 20px;
}

.cardTitle {
  align-self: flex-start;
}

.selectorContainer {
  justify-self: center;
  align-self: center;
}

.selector {
  border-color: rgba(255 255 255 / 0%);
  border-radius: 5px;
}

.inputFieldContainer {
  justify-self: center;
  align-self: center;
}

.inputField {
  border-color: rgba(255 255 255 / 0%);
  border-radius: 5px;
  max-width: 75%;
}

.endingNumberHolder {
  justify-self: center;
  align-self: center;
}
+15 −0
Original line number Original line Diff line number Diff line
import PropTypes from 'prop-types';
import './card.css';

export function Card(props) {
  Card.propTypes = {
    title: PropTypes.string.isRequired,
  };

  return (
    <div className="card">
      <h1 className="cardTitle">{props.title}</h1>
      {props.children}
    </div>
  );
}
+43 −0
Original line number Original line Diff line number Diff line
/* eslint-disable no-magic-numbers */
import { useSelector } from 'react-redux';
import { selectCurrentOutputUnit, selectCurrentInputUnit } from '../unit-selector/unitSelectorSlice';
import { selectNumberOfUnits } from '../number-input-field/numberInputFieldSlice';

export function EndingNumberOutput() {
  const conversionTable = {
    // To use these conversion multiply your specified unit with
    // its number to get meters.
    unitsToMeters: {
      km: 1000,
      m: 1.0,
      cm: 1 / 100,
      ft: 1 / 3.281,
      in: 1 / 39.3701,
    },
    // Multiply a number of meters by one of these conversions to
    // get that number of units.
    metersToUnits: {
      km: 1 / 1000,
      m: 1.0,
      cm: 100,
      ft: 3.281,
      in: 39.3701,
    },
  };

  const currentInputUnit = useSelector(selectCurrentInputUnit);
  const currentOutputUnit = useSelector(selectCurrentOutputUnit);
  const numberOfUnits = parseFloat(useSelector(selectNumberOfUnits));
  const numberOfMeters = numberOfUnits * conversionTable.unitsToMeters[currentInputUnit];
  let convertedNumber = numberOfMeters * conversionTable.metersToUnits[currentOutputUnit];

  if (isNaN(convertedNumber)) {
    convertedNumber = 'Please enter number to convert to ';
  }

  return (
    <div className="endingNumberHolder">
      <h1>{convertedNumber} {currentOutputUnit}</h1>
    </div>
  );
}
+21 −0
Original line number Original line Diff line number Diff line
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`the NumberInputField component numberInputField displays apropriate content 1`] = `
<div>
  <div
    class="inputFieldContainer"
  >
    <label
      for="inputField"
    >
      inputField
    </label>
    <input
      class="inputField"
      id="inputField"
      placeholder="Enter # of \\"cm\\""
      type="text"
    />
  </div>
</div>
`;
+26 −0
Original line number Original line Diff line number Diff line
import { useSelector, useDispatch } from 'react-redux';
import { selectCurrentInputUnit } from '../unit-selector/unitSelectorSlice';
import { setNumberOfUnits } from './numberInputFieldSlice';

export function NumberInputField() {
  const currentInputUnit = useSelector(selectCurrentInputUnit);
  const placeHolderString = `Enter # of "${currentInputUnit}"`;
  const dispatch = useDispatch();

  function handleChange(e) {
    const currentNumber = e.target.value;
    dispatch(setNumberOfUnits(currentNumber));
  }

  return (
    <div className="inputFieldContainer">
      <label for="inputField">inputField</label>
      <input id="inputField" className="inputField" type="text"
        placeholder={ placeHolderString } onChange={handleChange}/>
    </div>
  );
}

export const internals = {
  NumberInputField,
};
+58 −0
Original line number Original line Diff line number Diff line
import { render, screen } from '@testing-library/react';
import '../../testing/mockRedux.js';
import { NumberInputField} from './numberInputField.js';
import numberInputFieldSlice from '../number-input-field/numberInputFieldSlice.js';

import {
  selectNumberOfUnits,
  setNumberOfUnits,
} from './numberInputFieldSlice.js';

import {
  selectCurrentInputUnit,
} from '../unit-selector/unitSelectorSlice.js';

jest.mock('../unit-selector/unitSelectorSlice.js', () => ({
  selectCurrentInputUnit: jest.fn().mockName('selectCurrentInputUnit'),
}));

describe('the NumberInputField slice', () => {
  test('selects input value according to the store', () => {
    const state = {
      unitSelector: {
        currentInputUnit: 'm',
        currentOutputUnit: 'cm',
      },
      numberInputField: {
        numberToConvert: 10,
      },
    };

    const selectedInputUnit = selectNumberOfUnits(state);
    expect(selectedInputUnit).toEqual(10);
  });
});

describe('the NumberInputField slice', () => {
  test('setNumberOfUnits correctly adjusts state value', () => {
    // This is what the local state looks like to the "setNumberOfUnits" reducer.
    const state = numberInputFieldSlice.reducer({
      numberToConvert: 10,
    }, setNumberOfUnits(100));

    const numberOfUnits = 100;
    expect(state.numberToConvert).toEqual(numberOfUnits);
  });
});

describe('the NumberInputField component', () => {
  test('numberInputField displays apropriate content', () => {
    selectCurrentInputUnit.mockReturnValue('cm');

    const { container } = render(<NumberInputField/>);
    expect(screen.getByLabelText('inputField')).toHaveAttribute(
      'placeholder', expect.stringContaining('Enter # of "cm"'),
    );
    expect(container).toMatchSnapshot();
  });
});
+23 −0
Original line number Original line Diff line number Diff line
import { createSlice } from '@reduxjs/toolkit';

const numberInputFieldSlice = createSlice({
  name: 'numberInputField',
  initialState: {
    numberToConvert: 0,
  },
  reducers: {
    setNumberOfUnits: (state, action) => {
      const numToConvert = action.payload;
      state.numberToConvert = numToConvert;
    },
  },
});
export default numberInputFieldSlice;

export const {
  setNumberOfUnits,
} = numberInputFieldSlice.actions;

export function selectNumberOfUnits(state) {
  return state.numberInputField.numberToConvert;
}
+63 −0
Original line number Original line Diff line number Diff line
import { store } from '../../app/store';
import { useDispatch } from 'react-redux';
import {
  setCurrentInputUnit,
  setCurrentOutputUnit,
} from './unitSelectorSlice';
import PropTypes from 'prop-types';

export function UnitSelector(props) {
  // type will either be 'Input' or 'Output'
  UnitSelector.propTypes = {
    type: PropTypes.string.isRequired,
  };

  const unitSelectorOptions = [
    {
      label: 'Km',
      value: 'km',
    },
    {
      label: 'M',
      value: 'm',
    },
    {
      label: 'Cm',
      value: 'cm',
    },
    {
      label: 'Ft',
      value: 'ft',
    },
    {
      label: 'In',
      value: 'in',
    },
  ];

  const disbatch = useDispatch();
  function handleChange(e) {
    const unsubscribe = store.subscribe(() =>
      console.log('State after dispatch: ', store.getState()),
    );
    const currentUnit = e.target.value;
    console.log(currentUnit);

    if (props.type === 'Input') {
      disbatch(setCurrentInputUnit(currentUnit));
    }
    if (props.type === 'Output') {
      disbatch(setCurrentOutputUnit(currentUnit));
    }
    unsubscribe();
  }

  return (
    <div className="selectorContainer">
      <select className="selector" name="units" onChange={handleChange}>
        {unitSelectorOptions.map((option) =>
          <option value={option.value}>{option.label}</option>)}
      </select>
    </div>
  );
}
+66 −0
Original line number Original line Diff line number Diff line
import '../../testing/mockRedux.js';

import unitSelectorSlice, {
  selectCurrentInputUnit,
  selectCurrentOutputUnit,
  setCurrentInputUnit,
  setCurrentOutputUnit,
} from './unitSelectorSlice.js';

describe('the UnitSelector slice', () => {
  test('selects inputUnit value according to the store', () => {
    const state = {
      unitSelector: {
        currentInputUnit: 'm',
        currentOutputUnit: 'cm',
      },
      numberInputField: {
        numberToConvert: 10,
      },
    };

    const selectedInputUnit = selectCurrentInputUnit(state);
    expect(selectedInputUnit).toEqual('m');
  });
});

describe('the UnitSelector slice', () => {
  test('selects outputUnit value according to the store', () => {
    const state = {
      unitSelector: {
        currentInputUnit: 'm',
        currentOutputUnit: 'cm',
      },
      numberInputField: {
        numberToConvert: 10,
      },
    };

    const selectedOutputUnit = selectCurrentOutputUnit(state);
    expect(selectedOutputUnit).toEqual('cm');
  });
});

describe('the UnitSelector slice', () => {
  test('setcurrentInputUnit correctly adjusts state value', () => {
    // This is what the local state looks like to the "setCurrentInputUnit" reducer.
    const state = unitSelectorSlice.reducer({
      currentInputUnit: 'm',
      currentOutputUnit: 'cm',
    }, setCurrentInputUnit('km'));

    expect(state.currentInputUnit).toEqual('km');
  });
});

describe('the UnitSelector slice', () => {
  test('setcurrentOutputUnit correctly adjusts state value', () => {
    // This is what the local state looks like to the "setCurrentOutputUnit" reducer.
    const state = unitSelectorSlice.reducer({
      currentInputUnit: 'm',
      currentOutputUnit: 'cm',
    }, setCurrentOutputUnit('km'));

    expect(state.currentOutputUnit).toEqual('km');
  });
});
+34 −0
Original line number Original line Diff line number Diff line
import { createSlice } from '@reduxjs/toolkit';

const unitSelectorSlice = createSlice({
  name: 'unitSelector',
  initialState: {
    currentInputUnit: 'km',
    currentOutputUnit: 'km',
  },
  reducers: {
    // The action payload should have the unit to switch to with the in or out
    setCurrentInputUnit: (state, action) => {
      const newUnit = action.payload;
      state.currentInputUnit = newUnit;
    },
    setCurrentOutputUnit: (state, action) => {
      const newUnit = action.payload;
      state.currentOutputUnit = newUnit;
    },
  },
});
export default unitSelectorSlice;

export const {
  setCurrentInputUnit,
  setCurrentOutputUnit,
} = unitSelectorSlice.actions;

export function selectCurrentInputUnit(state) {
  return state.unitSelector.currentInputUnit;
}

export function selectCurrentOutputUnit(state) {
  return state.unitSelector.currentOutputUnit;
}
+47 −0
Original line number Original line Diff line number Diff line
:root {
  /* Colors */
  --letterbox-color: rgba(255 255 255 / 100%);

  /* background-color: rgba(101 142 156 / 100%); */
  --app-background-color: linear-gradient(102.28deg, rgba(231 189 251 / 50%) -7.7%, rgba(250 208 218 / 50%) 38.01%, rgba(168 198 243 / 50%) 91.43%);
  --font-color: rgba(0 0 0 / 100%);

  /* Sizes */
  --minimum-app-size: 300px;
}

* {
  font-size: 1.05em;
}

body {
  margin: 0;
  font-family: sans-serif;
  color: var(--font-color);
}

#root {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background: var(--letterbox-color);
}

#portrait {
  position: relative;
  margin: auto;
  min-width: var(--minimum-app-size);
  min-height: var(--minimum-app-size);
  width: 100%;
  height: 100%;
  max-width: 62.5vh;
  background: var(--app-background-color);
  overflow: hidden;
  transform: scale(1);
}

button {
  -webkit-tap-highlight-color: transparent;
}
+27 −0
Original line number Original line Diff line number Diff line
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { HashRouter as Router } from 'react-router-dom';

import { App } from './app.js';
import { store } from './app/store.js';

import './index.css';

import * as serviceWorkerRegistration from './serviceWorkerRegistration.js';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <Router>
        <div id="portrait">
          <App />
        </div>
      </Router>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.register();
+75 −0
Original line number Original line Diff line number Diff line
/* eslint-disable no-restricted-globals, no-underscore-dangle */

// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.

import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';

clientsClaim();

// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);

// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
  // Return false to exempt requests from being fulfilled by index.html.
  ({ request, url }) => {
    // If this isn't a navigation, skip.
    if (request.mode !== 'navigate') {
      return false;
    } // If this is a URL that starts with /_, skip.

    if (url.pathname.startsWith('/_')) {
      return false;
    } // If this looks like a URL for a resource, because it contains // a file extension, skip.

    if (url.pathname.match(fileExtensionRegexp)) {
      return false;
    } // Return true to signal that we want to use the handler.

    return true;
  },
  createHandlerBoundToURL(`${process.env.PUBLIC_URL}/index.html`),
);

const MAX_ENTRIES = 50;

// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
  // Add in any other file extensions or routing criteria as needed.
  // Customize this strategy as needed, e.g., by changing to CacheFirst.
  ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [
      // Ensure that once this runtime cache reaches a maximum size the
      // least-recently used images are removed.
      new ExpirationPlugin({ maxEntries: MAX_ENTRIES }),
    ],
  }),
);

// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

// Any other custom service worker logic can go here.
+140 −0
Original line number Original line Diff line number Diff line
/* eslint-disable no-restricted-syntax */

// This optional code is used to register a service worker.
// register() is not called by default.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.0/8 are considered localhost for IPv4.
    window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
);

export function register(config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (isLocalhost) {
        // This is running on localhost. Let's check if a service worker still exists or not.
        checkValidServiceWorker(swUrl, config);

        // Add some additional logging to localhost, pointing developers to the
        // service worker/PWA documentation.
        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service worker. To learn more, visit https://cra.link/PWA',
          );
        });
      } else {
        // Is not localhost. Just register service worker
        registerValidSW(swUrl, config);
      }
    });
  }
}

function registerValidSW(swUrl, config) {
  navigator.serviceWorker
    .register(swUrl)
    .then((registration) => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        if (installingWorker === null) {
          return;
        }
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the updated precached content has been fetched,
              // but the previous service worker will still serve the older
              // content until all client tabs are closed.
              console.log(
                'New content is available and will be used when all ' +
                  'tabs for this page are closed. See https://cra.link/PWA.',
              );

              // Execute callback
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.');

              // Execute callback
              if (config && config.onSuccess) {
                config.onSuccess(registration);
              }
            }
          }
        };
      };
    })
    .catch((error) => {
      console.error('Error during service worker registration:', error);
    });
}

const NOT_FOUND = 404;

function checkValidServiceWorker(swUrl, config) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' },
  })
    .then((response) => {
      // Ensure service worker exists, and that we really are getting a JS file.
      const contentType = response.headers.get('content-type');
      if (
        response.status === NOT_FOUND ||
        (contentType !== null && contentType.indexOf('javascript') === -1)
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then((registration) => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl, config);
      }
    })
    .catch(() => {
      console.log('No internet connection found. App is running in offline mode.');
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready
      .then((registration) => {
        registration.unregister();
      })
      .catch((error) => {
        console.error(error.message);
      });
  }
}
+5 −0
Original line number Original line Diff line number Diff line
import '@testing-library/jest-dom/extend-expect';

afterEach(() => {
  jest.clearAllMocks();
});
+11 −0
Original line number Original line Diff line number Diff line
export const mockDispatch = jest.fn().mockName('dispatch');

jest.mock('react-redux', () => ({
  useSelector: jest.fn((selector) => {
    if (selector.mock === undefined) {
      throw new Error(`Call to unmocked selector ${selector.name}`);
    }
    return selector();
  }).mockName('useSelector'),
  useDispatch: jest.fn().mockName('useDispatch').mockReturnValue(mockDispatch),
}));