Merge pull request #4 from Fisker-Inc/development

Initial admin site
This commit is contained in:
John Wu
2021-01-08 11:15:58 -08:00
committed by GitHub
60 changed files with 2813 additions and 201 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
README.md
.DS_Store

2
.env.template Normal file
View File

@@ -0,0 +1,2 @@
REACT_APP_AUTH_SERVICE_URL = https://dev-auth.fiskerdps.com
REACT_APP_UPLOAD_SERVICE_URL = http://localhost:8080/api/upload

View File

@@ -1 +0,0 @@
[{"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/index.js":"1","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/reportWebVitals.js":"2","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/ErrorBoundary.jsx":"3","/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/App/index.js":"4"},{"size":610,"mtime":1609866784202,"results":"5","hashOfConfig":"6"},{"size":362,"mtime":1609865788231,"results":"7","hashOfConfig":"6"},{"size":682,"mtime":1609866806365,"results":"8","hashOfConfig":"6"},{"size":406,"mtime":1609866869723,"results":"9","hashOfConfig":"6"},{"filePath":"10","messages":"11","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1qss98p",{"filePath":"12","messages":"13","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"16","messages":"17","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/index.js",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/reportWebVitals.js",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/ErrorBoundary.jsx",[],"/Users/jwufiskerinc.com/Documents/GitHub/file-upload-webapp/src/components/App/index.js",[]]

24
.github/workflows/test.workflow.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Node.js CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build --if-present
- run: npm test
env:
CI: true

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:12-alpine as builder
COPY package*.json ./
RUN npm install
COPY . .
COPY .env.template .env
RUN npm run build
FROM nginx:alpine
COPY --from=builder build /usr/share/nginx/html

81
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,81 @@
@Library('fisker') _
pipeline {
agent none
options {
ansiColor('xterm')
}
environment {
PROJECT = getProject()
ENV = getEnv()
}
stages {
stage('Build') {
when {
beforeAgent true
allOf {
not {
changeRequest()
}
anyOf {
branch 'development'
branch 'main'
}
}
}
agent {
kubernetes {
cloud 'dev'
inheritFrom 'fisker'
}
}
steps {
slack("Build Started - ${env.JOB_NAME} (${env.BUILD_URL})", 'info', '#team-eng-compute-jenkins')
slack(getChanges(), 'info', '#team-eng-compute-jenkins')
container('awscli') {
ecr()
}
container('kaniko') {
buildImage()
}
}
post {
failure {
slack("${env.JOB_NAME} build failed!", 'error', '#team-eng-compute-jenkins')
}
}
}
stage('Deploy') {
when {
beforeAgent true
allOf {
not {
changeRequest()
}
anyOf {
branch 'development'
branch 'main'
}
}
}
agent {
kubernetes {
cloud getEnv()
inheritFrom 'fisker'
}
}
steps {
slack("Deploying ${PROJECT} to ${ENV}... :partydeploy: ", 'info', '#team-eng-compute-jenkins')
container('helm') {
deploy(getEnv())
}
slack("Successfully deployed ${PROJECT} to ${ENV}! :tada: ", 'info', '#team-eng-compute-jenkins')
}
post {
failure {
slack("${PROJECT} deploy to ${ENV} failed!", 'error', '#team-eng-compute-jenkins')
}
}
}
}
}

View File

@@ -1,6 +1,14 @@
# Getting Started with Create React App
# Fisker OTA Admin Portal
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
Front-end web application for administarting OTA services
# Setup
1. Install Node 12
2. Run `npm install`
3. Setup environment variables listed in .env.template
4. Or copy .env.template to .env
5. Edit .env with the service urls for authentication and api services
## Available Scripts
@@ -39,24 +47,6 @@ Instead, it will copy all the configuration files and the transitive dependencie
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)

2
k8s/Chart.yaml Normal file
View File

@@ -0,0 +1,2 @@
name: ota-admin-portal
version: 1.0.0

View File

@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
app: {{ .Chart.Name }}
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.registry }}/{{ .Values.image.name }}:{{ .Values.image.tag}}"
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
env:
# non-secret env vars
{{- range $name, $value := $.Values.env }}
{{- if not (empty $value) }}
- name: {{ $name | quote }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
# Params for env vars populated from k8s secrets
{{- range $.Values.secrets }}
- name: {{ . }}
valueFrom:
secretKeyRef:
name: {{ $.Chart.Name }}
key: {{ . }}
{{- end }}

View File

@@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
labels:
app: {{ .Chart.Name }}
name: {{ .Chart.Name }}
spec:
rules:
- host: {{ .Values.ingress.hostname }}
http:
paths:
- backend:
serviceName: {{ .Chart.Name }}
servicePort: 80
path: /
tls:
- hosts:
- {{ .Values.ingress.hostname }}
secretName: fiskerdps-cert

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
spec:
selector:
app: {{ .Chart.Name }}
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP

12
k8s/values-dev.yaml Normal file
View File

@@ -0,0 +1,12 @@
ingress:
hostname: dev-ota-admin.fiskerdps.com
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
replicas: 1

12
k8s/values-prd.yaml Normal file
View File

@@ -0,0 +1,12 @@
ingress:
hostname: ota-admin.fiskerdps.com
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
replicas: 1

13
nginx.conf Normal file
View File

@@ -0,0 +1,13 @@
events { worker_connections 1024; }
http {
server {
listen 80;
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location / {
try_files $uri /index.html;
}
}
}

323
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1764,6 +1764,14 @@
"react-transition-group": "^4.4.0"
}
},
"@material-ui/icons": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz",
"integrity": "sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ==",
"requires": {
"@babel/runtime": "^7.4.4"
}
},
"@material-ui/styles": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz",
@@ -3067,6 +3075,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
},
"autoprefixer": {
"version": "9.8.6",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz",
@@ -3096,6 +3109,14 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz",
"integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"requires": {
"follow-redirects": "^1.10.0"
}
},
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -6501,6 +6522,21 @@
}
}
},
"file-selector": {
"version": "0.1.19",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.19.tgz",
"integrity": "sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==",
"requires": {
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -7140,6 +7176,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -8078,81 +8127,6 @@
"istanbul-lib-report": "^3.0.0"
}
},
"jest": {
"version": "26.6.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz",
"integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==",
"requires": {
"@jest/core": "^26.6.0",
"import-local": "^3.0.2",
"jest-cli": "^26.6.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest-cli": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz",
"integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==",
"requires": {
"@jest/core": "^26.6.3",
"@jest/test-result": "^26.6.2",
"@jest/types": "^26.6.2",
"chalk": "^4.0.0",
"exit": "^0.1.2",
"graceful-fs": "^4.2.4",
"import-local": "^3.0.2",
"is-ci": "^2.0.0",
"jest-config": "^26.6.3",
"jest-util": "^26.6.2",
"jest-validate": "^26.6.2",
"prompts": "^2.0.1",
"yargs": "^15.4.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"jest-changed-files": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
@@ -9987,6 +9961,16 @@
"object-visit": "^1.0.0"
}
},
"material-ui-dropzone": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/material-ui-dropzone/-/material-ui-dropzone-3.5.0.tgz",
"integrity": "sha512-3BC6mz/4OEM4ZpbqMfuMN065JQyqfEbifT6/VzIua7Zj4b0DaR5YPCgpN+fL/e8yBgTs9MGBZJQY06p5pfKwvw==",
"requires": {
"@babel/runtime": "^7.4.4",
"clsx": "^1.0.2",
"react-dropzone": "^10.2.1"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -10118,6 +10102,15 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"mini-css-extract-plugin": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz",
@@ -12541,6 +12534,16 @@
"scheduler": "^0.20.1"
}
},
"react-dropzone": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz",
"integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==",
"requires": {
"attr-accept": "^2.0.0",
"file-selector": "^0.1.12",
"prop-types": "^15.7.2"
}
},
"react-error-overlay": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz",
@@ -12556,6 +12559,52 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.1.tgz",
@@ -12620,6 +12669,113 @@
"webpack-dev-server": "3.11.0",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "5.1.4"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest": {
"version": "26.6.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz",
"integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==",
"requires": {
"@jest/core": "^26.6.0",
"import-local": "^3.0.2",
"jest-cli": "^26.6.0"
},
"dependencies": {
"jest-cli": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz",
"integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==",
"requires": {
"@jest/core": "^26.6.3",
"@jest/test-result": "^26.6.2",
"@jest/types": "^26.6.2",
"chalk": "^4.0.0",
"exit": "^0.1.2",
"graceful-fs": "^4.2.4",
"import-local": "^3.0.2",
"is-ci": "^2.0.0",
"jest-config": "^26.6.3",
"jest-util": "^26.6.2",
"jest-validate": "^26.6.2",
"prompts": "^2.0.1",
"yargs": "^15.4.1"
}
}
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"react-shallow-renderer": {
"version": "16.14.1",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz",
"integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==",
"dev": true,
"requires": {
"object-assign": "^4.1.1",
"react-is": "^16.12.0 || ^17.0.0"
}
},
"react-test-renderer": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz",
"integrity": "sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==",
"dev": true,
"requires": {
"object-assign": "^4.1.1",
"react-is": "^17.0.1",
"react-shallow-renderer": "^16.13.1",
"scheduler": "^0.20.1"
},
"dependencies": {
"react-is": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",
"integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==",
"dev": true
}
}
},
"react-transition-group": {
@@ -13023,6 +13179,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@@ -14702,6 +14863,11 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -15177,6 +15343,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -1,14 +1,18 @@
{
"name": "client",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"dependencies": {
"@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.8",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0",
"axios": "^0.21.1",
"material-ui-dropzone": "^3.5.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
},
@@ -16,6 +20,7 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache",
"eject": "react-scripts eject"
},
"eslintConfig": {
@@ -35,5 +40,11 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": "12.20.1"
},
"devDependencies": {
"react-test-renderer": "^17.0.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -10,34 +10,13 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<title>File Upload App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
public/logo-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "OTA Admin Portal",
"name": "Fisker OTA Admin Portal",
"icons": [
{
"src": "favicon.ico",
@@ -8,12 +8,12 @@
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "logo-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "logo-512.png",
"type": "image/png",
"sizes": "512x512"
}

View File

@@ -0,0 +1,17 @@
import { Typography } from "@material-ui/core";
import React from "react";
import useStyles from '../Styles';
const PageNotFound = () => {
const classes = useStyles();
return (
<div className={classes.paper}>
<Typography component="h1" variant="h2">
Page Not Found
</Typography>
</div>
);
}
export default PageNotFound;

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,8 +1,79 @@
import { render, screen } from '@testing-library/react';
import App from '.';
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/FileUploadContext");
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
import { render, screen, cleanup, waitForElementToBeRemoved, waitFor } from "@testing-library/react"
import { setToken } from "../Contexts/UserContext";
import App from ".";
const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }};
const LOADING_STATUS = "Loading...";
const renderRoute = async (route) => {
window.history.pushState({}, "", route);
const { container } = render(<App />);
if (screen.queryByText(LOADING_STATUS)) {
await waitForElementToBeRemoved(() => screen.getByText(LOADING_STATUS));
}
return container;
};
describe("App", () => {
afterEach(() => {
setToken(null);
cleanup();
});
it("Route / unauthenticated", async () => {
const container = await renderRoute("/");
expect(container.querySelector("h1").innerHTML).toEqual("Sign in");
expect(container).toMatchSnapshot();
});
it("Route /signup unauthenticated", async () => {
const container = await renderRoute("/signup");
expect(container.querySelector("h1").innerHTML).toEqual("Sign up");
expect(container).toMatchSnapshot();
});
it("Route /home unauthenticated", async () => {
const container = await renderRoute("/home");
expect(container.querySelector("h1").innerHTML).toEqual("Sign in");
expect(container).toMatchSnapshot();
});
it("Route / authenticated", async () => {
setToken(TEST_TOKEN);
const container = await renderRoute("/");
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
expect(container).toMatchSnapshot();
});
it("Route /signup authenticated", async () => {
setToken(TEST_TOKEN);
const container = await renderRoute("/signup");
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
expect(container).toMatchSnapshot();
});
it("Route /home authenticated", async () => {
setToken(TEST_TOKEN);
const container = await renderRoute("/home");
expect(container.querySelector("h1").innerHTML).toEqual("Upload file");
expect(container).toMatchSnapshot();
});
it("Route /page-not-found unauthenticated", async () => {
const container = await renderRoute("/page-not-found");
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
expect(container).toMatchSnapshot();
});
it("Route /page-not-found authenticated", async () => {
setToken(TEST_TOKEN);
const container = await renderRoute("/page-not-found");
expect(container.querySelector("h1").innerHTML).toEqual("Page Not Found");
expect(container).toMatchSnapshot();
});
})

View File

@@ -0,0 +1,780 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App Route / authenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-25"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Upload file
</h1>
<div
data-testid="mocked-fileuploadprovider"
>
<form
class="makeStyles-form-27"
novalidate=""
>
<div
class="MuiDropzoneArea-root"
tabindex="0"
>
<input
accept=""
autocomplete="off"
multiple=""
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="MuiDropzoneArea-textContainer"
>
<p
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
>
Drag and drop a file here or click
</p>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiDropzoneArea-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
/>
</svg>
</div>
</div>
</form>
</div>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Sign Out
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</main>
</div>
</div>
`;
exports[`App Route / unauthenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-1"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Sign in
</h1>
<form
action="{onSubmit}"
class="makeStyles-form-3"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
data-shrink="true"
for="email"
id="email-label"
>
Email Address
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="email"
class="MuiInputBase-input MuiOutlinedInput-input"
id="email"
name="email"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7 PrivateNotchedOutline-legendNotched-8"
>
<span>
Email Address
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="password"
id="password-label"
>
Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="current-password"
class="MuiInputBase-input MuiOutlinedInput-input"
id="password"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7"
>
<span>
Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Sign In
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
href="/signup"
>
Don't have an account? Sign Up
</a>
</div>
</div>
</form>
</div>
</main>
</div>
</div>
`;
exports[`App Route /home authenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-33"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Upload file
</h1>
<div
data-testid="mocked-fileuploadprovider"
>
<form
class="makeStyles-form-35"
novalidate=""
>
<div
class="MuiDropzoneArea-root"
tabindex="0"
>
<input
accept=""
autocomplete="off"
multiple=""
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="MuiDropzoneArea-textContainer"
>
<p
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
>
Drag and drop a file here or click
</p>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiDropzoneArea-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
/>
</svg>
</div>
</div>
</form>
</div>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Sign Out
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</main>
</div>
</div>
`;
exports[`App Route /home unauthenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-17"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Sign in
</h1>
<form
action="{onSubmit}"
class="makeStyles-form-19"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
data-shrink="true"
for="email"
id="email-label"
>
Email Address
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="email"
class="MuiInputBase-input MuiOutlinedInput-input"
id="email"
name="email"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23 PrivateNotchedOutline-legendNotched-24"
>
<span>
Email Address
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="password"
id="password-label"
>
Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="current-password"
class="MuiInputBase-input MuiOutlinedInput-input"
id="password"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-21 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-23"
>
<span>
Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-20 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Sign In
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
href="/signup"
>
Don't have an account? Sign Up
</a>
</div>
</div>
</form>
</div>
</main>
</div>
</div>
`;
exports[`App Route /page-not-found authenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-41"
>
<h1
class="MuiTypography-root MuiTypography-h2"
>
Page Not Found
</h1>
</div>
</div>
</div>
`;
exports[`App Route /page-not-found unauthenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-37"
>
<h1
class="MuiTypography-root MuiTypography-h2"
>
Page Not Found
</h1>
</div>
</div>
</div>
`;
exports[`App Route /signup authenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-29"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Upload file
</h1>
<div
data-testid="mocked-fileuploadprovider"
>
<form
class="makeStyles-form-31"
novalidate=""
>
<div
class="MuiDropzoneArea-root"
tabindex="0"
>
<input
accept=""
autocomplete="off"
multiple=""
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="MuiDropzoneArea-textContainer"
>
<p
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
>
Drag and drop a file here or click
</p>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiDropzoneArea-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
/>
</svg>
</div>
</div>
</form>
</div>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Sign Out
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</main>
</div>
</div>
`;
exports[`App Route /signup unauthenticated 1`] = `
<div>
<div
data-testid="mocked-userprovider"
>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-9"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Sign up
</h1>
<form
class="makeStyles-form-11"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
data-shrink="true"
for="email"
id="email-label"
>
Email Address
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="email"
class="MuiInputBase-input MuiOutlinedInput-input"
id="email"
name="email"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-15 PrivateNotchedOutline-legendNotched-16"
>
<span>
Email Address
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="password"
id="password-label"
>
Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="new-password"
class="MuiInputBase-input MuiOutlinedInput-input"
id="password"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-15"
>
<span>
Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="passwordConfirm"
id="passwordConfirm-label"
>
Confirm Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="passwordConfirm"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-15"
>
<span>
Confirm Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-12 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Sign Up
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
href="/"
>
Already have an account? Sign In
</a>
</div>
</div>
</form>
</div>
</main>
</div>
</div>
`;

View File

@@ -1,23 +0,0 @@
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<p>
File upload demo
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { UserProvider } from '../Contexts/UserContext';
import SiteRoutes from '../Routes/SiteRoutes';
function App() {
return (
<UserProvider>
<SiteRoutes />
</UserProvider>
);
}
export default App;

View File

@@ -0,0 +1,62 @@
import React, { useContext, useState } from "react";
import { uploadFile, getCancelToken } from "../../services/uploadFile";
const FileUploadContext = React.createContext();
export const FileUploadProvider = ({ children }) => {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState(null);
const [cancelUpload, setCancelUpload] = useState(null);
const done = () => {
setCancelUpload(null);
setUploading(false);
setProgress(0);
};
const cancel = async () => {
if (cancelUpload && progress < 100) {
cancelUpload.cancel();
setStatus("Upload cancelled");
}
done();
};
const upload = async (files) => {
try {
if (!files || files.length === 0) throw new Error("No file provided");
const file = files[0].file;
const filename = file.name;
setUploading(true);
setProgress(0);
setStatus(`Uploading ${filename}`);
setCancelUpload(getCancelToken());
const result = await uploadFile(file, setProgress, cancelUpload);
const url = ((result && result.url) ? result.url : "No URL available");
setStatus(`Uploaded ${filename}\n${url}`);
setCancelUpload(null);
setProgress(100);
}
catch (e) {
setStatus(`Error occured: ${e.message}`);
}
};
return (
<FileUploadContext.Provider value={{
uploading,
progress,
status,
upload,
cancel,
}}>
{children}
</FileUploadContext.Provider>
);
};
export const useFileUploadContext = () => useContext(FileUploadContext);

View File

@@ -0,0 +1,61 @@
jest.mock("../../services/uploadFile");
import {uploadFile, getCancelToken, setUploadFileResponse, setUploadFileDelay, getIssuedCancelToken } from "../../services/uploadFile"
import { FileUploadProvider, useFileUploadContext } from "../Contexts/FileUploadContext";
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
describe("FileUploadContext", () => {
beforeEach(() => {
const TestComp = () => {
const { progress, uploading, status, upload, cancel } = useFileUploadContext();
return (
<>
<div data-testid="uploading">{uploading.toString()}</div>
<div data-testid="progress">{progress.toString()}</div>
<div data-testid="status">{status}</div>
<button data-testid="uploadNoFile" onClick={() => upload()}/>
<button data-testid="upload" onClick={() => upload([{ file: { name: "test.jpg" }}])}/>
<button data-testid="cancel" onClick={() => cancel()}/>
</>
);
};
render(<FileUploadProvider><TestComp /></FileUploadProvider>);
});
afterEach(() => {
cleanup();
});
it("Initial state", async () => {
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
expect(screen.getByTestId("status").innerHTML).toEqual("");
})
it("Upload no file", async () => {
fireEvent.click(screen.getByTestId("uploadNoFile"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("progress").innerHTML).toEqual("0");
expect(screen.getByTestId("status").innerHTML).toEqual("Error occured: No file provided");
})
it("Upload file", async () => {
fireEvent.click(screen.getByTestId("upload"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("100"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
expect(screen.getByTestId("status").innerHTML).toEqual("Uploaded test.jpg\nCLOUDFRONT_URL");
})
it("Cancel upload", async () => {
setUploadFileDelay(true);
fireEvent.click(screen.getByTestId("upload"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("50"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("true");
expect(screen.getByTestId("status").innerHTML).toEqual("Uploading test.jpg");
fireEvent.click(screen.getByTestId("cancel"));
await waitFor(() => expect(screen.getByTestId("progress").innerHTML).toEqual("0"));
expect(screen.getByTestId("uploading").innerHTML).toEqual("false");
expect(screen.getByTestId("status").innerHTML).toEqual("Upload cancelled");
})
})

View File

@@ -0,0 +1,96 @@
import React, { useContext, useEffect, useState } from 'react';
import auth from '../../services/auth';
const UserContext = React.createContext();
export const UserProvider = ({ children }) => {
const [fetching, setFetching] = useState(false);
const [token, setToken] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (!localStorage) return;
const token = JSON.parse(localStorage.getItem("token"));
if (!token) return;
const { accessToken: { jwtToken }} = token;
const verifyToken = async (accessToken) => {
const result = await auth.verify(accessToken);
if (result.authenticated) {
setToken(token);
} else {
await signOut();
}
};
verifyToken(jwtToken);
return () => {};
}, []);
const signIn = async (username, password) => {
try {
if (!username) throw new Error('Email is required');
if (!password) throw new Error('Password is required');
setFetching(true);
setError(null);
const result = await auth.signIn(username, password);
if (result.message) throw new Error(result.message);
signedIn(result);
}
catch (error) {
setError(error.message);
}
finally {
setFetching(false);
}
};
const signUp = async (username, password, confirmPassword) => {
try {
if (!username) throw new Error('Email is required');
if (!password) throw new Error('Password is required');
if (password !== confirmPassword) throw new Error('Passwords do not match');
setFetching(true);
setError(null);
const result = await auth.signUp(username, password);
if (result.message) throw new Error(result.message);
}
catch (error) {
setError(error.message);
}
finally {
setFetching(false);
}
};
const signOut = async () => {
setToken(null);
if (!localStorage) return;
localStorage.removeItem("token");
};
const signedIn = (token) => {
setToken(token);
if (!localStorage || !token || !token.accessToken) return;
localStorage.setItem("token", JSON.stringify(token));
}
return (
<UserContext.Provider value={{
fetching,
token,
error,
setError,
signIn,
signUp,
signOut,
}}>
{children}
</UserContext.Provider>
);
};
export const useUserContext = () => useContext(UserContext);

View File

@@ -0,0 +1,169 @@
jest.mock("../../services/auth");
import {render, cleanup, screen, fireEvent, waitFor} from "@testing-library/react"
import { UserProvider, useUserContext } from "../Contexts/UserContext";
import auth from "../../services/auth";
const TEST_TOKEN = { accessToken: { jwtToken: "TEST" }};
describe("UseContext", () => {
describe("Signup", () => {
beforeEach(() => {
const TestComp = () => {
const { signUp, error, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<button data-testid="signUpNoEmail" onClick={() => signUp("")}/>
<button data-testid="signUpNoPassword" onClick={() => signUp("test@test.com", "")}/>
<button data-testid="signUpBadConfirm" onClick={() => signUp("test@test.com", "password", "")}/>
<button data-testid="signUp" onClick={() => signUp("test@test.com", "password", "password")}/>
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with no email address", () => {
fireEvent.click(screen.getByTestId("signUpNoEmail"));
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with no password", () => {
fireEvent.click(screen.getByTestId("signUpNoPassword"));
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("Error with non-matching password", () => {
fireEvent.click(screen.getByTestId("signUpBadConfirm"));
expect(screen.getByTestId("error").innerHTML).toEqual("Passwords do not match");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
});
it("No error sign up", async () => {
fireEvent.click(screen.getByTestId("signUp"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
});
it("Handle server error", async () => {
auth.setSignUpResponse({ message: "SERVER-ERROR", error: "ERR" });
fireEvent.click(screen.getByTestId("signUp"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
auth.setSignUpResponse({});
});
});
describe("Signin", () => {
beforeEach(() => {
const TestComp = () => {
const { signIn, error, token, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<div data-testid="token">{JSON.stringify(token)}</div>
<button data-testid="signInNoEmail" onClick={() => signIn("")}/>
<button data-testid="signInNoPassword" onClick={() => signIn("test@test.com", "")}/>
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
});
afterEach(() => {
cleanup();
});
it("Initial state", () => {
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
});
it("Error with no email address", () => {
fireEvent.click(screen.getByTestId("signInNoEmail"));
expect(screen.getByTestId("error").innerHTML).toEqual("Email is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
});
it("Error with no password", () => {
fireEvent.click(screen.getByTestId("signInNoPassword"));
expect(screen.getByTestId("error").innerHTML).toEqual("Password is required");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
});
it("No error sign in", async () => {
const TOKEN_STRING = JSON.stringify(TEST_TOKEN);
auth.setSignInResponse(TEST_TOKEN);
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("token").innerHTML).toEqual(TOKEN_STRING);
if (!localStorage) return;
expect(localStorage.getItem("token")).toEqual(TOKEN_STRING);
localStorage.removeItem("token");
});
it("Handle server error", async () => {
auth.setSignInResponse({ message: "SERVER-ERROR", error: "ERR" });
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
expect(screen.getByTestId("error").innerHTML).toEqual("SERVER-ERROR");
auth.setSignUpResponse({});
});
});
describe("Signout", () => {
beforeEach(async () => {
const TestComp = () => {
const { signIn, signOut, error, token, fetching } = useUserContext();
return (
<>
<div data-testid="error">{error}</div>
<div data-testid="fetching">{fetching.toString()}</div>
<div data-testid="token">{JSON.stringify(token)}</div>
<button data-testid="signIn" onClick={() => signIn("test@test.com", "password", "password")}/>
<button data-testid="signOut" onClick={() => signOut()}/>
</>
);
};
render(<UserProvider><TestComp /></UserProvider>);
auth.setSignInResponse(TEST_TOKEN);
fireEvent.click(screen.getByTestId("signIn"));
await waitFor(() => expect(screen.getByTestId("fetching").innerHTML).toEqual("false"));
});
afterEach(() => {
auth.setSignInResponse({});
cleanup();
});
it("Token cleared", () => {
fireEvent.click(screen.getByTestId("signOut"));
expect(screen.getByTestId("error").innerHTML).toEqual("");
expect(screen.getByTestId("fetching").innerHTML).toEqual("false");
expect(screen.getByTestId("token").innerHTML).toEqual("null");
if (!localStorage) return;
expect(localStorage.getItem('token')).toBeNull();
})
})
});

View File

@@ -0,0 +1,21 @@
import React from "react";
let uploading = false;
let progress = 0;
let status = null;
export const FileUploadProvider = ({ children }) => {
return (
<div data-testid="mocked-fileuploadprovider">
{children}
</div>
);
};
export const useFileUploadContext = () => ({
uploading,
progress,
status,
upload: jest.fn(),
cancel: jest.fn(),
});

View File

@@ -0,0 +1,35 @@
import React from 'react';
let token = null;
let fetching = false;
let error = null;
export const UserProvider = ({ children }) => {
return (
<div data-testid="mocked-userprovider">
{children}
</div>
);
};
export const useUserContext = () => ({
token,
fetching,
error,
setError: jest.fn(),
signIn: jest.fn(),
signUp: jest.fn(),
signOut: jest.fn(),
});
export const setToken = (val) => {
token = val;
};
export const setFetching = (val) => {
fetching = val;
};
export const setError = (val) => {
error = val;
};

View File

@@ -11,8 +11,6 @@ export default class ErrorBoundary extends Component {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// eslint-disable-next-line no-console
console.log({ error, errorInfo });
this.setState({ errorInfo });
}
render() {

View File

@@ -0,0 +1,15 @@
jest.mock("../Contexts/UserContext");
jest.mock("../Contexts/FileUploadContext");
import { BrowserRouter } from 'react-router-dom';
import { render, cleanup } from "@testing-library/react"
import FileUploadForm from './index';
describe("File Upload Form", () => {
it("Should render", () => {
const { container } = render(<BrowserRouter><FileUploadForm /></BrowserRouter>);
expect(container).toMatchSnapshot();
cleanup();
})
})

View File

@@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`File Upload Form Should render 1`] = `
<div>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-1"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Upload file
</h1>
<div
data-testid="mocked-fileuploadprovider"
>
<form
class="makeStyles-form-3"
novalidate=""
>
<div
class="MuiDropzoneArea-root"
tabindex="0"
>
<input
accept=""
autocomplete="off"
multiple=""
style="display: none;"
tabindex="-1"
type="file"
/>
<div
class="MuiDropzoneArea-textContainer"
>
<p
class="MuiTypography-root MuiDropzoneArea-text MuiTypography-h5"
>
Drag and drop a file here or click
</p>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiDropzoneArea-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"
/>
</svg>
</div>
</div>
</form>
</div>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-text"
tabindex="0"
type="button"
>
<span
class="MuiButton-label"
>
Sign Out
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</main>
</div>
`;

View File

@@ -0,0 +1,46 @@
import React from "react";
import { Button, Container, CssBaseline, Grid, Typography } from "@material-ui/core";
import { DropzoneAreaBase } from "material-ui-dropzone";
import { useUserContext } from "../Contexts/UserContext";
import { useFileUploadContext, FileUploadProvider } from "../Contexts/FileUploadContext";
import ModalProgressBar from "../ModalProgressBar";
import useStyles from "../Styles";
const FileUploadZone = ({ classes }) => {
const { uploading, progress, status, upload, cancel } = useFileUploadContext();
return (
<form className={classes.form} noValidate>
<DropzoneAreaBase
maxFileSize={5e+7}
showAlerts={false}
onAdd={upload}
/>
<ModalProgressBar uploading={uploading} progress={progress} onCancel={cancel} status={status} />
</form>
);
};
export default function FileUploadForm() {
const { signOut } = useUserContext();
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Upload file
</Typography>
<FileUploadProvider>
<FileUploadZone classes={classes} />
</FileUploadProvider>
<Grid container>
<Grid item >
<Button onClick={signOut}>Sign Out</Button>
</Grid>
</Grid>
</div>
</Container>
);
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { Snackbar } from "@material-ui/core";
import { useUserContext } from './Contexts/UserContext';
export const MessageBar = () => {
const { error, setError } = useUserContext();
const open = (error !== null);
return (<Snackbar
open={open}
message={error}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
autoHideDuration={10000}
onClose={() => setError(null)}/>)
}

View File

@@ -0,0 +1,44 @@
import React from "react";
import Modal from '@material-ui/core/Modal';
import { Button, LinearProgress } from "@material-ui/core";
const getModalStyle = () => {
const top = 30;
const left = 50;
return {
width: `350px`,
top: `${top}%`,
left: `${left}%`,
transform: `translate(-${left}%, -${top}%)`,
backgroundColor: `white`,
border: `none`,
position: `absolute`,
margin: `1em`,
padding: `1em`,
textAlign: `center`,
};
};
const ModalProgressBar = ({ onCancel, uploading, progress, status }) => {
const modalStyle = getModalStyle();
const onClickCancel = () => {
if (onCancel) onCancel();
}
return (
<Modal open={uploading}>
<div style={modalStyle}>
{status && <p>{status}</p>}
<LinearProgress variant="determinate" value={progress} />
<Button onClick={onClickCancel}>
{ progress < 100 ? "Cancel" : "Done" }
</Button>
</div>
</Modal>
);
}
export default ModalProgressBar;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
export const TYPES = {
PUBLIC: 0,
GUEST: 1,
PROTECTED: 2,
};
export const AuthRoute = ({ token, type, ...others }) => {
if (!token && type === TYPES.PROTECTED) {
return <Redirect to="/" />;
}
else if (token && type === TYPES.GUEST) {
return <Redirect to="/home" />;
}
return <Route render {...others} />;
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { useUserContext } from '../Contexts/UserContext';
export const ProtectedRoute = ({ render, ...others }) => {
const context = useUserContext();
const { token, setError } = context;
if (!token) {
setError('Please sign in to access');
return <Redirect to="/" />;
}
return <Route render {...others} />;
}

View File

@@ -0,0 +1,34 @@
import React, { Suspense } from 'react';
import {
BrowserRouter,
Switch,
} from 'react-router-dom';
import { AuthRoute, TYPES } from '../Routes/AuthRoute'
import { MessageBar } from '../MessageBar';
import { useUserContext } from '../Contexts/UserContext';
const SignInForm = React.lazy(() => import('../SignInForm'));
const SignUpForm = React.lazy(() => import('../SignUpForm'));
const FileUploadForm = React.lazy(() => import('../FileUploadForm'));
const PageNotFound = React.lazy(() => import('../404'));
const SiteRoutes = () => {
const { token } = useUserContext();
return (
<Suspense fallback={"Loading..."}>
<MessageBar />
<BrowserRouter>
<Switch>
<AuthRoute path="/" exact render={() => <SignInForm />} type={TYPES.GUEST} token={token} />
<AuthRoute path="/signup" exact render={() => <SignUpForm />} type={TYPES.GUEST} token={token} />
<AuthRoute path="/home" render={() => <FileUploadForm />} type={TYPES.PROTECTED} token={token} />
<PageNotFound />
</Switch>
</BrowserRouter>
</Suspense>
);
};
export default SiteRoutes;

View File

@@ -0,0 +1,15 @@
jest.mock("../Contexts/UserContext");
import React from "react";
import { BrowserRouter } from 'react-router-dom';
import { render, cleanup } from "@testing-library/react"
import SignInForm from './index';
describe("Sign In Form", () => {
it("Should render", () => {
const { container } = render(<BrowserRouter><SignInForm /></BrowserRouter>);
expect(container).toMatchSnapshot();
cleanup();
})
})

View File

@@ -0,0 +1,145 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sign In Form Should render 1`] = `
<div>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-1"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Sign in
</h1>
<form
action="{onSubmit}"
class="makeStyles-form-3"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
data-shrink="true"
for="email"
id="email-label"
>
Email Address
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="email"
class="MuiInputBase-input MuiOutlinedInput-input"
id="email"
name="email"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7 PrivateNotchedOutline-legendNotched-8"
>
<span>
Email Address
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="password"
id="password-label"
>
Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="current-password"
class="MuiInputBase-input MuiOutlinedInput-input"
id="password"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7"
>
<span>
Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Sign In
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
href="/signup"
>
Don't have an account? Sign Up
</a>
</div>
</div>
</form>
</div>
</main>
</div>
`;

View File

@@ -0,0 +1,78 @@
import React, { useRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core';
import { useUserContext } from '../Contexts/UserContext';
import useStyles from '../Styles';
export default function SignInForm() {
const classes = useStyles();
const emailEl = useRef(null);
const passwordEl = useRef(null);
const { fetching, signIn, setError } = useUserContext();
const onSubmit = async (event) => {
try {
event.preventDefault();
const username = emailEl.current.value;
const password = passwordEl.current.value;
await signIn(username, password);
}
catch (e) {
setError(e.message);
}
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
inputRef={emailEl}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
inputRef={passwordEl}
/>
<Button
type="submit"
disabled={fetching}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{ fetching ? "Signing In..." : "Sign In" }
</Button>
<Grid container>
<Grid item>
<Link component={RouterLink} to="/signup" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}

View File

@@ -0,0 +1,14 @@
jest.mock("../Contexts/UserContext");
import { BrowserRouter } from 'react-router-dom';
import { render, cleanup } from "@testing-library/react"
import SignUpForm from './index';
describe("Sign Up Form", () => {
it("Should render", () => {
const { container } = render(<BrowserRouter><SignUpForm /></BrowserRouter>);
expect(container).toMatchSnapshot();
cleanup();
})
})

View File

@@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Sign Up Form Should render 1`] = `
<div>
<main
class="MuiContainer-root MuiContainer-maxWidthXs"
>
<div
class="makeStyles-paper-1"
>
<h1
class="MuiTypography-root MuiTypography-h5"
>
Sign up
</h1>
<form
class="makeStyles-form-3"
novalidate=""
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused Mui-required Mui-required"
data-shrink="true"
for="email"
id="email-label"
>
Email Address
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="email"
class="MuiInputBase-input MuiOutlinedInput-input"
id="email"
name="email"
required=""
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7 PrivateNotchedOutline-legendNotched-8"
>
<span>
Email Address
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="password"
id="password-label"
>
Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
autocomplete="new-password"
class="MuiInputBase-input MuiOutlinedInput-input"
id="password"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7"
>
<span>
Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-outlined Mui-required Mui-required"
data-shrink="false"
for="passwordConfirm"
id="passwordConfirm-label"
>
Confirm Password
<span
aria-hidden="true"
class="MuiFormLabel-asterisk MuiInputLabel-asterisk"
>
*
</span>
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="passwordConfirm"
name="password"
required=""
type="password"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-5 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-7"
>
<span>
Confirm Password
 *
</span>
</legend>
</fieldset>
</div>
</div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Sign Up
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-body2 MuiTypography-colorPrimary"
href="/"
>
Already have an account? Sign In
</a>
</div>
</div>
</form>
</div>
</main>
</div>
`;

View File

@@ -0,0 +1,91 @@
import React, { useRef } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Button, Container, CssBaseline, Grid, Link, TextField, Typography } from '@material-ui/core';
import useStyles from '../Styles';
import { useUserContext } from '../Contexts/UserContext';
export default function SignInForm() {
const { signUp, signIn, fetching, setError } = useUserContext();
const classes = useStyles();
const emailEl = useRef(null);
const passwordEl = useRef(null);
const confirmEl = useRef(null);
const onSubmit = async (event) => {
try {
event.preventDefault();
const email = emailEl.current.value;
const password = passwordEl.current.value;
const confirm = confirmEl.current.value;
await signUp(email, password, confirm);
await signIn(email, password);
}
catch (e) {
setError(e.message);
}
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<form className={classes.form} noValidate onSubmit={onSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
inputRef={emailEl}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
inputRef={passwordEl}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Confirm Password"
type="password"
id="passwordConfirm"
inputRef={confirmEl}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={fetching}
>
{ fetching ? "Signing Up..." : "Sign Up" }
</Button>
<Grid container>
<Grid item>
<Link component={RouterLink} to="/" variant="body2">
{"Already have an account? Sign In"}
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}

23
src/components/Styles.jsx Normal file
View File

@@ -0,0 +1,23 @@
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.primary.main,
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
export default useStyles;

View File

@@ -2,14 +2,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import ErrorBoundary from './components/ErrorBoundary';
// import ErrorBoundary from './components/ErrorBoundary';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,16 @@
let signInResponse = {};
let signUpResponse = {};
let verifyResponse = {};
const logResponse = (response) => {
return response;
};
export default {
signIn: async (username, password) => logResponse(signInResponse),
signUp: async (username, password) => logResponse(signUpResponse),
verify: async (accessToken) => logResponse(verifyResponse),
setSignInResponse: (value) => { signInResponse = value; },
setSignUpResponse: (value) => { signUpResponse = value; },
setVerifyResponse: (value) => { verifyResponse = value; },
}

View File

@@ -0,0 +1,29 @@
import delay from "../../utils/delay";
let uploadFileResponse = { url: "CLOUDFRONT_URL" };
let uploadFileDelay = false;
let issuedCancelToken = null;
export const getCancelToken = () => {
issuedCancelToken = {
cancel: jest.fn()
}
return issuedCancelToken;
}
export const uploadFile = async (file, onProgress, cancelToken) => {
if (!uploadFileDelay) return uploadFileResponse;
onProgress(50);
await delay(10000);
return {};
};
export const setUploadFileResponse = (value) => {
uploadFileResponse = value;
}
export const setUploadFileDelay = (value) => {
uploadFileDelay = value;
}
export const getIssuedCancelToken = () => issuedCancelToken;

35
src/services/auth.js Normal file
View File

@@ -0,0 +1,35 @@
const AUTH_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const auth = {
signIn: (username, password) => fetch(`${AUTH_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username,
password,
})
}).then((response) => response.json()),
signUp: (username, password) => fetch(`${AUTH_URL}/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username,
password,
})
}).then((response) => response.json()),
verify: (accessToken) => fetch(`${AUTH_URL}/auth/verify`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ token: accessToken })
}).then((response) => response.json()),
};
export default auth;

View File

@@ -0,0 +1,29 @@
import axios from 'axios';
const UPLOAD_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
export const getCancelToken = () => {
const token = axios.CancelToken;
return token.source();
}
export const uploadFile = (file, onProgress, cancelToken) => {
const form = new FormData();
let options = {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
cancelToken,
};
if (onProgress) {
options = {
...options,
onUploadProgress: (event) => {
onProgress(Math.min(99, Math.floor((event.loaded / event.total) * 100)));
}
}
}
form.append('file', file);
return axios.post(UPLOAD_ENDPOINT, form, options);
};

8
src/utils/delay.js Normal file
View File

@@ -0,0 +1,8 @@
const delay = (duration) => new Promise((resolve) => {
const timer = setTimeout(() => {
clearTimeout(timer);
resolve();
}, duration);
})
export default delay;