14 Commits

Author SHA1 Message Date
ddc552c584 Merge pull request #3 from Inforitnl/feature/contract-testing
Added a nestJS based contract api
2022-08-15 11:46:22 +02:00
3db77f96b9 Added a nestJS based contract api
- Added an example with trucks and basic fetch in useEffect on page load
  - Added simply test to see whether any data is displayed (and shows the interceptor)
Introduced "CypressStrictMode" which wraps React.StrictMode and checks whether Cypress is involved, if so disable StrictMode.
2022-08-15 11:42:19 +02:00
c0a0ea66a6 eslint now receives the glob itself 2022-08-08 14:48:59 +02:00
4b61b4a370 Added styled components
- also removed existing css
  - left the capabilities included
2022-08-08 14:08:16 +02:00
e733c4a9f6 Merge pull request #2 from Inforitnl/master
Sync back to private stuff
2022-07-26 11:18:17 +02:00
b9d3025163 Added react-oidc (use demo/demo)
Added an example of an authentication protected page (tenders)
Added an example with the built in proxy (to combat CORS) (tendersguru)
2022-07-26 11:15:36 +02:00
8496f5cfbe Moved examples into example directory
Moved routes to separate file
Used route constants
2022-07-26 10:32:11 +02:00
de1484e9a1 Merge branch 'Mastermindzh:master' into master 2022-07-25 12:44:35 +02:00
2728820b71 Merge branch 'Mastermindzh:master' into master 2022-07-25 11:22:28 +02:00
ca0f973d07 Merge branch 'Mastermindzh:master' into master 2022-07-25 11:20:50 +02:00
126c80c7c4 Merge pull request #2 from Inforitnl/update/from-upstream
Update/from upstream
2022-07-25 11:16:50 +02:00
3d5e41ec42 Merge branch 'master' of github.com:Mastermindzh/react-starter-kit into update/from-upstream 2022-07-25 11:15:48 +02:00
dc81451685 Merge branch 'Mastermindzh:master' into master 2022-07-19 12:22:54 +02:00
d41e9d3af4 forking outside of github instructions 2022-07-19 11:17:38 +02:00
62 changed files with 16446 additions and 601 deletions

View File

@@ -2,5 +2,6 @@
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged npm run lint-staged
npm run lint
npm run test-ci npm run test-ci
npm run e2e-ci npm run e2e-ci

View File

@@ -3,15 +3,21 @@
"browserslist", "browserslist",
"camelcase", "camelcase",
"deepmerge", "deepmerge",
"etags",
"flexbugs", "flexbugs",
"Immer", "Immer",
"Keycloak",
"languagedetector", "languagedetector",
"lcov",
"luxon", "luxon",
"nestjs",
"oidc",
"pmmmwh", "pmmmwh",
"preinstall", "preinstall",
"reduxjs", "reduxjs",
"SVGR", "SVGR",
"tailwindcss", "tailwindcss",
"tendersguru",
"testid", "testid",
"typeahead", "typeahead",
"uncompiled" "uncompiled"

View File

@@ -4,7 +4,7 @@
"body": [ "body": [
"import { FunctionComponent } from \"react\";", "import { FunctionComponent } from \"react\";",
"", "",
"type Props = {}", "type Props = {};",
"", "",
"export const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = () => {", "export const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = () => {",
" return <h1>${1:${TM_FILENAME_BASE}}</h1>;", " return <h1>${1:${TM_FILENAME_BASE}}</h1>;",
@@ -68,5 +68,27 @@
"", "",
"export default ${1:${TM_FILENAME_BASE}}Slice.reducer;" "export default ${1:${TM_FILENAME_BASE}}Slice.reducer;"
] ]
},
"react-i18next useTranslate hook": {
"prefix": ["useTranslation", "translate", "i18-trans"],
"body": ["const [translate] = useTranslation();"]
},
"react-styled-component": {
"prefix": ["rsfc", "rsc", "react-styled-component"],
"body": [
"import { FunctionComponent } from \"react\";",
"import styled from \"styled-components\";",
"",
"type Props = { className?: string };",
"",
"const ${1:${TM_FILENAME_BASE}}: FunctionComponent<Props> = ({className}) => {",
" return <h1 className={className}>${1:${TM_FILENAME_BASE}}</h1>;",
"};",
"",
"const Styled${1:${TM_FILENAME_BASE}} = styled(${1:${TM_FILENAME_BASE}})``;",
"",
"export { Styled${1:${TM_FILENAME_BASE}} as ${1:${TM_FILENAME_BASE}} };",
""
]
} }
} }

View File

@@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.2] - 2022-08-15
- Added a nestJS based contract api
- Added an example with trucks and basic fetch in useEffect on page load
- Added simply test to see whether any data is displayed (and shows the interceptor)
- Introduced "CypressStrictMode" which wraps React.StrictMode and checks whether Cypress is involved, if so disable StrictMode.
## [0.6.1] - 2022-08-08
- eslint now receives the glob itself
## [0.6.0] - 2022-08-08
- Added styled components
- also removed existing css
- left the capabilities included
## [0.5.0] - 2022-06-26
- Added react-oidc (use demo/demo)
- Added an example of an authentication protected page (tenders)
- Added an example with the built in proxy (to combat CORS) (tendersguru)
## [0.4.1] - 2022-06-26
- Moved examples into example directory
- Moved routes to separate file
- Used route constants
## [0.4.0] - 2022-07-25 ## [0.4.0] - 2022-07-25
- Added the possibility to override partial configs during deployments - Added the possibility to override partial configs during deployments

View File

@@ -17,6 +17,7 @@ Includes:
- [Getting started](#getting-started) - [Getting started](#getting-started)
- [Project structure](#project-structure) - [Project structure](#project-structure)
- ["Forking" outside of Github](#forking-outside-of-github)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Using the `config.ts` file](#using-the-configts-file) - [Using the `config.ts` file](#using-the-configts-file)
- [adding values](#adding-values) - [adding values](#adding-values)
@@ -56,6 +57,17 @@ Only the important files are shown
└── tsconfig.json └── tsconfig.json
``` ```
## "Forking" outside of Github
To use this base in other git software (not Github) you will have to manually manage the upstream.
Go into your existing repo and execute the following commands:
1. `git remote add upstream <clone-url>`
2. `git pull upstream master` # or other branchname
3. `git push`
Then, when you need to sync again you can repeat step 2 and 3
## Configuration ## Configuration
This starter kit comes with runtime configuration out-of-the-box. This starter kit comes with runtime configuration out-of-the-box.

View File

@@ -0,0 +1,66 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
sourceType: "module",
},
plugins: ["@typescript-eslint/eslint-plugin", "import"],
extends: ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: [".eslintrc.js"],
rules: {
"no-console": [
"error",
{
allow: ["debug", "error"],
},
],
"no-eval": "error",
"import/first": "error",
camelcase: [
"error",
{
ignoreImports: true,
ignoreDestructuring: true,
},
],
"consistent-return": "warn",
"comma-dangle": ["warn", "always-multiline"],
"constructor-super": "error",
curly: "error",
"eol-last": "warn",
eqeqeq: ["error", "smart"],
"import/order": "always",
"new-parens": "error",
"no-debugger": "error",
"no-fallthrough": "off",
"max-len": [
"warn",
{
code: 100,
},
],
"no-shadow": [
"error",
{
hoist: "all",
},
],
"no-trailing-spaces": "warn",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-arrow/prefer-arrow-functions": "off",
"prefer-const": "error",
radix: "off",
"space-in-parens": ["off", "never"],
quotes: [2, "double"],
},
};

35
contracts/api/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

11
contracts/api/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Contract api
This API is meant to emulate the different contracts that you have with external systems
## use api based routing
You can use the first part of the URL as your contract and put your resources after that.
e.g, for `fake` you could do:
`URL/fake/trucks` and `URL/fake/containers`.
Then simply make `URL/fake` your root url in the React config

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

14537
contracts/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
{
"name": "contract-testing-api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.4",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"inforit-prettier-config": "^1.0.0",
"jest": "28.1.2",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.0.0",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"prettier": "inforit-prettier-config"
}

View File

@@ -0,0 +1,3 @@
export const API_CONSTANTS = {
fake: "fake",
};

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from "@nestjs/common";
@Controller()
export class AppController {
@Get()
getHello(): string {
return "Welcome to the contract api";
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { FakeTrucksController } from "./contracts/fake/trucks.controller";
@Module({
imports: [],
controllers: [AppController, FakeTrucksController],
providers: [],
})
export class AppModule {}

View File

@@ -0,0 +1,21 @@
import { Controller, Get } from "@nestjs/common";
import { API_CONSTANTS } from "./../../api.constants";
@Controller(`${API_CONSTANTS.fake}/trucks`)
export class FakeTrucksController {
@Get()
get() {
return [
{
id: "de5ddb70-b2a7-4309-a992-62260a09683a",
licensePlate: "xx-yy-zz",
color: "black",
},
{
id: "087e0b0b-1c13-46e3-8920-762f5738072e",
licensePlate: "xx-yy-zz",
color: "red",
},
];
}
}

12
contracts/api/src/main.ts Normal file
View File

@@ -0,0 +1,12 @@
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// allow app to be called from everywhere
app.enableCors();
// you can disable etags so that you always get 200's instead of 304s :)
// app.getHttpAdapter().getInstance().set("etag", false);
await app.listen(9600);
}
bootstrap();

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

1
contracts/docs/.gitkeep Normal file
View File

@@ -0,0 +1 @@
Put your written contracts here

View File

@@ -6,6 +6,9 @@ export default defineConfig({
// implement node event listeners here // implement node event listeners here
}, },
}, },
env: {
appBaseUrl: "http://localhost:3000",
},
video: false, video: false,
reporter: "mocha-junit-reporter", reporter: "mocha-junit-reporter",
reporterOptions: { reporterOptions: {

27
cypress/e2e/trucks.cy.ts Normal file
View File

@@ -0,0 +1,27 @@
/// <reference types="cypress" />
describe("Application trucks", () => {
beforeEach(() => {
cy.visit(Cypress.env("appBaseUrl"));
});
it("Should render the navigation links", () => {
cy.get("[data-testid='nav']");
});
it("Should navigate to trucks and display a result when clicking on trucks", () => {
cy.get('[data-testid="nav.trucks"]').click();
cy.contains("trucks (contract api) page");
});
it("Should eventually (after the http call) display the results on screen", () => {
cy.intercept({
method: "GET",
url: "http://localhost:9600/fake/trucks",
}).as("getTrucks");
cy.get('[data-testid="nav.trucks"]').click();
cy.contains("trucks (contract api) page");
cy.wait("@getTrucks").its("response.statusCode").should("be.oneOf", [200, 304]);
cy.get('[data-testid="trucksResult"]').should("not.be.empty");
});
});

View File

@@ -34,4 +34,4 @@
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element> // visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// } // }
// } // }
// } // }

View File

@@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import "./commands";
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

1575
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "react-starter-kit", "name": "react-starter-kit",
"version": "0.4.0", "version": "0.6.2",
"description": "A modern, create-react-app-based, starter kit for React projects", "description": "A modern, create-react-app-based, starter kit for React projects",
"keywords": [ "keywords": [
"react", "react",
@@ -28,16 +28,19 @@
"build:prod": "npm run build", "build:prod": "npm run build",
"cypress-run": "cypress run", "cypress-run": "cypress run",
"e2e": "cypress open -d --e2e", "e2e": "cypress open -d --e2e",
"e2e-ci": "start-server-and-test start http://localhost:3000 cypress-run", "e2e-ci": "start-server-and-test start-e2e-dependencies \"http://localhost:3000|9600\" cypress-run",
"postinstall": "husky install", "postinstall": "husky install && npm install --prefix contracts/api",
"lint": "GLOBIGNORE='src/types' && eslint src/**", "lint": "eslint \"src/**\"",
"lint-staged": "lint-staged --relative", "lint-staged": "lint-staged --relative",
"organize-package-json": "npx format-package -w && npx sort-package-json", "organize-package-json": "npx format-package -w && npx sort-package-json",
"pretty-quick": "pretty-quick --staged", "pretty-quick": "pretty-quick --staged",
"start": "node scripts/start.js", "start": "node scripts/start.js",
"start-with-contract": "concurrently \"npm start\" \"npm run start-contract-api\"",
"start-contract-api": "npm run start:dev --prefix contracts/api",
"start-e2e-dependencies": "npm run start-with-contract",
"test": "node scripts/test.js --verbose", "test": "node scripts/test.js --verbose",
"test-ci": "node scripts/test.js --ci --coverage", "test-ci": "node scripts/test.js --ci --coverage",
"test-live-coverage": " concurrently --kill-others \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"", "test-live-coverage": "concurrently \"npm run test-with-coverage\" \"npx http-server -c-1 coverage/lcov-report\"",
"test-with-coverage": "node scripts/test.js --coverage", "test-with-coverage": "node scripts/test.js --coverage",
"test:prod": "npm run test-ci && npm run e2e-ci" "test:prod": "npm run test-ci && npm run e2e-ci"
}, },
@@ -45,6 +48,7 @@
"*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write" "*.{ts,js,jsx,tsx,css,scss,json,md}": "prettier --write"
}, },
"dependencies": { "dependencies": {
"@axa-fr/react-oidc": "^6.0.0-beta10",
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"i18next-http-backend": "^1.4.1", "i18next-http-backend": "^1.4.1",
@@ -55,7 +59,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"tailwindcss": "^3.1.6" "styled-components": "^5.3.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.9", "@babel/core": "^7.18.9",
@@ -70,6 +74,7 @@
"@types/node": "^18.6.1", "@types/node": "^18.6.1",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.25",
"babel-jest": "^27.4.2", "babel-jest": "^27.4.2",
"babel-loader": "^8.2.5", "babel-loader": "^8.2.5",
"babel-plugin-named-asset-import": "^0.3.8", "babel-plugin-named-asset-import": "^0.3.8",
@@ -95,6 +100,7 @@
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "^2.0.6",
"husky": "^8.0.1", "husky": "^8.0.1",
"i18next": "^21.8.14", "i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4", "i18next-browser-languagedetector": "^6.1.4",
@@ -110,7 +116,7 @@
"mocha-junit-reporter": "^2.0.2", "mocha-junit-reporter": "^2.0.2",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1", "postcss-loader": "^7.0.1",
"postcss-normalize": "^10.0.1", "postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.7.2", "postcss-preset-env": "^7.7.2",
"prettier": "^2.7.1", "prettier": "^2.7.1",

View File

@@ -1,6 +1,30 @@
const defaultConfig = { const defaultConfig = {
version: "0.1.0", version: "0.1.0",
name: "React-starter-kit", name: "React-starter-kit",
services: {
fake: {
root: "http://localhost:9600/fake",
trucks: "trucks",
},
},
// oidc: {
// url: "private_url",
// realm: "inforit",
// clientId: "react-base",
// scope: "openid profile email",
// redirectUri: "http://localhost:3000/authentication/callback",
// silentRedirectUri: "http://localhost:3000/authentication/silent-callback",
// serviceWorkerOnly: false,
// },
oidc: {
url: "https://sso.mastermindzh.tech/realms/public-tests",
realm: "public-tests",
clientId: "react-starter-kit",
scope: "openid profile email",
redirectUri: "http://localhost:3000/authentication/callback",
silentRedirectUri: "http://localhost:3000/authentication/silent-callback",
serviceWorkerOnly: false,
},
}; };
// ignore this :) // ignore this :)

View File

@@ -10,7 +10,9 @@
"nav": { "nav": {
"home": "home", "home": "home",
"about": "about", "about": "about",
"counter": "counter" "counter": "counter",
"tenders": "tenders (with auth)",
"trucks": "trucks (contract api)"
}, },
"about": { "about": {
"title": "About" "title": "About"

View File

@@ -10,7 +10,9 @@
"nav": { "nav": {
"home": "home", "home": "home",
"about": "over ons", "about": "over ons",
"counter": "teller" "counter": "teller",
"tenders": "aanbestedingen (met auth)",
"trucks": "trucks (contract api)"
}, },
"about": { "about": {
"title": "Over ons" "title": "Over ons"

View File

@@ -1,26 +1,13 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { AboutContainer } from "./features/about/About";
import { CounterContainer } from "./features/counter/Counter";
import { HomeContainer } from "./features/home/Home";
import { Navbar } from "./infrastructure/navbar/Navbar"; import { Navbar } from "./infrastructure/navbar/Navbar";
import { AppRoutes } from "./Routes";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<div className="App"> <div className="App">
<Navbar /> <Navbar />
<Routes> <AppRoutes />
<Route path="/" element={<HomeContainer />} />
<Route path="/about" element={<AboutContainer />} />
<Route path="/counter" element={<CounterContainer />} />
{/* <Route index element={<Home />} /> */}
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
{/* </Route> */}
</Routes>
</div> </div>
</BrowserRouter> </BrowserRouter>
); );

45
src/Routes.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { OidcSecure } from "@axa-fr/react-oidc";
import { FunctionComponent } from "react";
import { Route, Routes } from "react-router-dom";
import { AboutContainer } from "./features/about/About";
import { CounterContainer } from "./features/examples/counter/Counter";
import { Tenders } from "./features/examples/tenders/Tenders";
import { Trucks } from "./features/examples/trucks/Trucks";
import { HomeContainer } from "./features/home/Home";
type Props = {};
export const ROUTE_KEYS = {
home: "",
about: "about",
counter: "counter",
tenders: "tenders",
trucks: "trucks",
};
export const AppRoutes: FunctionComponent<Props> = () => {
const { home, about, counter, tenders, trucks } = ROUTE_KEYS;
return (
<Routes>
<Route path={home} element={<HomeContainer />} />
<Route path={about} element={<AboutContainer />} />
<Route path={counter} element={<CounterContainer />} />
{/* a route with authentication */}
<Route
path={tenders}
element={
<OidcSecure>
<Tenders />
</OidcSecure>
}
/>
<Route path={trucks} element={<Trucks />} />
{/* <Route index element={<Home />} /> */}
{/* <Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route> */}
{/* </Route> */}
</Routes>
);
};

View File

@@ -1,5 +1,5 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/state/counterSlice"; import counterReducer from "../features/examples/counter/state/counterSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {

View File

@@ -2,7 +2,14 @@ import { FunctionComponent, ReactNode } from "react";
import { I18nextProvider, useTranslation } from "react-i18next"; import { I18nextProvider, useTranslation } from "react-i18next";
import i18n from "./i18n"; import i18n from "./i18n";
type Props = { children?: ReactNode; keysOnly?: boolean }; type Props = {
children?: ReactNode;
/**
* Whether to show the translation keys instead of translated text
* Can be useful to test languages that don't have full translations
*/
keysOnly?: boolean;
};
const ProvidedComponent: FunctionComponent<Props> = ({ children, keysOnly }) => { const ProvidedComponent: FunctionComponent<Props> = ({ children, keysOnly }) => {
const [_translate, i18nSettings] = useTranslation(); const [_translate, i18nSettings] = useTranslation();

View File

@@ -1,4 +0,0 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) => setTimeout(() => resolve({ data: amount }), 500));
}

View File

@@ -18,7 +18,7 @@
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
margin-top: 2px; margin-top: 2px;
font-family: 'Courier New', Courier, monospace; font-family: "Courier New", Courier, monospace;
} }
.button { .button {
@@ -60,7 +60,7 @@
} }
.asyncButton:after { .asyncButton:after {
content: ''; content: "";
background-color: rgba(112, 76, 182, 0.15); background-color: rgba(112, 76, 182, 0.15);
display: block; display: block;
position: absolute; position: absolute;

View File

@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../../app/hooks";
import styles from "./Counter.module.css"; import styles from "./Counter.module.css";
import { incrementAsync } from "./state/actions/incrementAsync"; import { incrementAsync } from "./state/actions/incrementAsync";
import { import {

View File

@@ -0,0 +1,6 @@
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500),
);
}

View File

@@ -1,8 +1,4 @@
import counterReducer, { import counterReducer, { increment, decrement, incrementByAmount } from "./counterSlice";
increment,
decrement,
incrementByAmount,
} from "./counterSlice";
import { CounterState } from "../models/CounterState"; import { CounterState } from "../models/CounterState";
describe("counter reducer", () => { describe("counter reducer", () => {

View File

@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppThunk, RootState } from "../../../app/store"; import { AppThunk, RootState } from "../../../../app/store";
import { CounterState } from "../models/CounterState"; import { CounterState } from "../models/CounterState";
import { incrementAsync } from "./actions/incrementAsync"; import { incrementAsync } from "./actions/incrementAsync";

View File

@@ -0,0 +1,33 @@
import { useOidcAccessToken } from "@axa-fr/react-oidc";
import { FunctionComponent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
type Props = {};
export const Tenders: FunctionComponent<Props> = () => {
const [tenders, setTenders] = useState(null);
const [translate] = useTranslation();
const { accessToken } = useOidcAccessToken();
useEffect(() => {
// this uses the CORS proxy in our webpack setup
// tendersguru is mapped to "https://tenders.guru" in setupProxy.js
// it will strip the tendersguru part, so the full url will be: https://tenders.guru/api/ro/tenders
fetch("tendersguru/api/ro/tenders", {
// fetch's way of adding headers. Not required to access the api... but :shrug:
headers: new Headers({
Authorization: `Bearer ${accessToken}`,
}),
})
.then((response) => response.json())
.then((data) => {
setTenders(data);
});
}, [accessToken]);
return (
<>
<h1>{translate("nav.tenders")}</h1>
{tenders ? <pre>{JSON.stringify(tenders, null, 2)}</pre> : <h2>loading...</h2>}
</>
);
};

View File

@@ -0,0 +1,34 @@
import { useOidcAccessToken } from "@axa-fr/react-oidc";
import { FunctionComponent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Config } from "../../../infrastructure/config";
type Props = {};
export const Trucks: FunctionComponent<Props> = () => {
const [trucks, setTrucks] = useState(null);
const [translate] = useTranslation();
const { accessToken } = useOidcAccessToken();
useEffect(() => {
const { fake } = Config.services;
fetch(`${fake.root}/${fake.trucks}`, {
// fetch's way of adding headers. Not required to access the api... but :shrug:
headers: new Headers({
Authorization: `Bearer ${accessToken}`,
}),
})
.then((response) => response.json())
.then((data) => {
setTrucks(data);
});
}, [accessToken]);
return (
<>
<h1>{translate("nav.trucks")} page</h1>
<div data-testid="trucksResult">
{trucks ? <pre>{JSON.stringify(trucks, null, 2)}</pre> : <h2>loading...</h2>}
</div>
</>
);
};

View File

@@ -8,19 +8,39 @@ export const HomeContainer: FunctionComponent<Props> = () => {
<p>This is the react base app :)</p> <p>This is the react base app :)</p>
<span> <span>
<span>Learn </span> <span>Learn </span>
<a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
>
React React
</a> </a>
<span>, </span> <span>, </span>
<a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Redux
</a> </a>
<span>, </span> <span>, </span>
<a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://redux-toolkit.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Toolkit Redux Toolkit
</a> </a>
,<span> and </span> ,<span> and </span>
<a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> <a
className="App-link"
href="https://react-redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
React Redux React Redux
</a> </a>
</span> </span>

View File

@@ -1,11 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}

View File

@@ -1,24 +1,43 @@
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { createGlobalStyle } from "styled-components";
import App from "./App"; import App from "./App";
import { store } from "./app/store"; import { store } from "./app/store";
import "./index.css"; import { CypressStrictMode } from "./infrastructure/CypressStrictMode";
import "./infrastructure/i18n/init"; import "./infrastructure/i18n/init";
import { OidcProvider } from "./infrastructure/sso/OidcProvider";
import { Loader } from "./infrastructure/wrappers/WithPageSuspense"; import { Loader } from "./infrastructure/wrappers/WithPageSuspense";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
const container = document.getElementById("root")!; const container = document.getElementById("root")!;
const root = createRoot(container); const root = createRoot(container);
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
}
`;
root.render( root.render(
<React.StrictMode> <CypressStrictMode>
<GlobalStyle />
<Provider store={store}> <Provider store={store}>
<Loader> <OidcProvider>
<App /> <Loader>
</Loader> <App />
</Loader>
</OidcProvider>
</Provider> </Provider>
</React.StrictMode>, </CypressStrictMode>,
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@@ -0,0 +1,16 @@
import React, { FunctionComponent, ReactNode } from "react";
type Props = { children?: ReactNode };
/**
* React StrictMode that disables itself when detected to be running in Cypress
*/
export const CypressStrictMode: FunctionComponent<Props> = ({ children }) => {
const isInCypress = (window as any).Cypress;
if (isInCypress) {
return <>{children}</>;
} else {
return <React.StrictMode>{children}</React.StrictMode>;
}
};

View File

@@ -1,4 +1,18 @@
import { OIDCConfig } from "../sso/models/OIDCConfig";
export interface RunTimeConfig { export interface RunTimeConfig {
version: number; version: number;
name: string; name: string;
services: {
fake: {
root: string;
trucks: string;
};
};
/**
* Settings for the OIDC connection
*/
oidc: OIDCConfig;
} }

View File

@@ -1,4 +1,5 @@
import deepmerge from "deepmerge"; import deepmerge from "deepmerge";
import { RunTimeConfig } from "./RunTimeConfig";
/** /**
* gets and merges both the regular config and the override config from the window * gets and merges both the regular config and the override config from the window
@@ -12,4 +13,4 @@ export const mergeConfigs = () => {
mergeConfigs(); mergeConfigs();
export const Config = window.mergedConfig; export const Config: RunTimeConfig = window.mergedConfig;

View File

@@ -1,3 +0,0 @@
nav a {
padding-right: 10px;
}

View File

@@ -1,18 +1,25 @@
import { render, screen } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations"; import { WithTestTranslations } from "../../app/tests/mocks/i18n/WithTestTranslations";
import { OidcProvider } from "../sso/OidcProvider";
import { WithRouter } from "../wrappers/WithRouter"; import { WithRouter } from "../wrappers/WithRouter";
import { Navbar } from "./Navbar"; import { Navbar } from "./Navbar";
describe("Navbar container", () => { describe("Navbar container", () => {
it.only("renders a navigation section identified by the nav test-id", () => { it.only("renders a navigation section identified by the nav test-id", async () => {
render( render(
<WithTestTranslations> <WithTestTranslations>
<WithRouter> {/* for simple tests where we don't need auth we don't actually have to mock responses */}
<Navbar /> <OidcProvider>
</WithRouter> <WithRouter>
<Navbar />
</WithRouter>
</OidcProvider>
</WithTestTranslations>, </WithTestTranslations>,
); );
// because of the extra loaders we wait for a result just to be sure
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0); // see: https://testing-library.com/docs/guide-disappearance/
await waitFor(() => {
expect(screen.getAllByTestId("nav")?.length).toBeGreaterThan(0);
});
}); });
}); });

View File

@@ -1,38 +1,64 @@
import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import styled from "styled-components";
import { ROUTE_KEYS } from "../../Routes";
import { Config } from "../config"; import { Config } from "../config";
import "./Navbar.css"; type Props = { className?: string };
type Props = {};
export const Navbar: FunctionComponent<Props> = () => { const Navbar: FunctionComponent<Props> = ({ className }) => {
const [translate, i18n] = useTranslation(); const [translate, i18n] = useTranslation();
const { login, logout, isAuthenticated } = useOidc();
const { accessTokenPayload } = useOidcAccessToken();
const { home, about, counter, tenders, trucks } = ROUTE_KEYS;
return ( return (
<> <div className={className}>
<h1>{translate("navBar.intro")}</h1> <h1>{translate("navBar.intro")}</h1>
<p> <p>
{/* trans can also be used to translate */} {/* trans can also be used to translate */}
{Config.name} <Trans i18nKey="navBar.version">version:</Trans> {Config.name} <Trans i18nKey="navBar.version">version:</Trans>
{JSON.stringify(Config.version)} {JSON.stringify(Config.version)}
<button
onClick={() => {
isAuthenticated ? logout() : login("/");
}}
>
{isAuthenticated ? `logout (${accessTokenPayload.email})` : "login"}
</button>
</p> </p>
{/* This translation uses a formatter in the translation files */} {/* This translation uses a formatter in the translation files */}
<p>{translate("navBar.currentDate", { date: DateTime.now().toJSDate() })}</p> <p>{translate("navBar.currentDate", { date: DateTime.now().toJSDate() })}</p>
<nav data-testid="nav"> <nav data-testid="nav">
<Link to="/" data-testid="nav.home"> <Link to={home} data-testid="nav.home">
{translate("nav.home")} {translate("nav.home")}
</Link> </Link>
<Link to="/about" data-testid="nav.about"> <Link to={about} data-testid="nav.about">
{translate("nav.about")} {translate("nav.about")}
</Link> </Link>
<Link to="/counter" data-testid="nav.counter"> <Link to={counter} data-testid="nav.counter">
{translate("nav.counter")} {translate("nav.counter")}
</Link> </Link>
<Link to={tenders} data-testid="nav.tenders">
{translate("nav.tenders")}
</Link>
<Link to={trucks} data-testid="nav.trucks">
{translate("nav.trucks")}
</Link>
<button onClick={() => i18n.changeLanguage("en")}>en</button> <button onClick={() => i18n.changeLanguage("en")}>en</button>
<button onClick={() => i18n.changeLanguage("nl")}>nl</button> <button onClick={() => i18n.changeLanguage("nl")}>nl</button>
<hr /> <hr />
</nav> </nav>
</> </div>
); );
}; };
const StyledNavBar = styled(Navbar)`
nav a {
padding-right: 10px;
}
`;
export { StyledNavBar as Navbar };

View File

@@ -0,0 +1,12 @@
import { FunctionComponent } from "react";
import styled from "styled-components";
type Props = {};
const TestComponent: FunctionComponent<Props> = () => {
return <h1>TestComponent</h1>;
};
const StyledTestComponent = styled(TestComponent)``;
export { StyledTestComponent as TestComponent };

View File

@@ -0,0 +1,14 @@
import { OidcConfiguration } from "@axa-fr/react-oidc/dist/vanilla/oidc";
import { Config } from "../config";
const { clientId, redirectUri, silentRedirectUri, scope, url } = Config.oidc;
/* eslint-disable camelcase */
export const SSOConfiguration: OidcConfiguration = {
client_id: clientId,
redirect_uri: redirectUri,
silent_redirect_uri: silentRedirectUri,
scope,
authority: url,
service_worker_only: false,
};

View File

@@ -0,0 +1,28 @@
import { OidcProvider as ReactOidcProvider } from "@axa-fr/react-oidc";
import { FunctionComponent, ReactNode } from "react";
import { AppLoader } from "../loader/appLoader";
import { SSOConfiguration } from "./Configuration";
import { Authenticating } from "./overrides/Authenticating";
import { AuthenticatingError } from "./overrides/AuthenticatingError";
import { CallBackSuccess } from "./overrides/CallBackSuccess";
import { ServiceWorkerNotSupported } from "./overrides/ServiceWorkerNotSupported";
import { SessionLost } from "./overrides/SessionLost";
type Props = { children?: ReactNode; [x: string]: any };
export const OidcProvider: FunctionComponent<Props> = ({ children, ...rest }) => {
return (
<ReactOidcProvider
loadingComponent={AppLoader}
authenticatingErrorComponent={AuthenticatingError}
authenticatingComponent={Authenticating}
sessionLostComponent={SessionLost}
serviceWorkerNotSupportedComponent={ServiceWorkerNotSupported}
callbackSuccessComponent={CallBackSuccess}
configuration={SSOConfiguration}
{...rest}
>
{children}
</ReactOidcProvider>
);
};

View File

@@ -0,0 +1,31 @@
export interface OIDCConfig {
/**
* Authority URL
*/
url: string;
/**
* Realm to authenticate against
*/
realm: string;
/**
* Id of this client
*/
clientId: string;
/**
* Scope(s) that you want to request
*/
scope: string;
/**
* URI to redirect to after successful login
*/
redirectUri: string;
/**
* redirect uri for the silent refresh
*/
silentRedirectUri: string;
}

View File

@@ -0,0 +1,3 @@
export interface SSOResult {
configurationName: string;
}

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const Authenticating: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Authentication in progress for {configurationName}</h1>
<p>You will be redirected to the login page.</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const AuthenticatingError: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Error for {configurationName}</h1>
<p>An error occurred during authentication.</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const CallBackSuccess: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Authentication complete for {configurationName}</h1>
<p>You will be redirected...</p>
</>
);

View File

@@ -0,0 +1,9 @@
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const ServiceWorkerNotSupported: FunctionComponent<SSOResult> = ({ configurationName }) => (
<>
<h1>Unable to authenticate on this browser for {configurationName}</h1>
<p>Your browser is not configured to support Service Workers.</p>
</>
);

View File

@@ -0,0 +1,17 @@
import { useOidc } from "@axa-fr/react-oidc";
import { FunctionComponent } from "react";
import { SSOResult } from "../models/SSOResult";
export const SessionLost: FunctionComponent<SSOResult> = ({ configurationName }) => {
const { login } = useOidc(configurationName);
return (
<>
<h1>Session timed out for {configurationName}</h1>
<p>Your session has expired. Please re-authenticate.</p>
<button type="button" onClick={() => login("/")}>
Login
</button>
</>
);
};

28
src/setupProxy.js Normal file
View File

@@ -0,0 +1,28 @@
const { createProxyMiddleware } = require("http-proxy-middleware");
/**
* Create a simple proxy that automatically removes the prefix endpoint
* @param {*} app app to configure proxy on
* @param {*} endpoint endpoint you want to use for the proxy
* @param {*} target proxy target
*/
const createSimpleProxy = (app, endpoint, target) => {
app.use(
endpoint,
createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: {
[`^${endpoint}`]: "",
},
}),
);
};
/**
* Actual proxy configuration
* @param {*} app app to configure proxy on
*/
module.exports = function (app) {
createSimpleProxy(app, "/tendersguru", "https://tenders.guru");
};