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
29 files
+ 700
0
Compare changes
  • Side-by-side
  • Inline

Files

.gitattributes

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

.gitignore

0 → 100644
+23 −0
Original line number 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 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
+183 −0
Original line number 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:

```bash
$ 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:

```bash
$ npm install
```

(You may see a few 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.

## 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 the universal 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` in the outer folder to update `package-lock.json` based
   on the above changes.
Original line number Diff line number Diff line
Subproject commit a85278e2bd62f637800bc32fa6b372bc2855546e

minimal-app/.gitignore

0 → 100644
+23 −0
Original line number 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.

+81 −0
Original line number Diff line number Diff line
{
  "name": "@unlsoft/minimal-app",
  "version": "1.0.0",
  "description": "A minimal app to be used as starter code for labs and homework.",
  "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.8.3",
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^14.3.0",
    "classnames": "^2.3.1",
    "npm-run-all": "^4.1.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-redux": "^8.0.2",
    "react-router-dom": "^6.3.0",
    "react-scripts": "^5.0.1",
    "workbox-background-sync": "^6.5.3",
    "workbox-broadcast-update": "^6.5.3",
    "workbox-cacheable-response": "^6.5.3",
    "workbox-core": "^6.5.3",
    "workbox-expiration": "^6.5.3",
    "workbox-navigation-preload": "^6.5.3",
    "workbox-precaching": "^6.5.3",
    "workbox-range-requests": "^6.5.3",
    "workbox-routing": "^6.5.3",
    "workbox-strategies": "^6.5.3",
    "workbox-streams": "^6.5.3"
  },
  "devDependencies": {
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "@unlsoft/eslint-config": "file:../eslint-config",
    "@unlsoft/stylelint-config": "file:../stylelint-config",
    "eslint-plugin-jest-dom": "^4.0.2",
    "stylelint": "^14.9.1"
  },
  "stylelint": {
    "extends": "@unlsoft/stylelint-config"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest",
      "plugin:jest-dom/recommended",
      "plugin:testing-library/react",
      "@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"
    ]
  }
}
+24 −0
Original line number 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="A minimal app to be used as starter code for labs and homework."
    />
    <meta name="theme-color" content="#d00000" />
    <link rel="icon" href="%PUBLIC_URL%/logo.png" />
    <link rel="icon" href="%PUBLIC_URL%/logo.svg" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
    <link rel="apple-touch-startup-image" href="%PUBLIC_URL%/logo.png" />
    <link rel="apple-touch-startup-image" href="%PUBLIC_URL%/logo.svg" />
    <title>Minimal React Redux App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
+9.58 KiB

9.58 KiB

+5 −0
Original line number Diff line number Diff line
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152 152">
  <rect x="0" y="0" width="152" height="152" fill="rgba(0 0 0 / 100%)" />
  <path d="M147,1H90V42h10V75.673L53.532,2.393,52.648,1H2V42H12v66H2v41H62V108H52V74.336l46.467,73.271L99.351,149H150V108H140V42h10V1Z" stroke-width="3" stroke="rgba(255 255 255 / 100%)" fill="rgba(208 0 0 / 100%)">
  </path>
</svg>
+24 −0
Original line number Diff line number Diff line
{
  "short_name": "Minimal App",
  "name": "Minimal React Redux App",
  "description": "A minimal app to be used as starter code for labs and homework.",
  "icons": [
    {
      "src": "logo.svg",
      "type": "image/svg+xml",
      "sizes": "192x192 512x512",
      "purpose": "any maskable"
    },
    {
      "src": "logo.png",
      "type": "image/png",
      "sizes": "512x512",
      "purpose": "any maskable"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#d00000",
  "background_color": "#ffffff"
}
+15 −0
Original line number Diff line number Diff line
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`the app has one labeled button that displays the value from the store 1`] = `
<div>
  <label>
    Taps
    : 
    <button
      class="blue"
    >
      9999
    </button>
  </label>
</div>
`;

minimal-app/src/app.js

0 → 100644
+15 −0
Original line number Diff line number Diff line
import { Routes, Route } from 'react-router-dom';

import { Counter } from './features/counter/counter.js';

export function App() {
  const page =
    <>
      <Counter label={'Taps'} />
    </>;
  return (
    <Routes>
      <Route path={'/*'} element={page} />
    </Routes>
  );
}
+24 −0
Original line number Diff line number Diff line
import { render } from '@testing-library/react';
import './testing/mockRedux.js';
import { MemoryRouter as Router } from 'react-router-dom';

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

import {
  selectValue,
} from './features/counter/counterSlice.js';
jest.mock('./features/counter/counterSlice.js', () => ({
  selectValue: jest.fn().mockName('selectValue'),
}));

describe('the app', () => {
  test('has one labeled button that displays the value from the store', () => {
    selectValue.mockReturnValue(9999);
    const { container } = render(
      <Router initialEntries={['/']}>
        <App />
      </Router>,
    );
    expect(container).toMatchSnapshot();
  });
});
+9 −0
Original line number Diff line number Diff line
import { configureStore } from '@reduxjs/toolkit';

import counterSlice from '../features/counter/counterSlice.js';

export const store = configureStore({
  reducer: {
    [counterSlice.name]: counterSlice.reducer,
  },
});
+36 −0
Original line number Diff line number Diff line
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';

import classNames from 'classnames';
import styles from './counter.module.css';

import {
  selectValue,
  setValue,
} from './counterSlice.js';

export function Counter(props) {
  const value = useSelector(selectValue);
  const dispatch = useDispatch();

  const classes = classNames({
    [styles.red]: value % 2 === 0,
    [styles.blue]: value % 2 === 1,
  });
  const onClick = () => dispatch(setValue({
    value: value + 1,
  }));

  return (
    <label>
      {props.label}:&nbsp;
      <button className={classes} onClick={onClick}>
        {value}
      </button>
    </label>
  );
}

Counter.propTypes = {
  label: PropTypes.string.isRequired,
};
+7 −0
Original line number Diff line number Diff line
.red {
  color: rgba(255 0 0 / 100%);
}

.blue {
  color: rgba(0 0 255 / 100%);
}
+41 −0
Original line number Diff line number Diff line
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '../../testing/mockRedux.js';

import { Counter } from './counter.js';

import {
  selectValue,
  setValue,
} from './counterSlice.js';
jest.mock('./counterSlice.js', () => ({
  selectValue: jest.fn().mockName('selectValue'),
  setValue: jest.fn().mockName('setValue'),
}));

describe('the button', () => {
  test('displays the value from the store', () => {
    selectValue.mockReturnValue(9999);
    render(<Counter label={'Foo'} />);
    expect(screen.getByRole('button')).toHaveTextContent(/^9999$/);
  });
  test('is red when the value from the store is even', () => {
    selectValue.mockReturnValue(9998);
    render(<Counter label={'Foo'} />);
    expect(screen.getByRole('button')).toHaveClass('red');
  });
  test('is blue when the value from the store is odd', () => {
    selectValue.mockReturnValue(9999);
    render(<Counter label={'Foo'} />);
    expect(screen.getByRole('button')).toHaveClass('blue');
  });
  test('increments the value in the store when clicked', async() => {
    selectValue.mockReturnValue(9999);
    render(<Counter label={'Foo'} />);
    await userEvent.click(screen.getByRole('button'));
    expect(setValue).toHaveBeenCalledTimes(1);
    expect(setValue).toHaveBeenCalledWith({
      value: 9999 + 1,
    });
  });
});
+25 −0
Original line number Diff line number Diff line
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    setValue: (counter, action) => {
      const {
        value,
      } = action.payload;
      counter.value = value;
    },
  },
});
export default counterSlice;

export const {
  setValue,
} = counterSlice.actions;

export function selectValue(state) {
  return state.counter.value;
}
+36 −0
Original line number Diff line number Diff line
import counterSlice, {
  selectValue,
  setValue,
} from './counterSlice.js';

describe('the initial state', () => {
  test('has a value of zero', () => {
    const state = counterSlice.reducer(undefined, {});
    expect(state).toEqual({
      value: 0,
    });
  });
});

describe('selectValue', () => {
  test('reads the stored value', () => {
    const state = {
      value: 9999,
    };
    const result = selectValue({ [counterSlice.name]: state });
    expect(result).toBe(9999);
  });
});

describe('setValue', () => {
  test('overwrites the stored value', () => {
    const state = counterSlice.reducer({
      value: 8888,
    }, setValue({
      value: 9999,
    }));
    expect(state).toEqual({
      value: 9999,
    });
  });
});
+41 −0
Original line number Diff line number Diff line
:root {
  /* Colors */
  --letterbox-color: rgba(0 0 0 / 100%);
  --app-background-color: rgba(239 239 239 / 100%);
  --font-color: rgba(0 0 0 / 100%);

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

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;
}
+21 −0
Original line number Diff line number Diff line
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { HashRouter as Router } from 'react-router-dom';

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

import './index.css';

createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <Router>
        <div id="portrait">
          <App />
        </div>
      </Router>
    </Provider>
  </React.StrictMode>,
);
+1 −0
Original line number Diff line number Diff line
import '@testing-library/jest-dom/extend-expect';
+11 −0
Original line number 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),
}));

package-lock.json

0 → 100644
+0 −0

File added.

Preview size limit exceeded, changes collapsed.

package.json

0 → 100644
+30 −0
Original line number Diff line number Diff line
{
  "name": "@unlsoft/starter-code",
  "version": "1.0.0",
  "description": "A project skeleton to be used as starter code for labs and homework.",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "postinstall:stylelint-config": "cd stylelint-config && npm install",
    "postinstall:eslint-config": "cd eslint-config && npm install",
    "postinstall:app": "cd minimal-app && npm install",
    "postinstall": "run-s postinstall:**",
    "lint:app": "cd minimal-app && npm run lint",
    "lint": "run-s --continue-on-error lint:**",
    "test-once:app": "cd minimal-app && npm run test-once",
    "test-once": "run-s --continue-on-error test-once:**",
    "test": "run-s test-once",
    "start": "cd minimal-app && npm run start",
    "build:app": "cd minimal-app && 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 Diff line number Diff line
{
	"folders": [
		{
			"path": "."
		}
	],
	"settings": {
		"files.eol": "\n",
		"files.exclude": {
			"**/node_modules": true
		},
		"files.trimFinalNewlines": true,
		"files.trimTrailingWhitespace": true
	}
}
Original line number Diff line number Diff line
Subproject commit 948fc884d068e87763fa0f0c165ca0cd256bf43d