Skip to content

Commit

Permalink
Fixed all issues and added eslint
Browse files Browse the repository at this point in the history
  • Loading branch information
danias committed Aug 1, 2023
1 parent c1a167a commit 1cced59
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 298 deletions.
89 changes: 54 additions & 35 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,4 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"airbnb",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:jsx-a11y/recommended"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"react/function-component-definition": "off",
"react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }],
"react/react-in-jsx-scope": "off",
"class-methods-use-this": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
],
"quotes": ["error", "single"],
"@typescript-eslint/quotes": ["error", "single"],
"no-underscore-dangle": "off"
},
"settings": {
"import/resolver": {
"node": {
Expand All @@ -37,8 +7,57 @@
}
},
"env": {
"browser": true,
"node": true,
"jest": true
}
}
"browser": true,
"es2021": true
},
"extends": [
"airbnb",
"airbnb-typescript",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"prettier"
],
"overrides": [
{
"extends": [],
"files": [
"*.ts",
"*.tsx"
]
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"react",
"jsx-a11y",
"react-hooks",
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"react/function-component-definition": "off",
"react/jsx-filename-extension": [1, { "extensions": [".tsx", ".ts"] }],
"react/react-in-jsx-scope": "off",
"class-methods-use-this": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
],
"quotes": ["error", "single"],
"@typescript-eslint/quotes": ["error", "single"],
"no-underscore-dangle": "off"
}
}
2 changes: 1 addition & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:19-alpine
FROM node:19-alpine
# Set the working directory to /app inside the container
WORKDIR /app
# Copy app files
Expand Down
28 changes: 28 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ This project was bootstrapped with [Create React App](https://github.com/faceboo

You should not need to run it directly from this folder as it is part of the docker build file but if you want to run it locally for development you can follow the instructions below at "Available Scripts".

## Web App Design

The web app is using a flavour of the MVVM (Model-View-View Model) pattern. You should be aware of the following ideas:

### Components

Here we have simple React code combined with imports of CSS files for the formatting. These components do not have state and you could inject anything you like through the props to test them.

### Controllers

A controller wraps a Component and maps the functions and values coming from a View Model (see below). You might have the occasional useState for maybe an [open, setOpen] value but nothing more.

### State

For state Recoil is being used but the state interacts with the ViewModel and should not be directly accessed or mutated from the Controllers with the exception of the App.tsx file that sets the setters and getters in the View Models.

### View Models

A View Model is a class that has a clearly defined interface that provides controllers with all the necessary functions and values to feed to their Components. We are utilizing the concept of CQRS meaning that we have separate functions that cause side-effects (aka mutations) and separate values that work like queries.

### Repositories

Repositories are used to interact with the app state (e.g. localStorage) and the Services (see below). For example, a service expects some authentication metadata (JWT) with the requests and instead of complicating the ViewModel with these details, a Repository provides a cleaner interface to the ViewModel for using the Services by taking care of the JWT injection etc. You can also use Repositories to deal with local caching etc.

### Services

Services are the gRPC endpoints that are automatically generated by the protobuf file and you could also add other services to wrap 3rd party API calls (e.g. Google Maps API).

## Technologies Used

To communicate with the backend, grpc-web is being used for the main functionality while a couple REST requests are used for registration and login.
Expand Down
62 changes: 34 additions & 28 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
{
"name": "todo-frontend",
"version": "0.1.1",
"version": "0.2.0",
"private": true,
"dependencies": {
"@chakra-ui/icons": "^2.1.0",
"@chakra-ui/react": "^2.8.0",
"@chakra-ui/system": "^2.6.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@grpc/grpc-js": "^1.8.21",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"axios": "^1.3.5",
"eslint": "^8.45.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"@grpc/grpc-js": "^1.8.21",
"eventemitter3": "^5.0.1",
"framer-motion": "^10.15.0",
"google-protobuf": "^3.21.2",
"grpc-web": "^1.4.2",
"jwt-decode": "^3.1.2",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
Expand All @@ -37,11 +26,12 @@
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"proto": "protoc -I=. src/bitloops/proto/todo.proto --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=typescript,mode=grpcwebtext:.",
"newproto": "protoc -I=. src/bitloops/proto/todo.proto --ts_out=. --ts_opt=target=web,json_names,unary_rpc_promise=true,no_namespace --grpc-web_out=import_style=typescript,mode=grpcwebtext:.",
"proto": "protoc -I=. src/bitloops/proto/todo.proto --ts_out=. --ts_opt=target=web,json_names,unary_rpc_promise=true,no_namespace --grpc-web_out=import_style=typescript,mode=grpcwebtext:.",
"docker:build": "docker build -t todo-frontend .",
"docker:run": "docker run -dp 3000:3000 --name todo-frontend todo-frontend",
"docker": "docker build -t todo-frontend . && docker run -dp 3000:3000 --name todo-frontend todo-frontend"
"docker": "docker build -t todo-frontend . && docker run -dp 3000:3000 --name todo-frontend todo-frontend",
"postinstall": "patch-package",
"lint": "eslint ."
},
"eslintConfig": {
"extends": [
Expand All @@ -62,21 +52,37 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",

"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/google-protobuf": "^3.15.6",
"@types/jest": "^29.5.0",
"@types/node": "^20.4.5",
"@types/react": "^18.0.34",
"@types/react-dom": "^18.0.11",
"eslint-config-prettier": "^8.8.0",
"@typescript-eslint/eslint-plugin": ">=6.0.0",
"@typescript-eslint/parser": ">=6.0.0",
"eslint": ">=8.0.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.9.0",
"eslint-config-xo": "^0.43.1",
"eslint-config-xo-typescript": "^1.0.1",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.0.0",
"protoc-gen-grpc-web": "^1.4.1",

"react-scripts": "5.0.1",
"typescript": "^5.0.4",
"web-vitals": "^3.3.1"
"typescript": ">=4.7",
"web-vitals": "^2.1.0",

"@types/google-protobuf": "^3.15.6",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"protoc-gen-grpc-web": "^1.4.2"
}
}
15 changes: 15 additions & 0 deletions frontend/patches/grpc-web+1.4.2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/node_modules/grpc-web/index.d.ts b/node_modules/grpc-web/index.d.ts
index 09fb671..9b6b233 100644
--- a/node_modules/grpc-web/index.d.ts
+++ b/node_modules/grpc-web/index.d.ts
@@ -71,8 +71,8 @@ declare module "grpc-web" {
export class MethodDescriptor<REQ, RESP> {
constructor(name: string,
methodType: string,
- requestType: new (...args: unknown[]) => REQ,
- responseType: new (...args: unknown[]) => RESP,
+ requestType: new (...args: any) => REQ,
+ responseType: new (...args: any) => RESP,
requestSerializeFn: any,
responseDeserializeFn: any);
getName(): string;
2 changes: 1 addition & 1 deletion frontend/src/bitloops/proto/TodoServiceClientPb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import * as grpcWeb from 'grpc-web';

import * as src_bitloops_proto_todo_pb from './todo_pb.d';
import * as src_bitloops_proto_todo_pb from '../../../src/bitloops/proto/todo_pb';


export class TodoServiceClient {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Todo/Entry/TodoEntryController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ interface TodoEntityProps {
function TodoEntryController(props: TodoEntityProps): JSX.Element {
const { id } = props;
const { setTodo, useTodoSelectors } = useTodoViewModel();
const { todo } = useTodoSelectors();
const { useTodo } = useTodoSelectors();
const [editable, setEditable] = React.useState<string | null>(null);
const todoViewModel = useTodoViewModel();
const oldTodo = todo(id);
const oldTodo = useTodo(id);

const updateLocalItem = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('updateLocalItem', e);
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Todo/Panel/TodoPanelController.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';

import TodoPanelComponent from './TodoPanelComponent';
import { useTodoViewModel } from '../../../view-models/TodoViewModel';
Expand All @@ -9,6 +9,10 @@ function TodoPanelController(): JSX.Element {
const { useTodoSelectors } = useTodoViewModel();
const { todoIds } = useTodoSelectors();

useEffect(() => {
todoViewModel.fetchAllTodo();
}, []);

const addItem = () => {
if (newTodoTitle) todoViewModel.addTodo(newTodoTitle);
setNewTodoTitle('');
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/context/DI.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { ReactNode, createContext, useContext } from 'react';
import IamService from '../infra/services/IamService';
import { IamRepository, IIamRepository } from '../infra/repositories/iam';
import { ITodoRepository, TodoRepository } from '../infra/repositories/todo';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { TodoServiceClient } from '../bitloops/proto/todo';
import { PROXY_URL } from '../config';
import { IIamRepository } from '../infra/interfaces/IIamRepository';
import { ITodoRepository } from '../infra/interfaces/ITodoRepository';
import IamRepository from '../infra/repositories/iam';
import TodoRepository from '../infra/repositories/todo';

export interface AppContext {
iamRepository: IIamRepository;
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/infra/interfaces/IIamRepository.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { User } from '../../models/User';

export type IamResponse = {
status: 'success' | 'cached';
user: User;
};

export interface IIamRepository {
loginWithEmailPassword(email: string, password: string): Promise<User>;
registerWithEmailPassword(email: string, password: string): Promise<void>;
isAuthenticated(): boolean;
logout(): void;
getUser(): User | null;
setUser(user: User): void;
}
33 changes: 33 additions & 0 deletions frontend/src/infra/interfaces/ITodoRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Todo from '../../models/Todo';

type GetAllTodoSuccessResponse = {
status: 'success';
todos: Todo[];
error: undefined;
};

type GetAllTodoCachedResponse = {
status: 'cached';
todos: Todo[];
error: undefined;
};

type GetAllTodoErrorResponse = {
status: 'error';
error: string;
todos: undefined;
};

export type GetAllTodoResponse =
| GetAllTodoSuccessResponse
| GetAllTodoCachedResponse
| GetAllTodoErrorResponse;

export interface ITodoRepository {
getAllTodo(callback: (asyncResponse: GetAllTodoResponse) => void): GetAllTodoResponse;
modifyTodoTitle(id: string, title: string): Promise<void>;
completeTodo(id: string): Promise<void>;
uncompleteTodo(id: string): Promise<void>;
deleteTodo(id: string): Promise<void>;
addTodo(title: string): Promise<void>;
}
3 changes: 1 addition & 2 deletions frontend/src/infra/repositories/iam/IamRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import JwtDecode from 'jwt-decode';
import { IIamRepository } from '.';
import { IIamRepository } from '../../interfaces/IIamRepository';
import { User } from '../../../models/User';
import { IIamService } from '../../interfaces/IIamService';
import { EventBus, Events } from '../../../Events';
Expand Down Expand Up @@ -29,7 +29,6 @@ const isExpired = (token: string): boolean => {
};

class IamRepository implements IIamRepository {
// eslint-disable-next-line no-useless-constructor, no-empty-function
constructor(private iamService: IIamService) {
EventBus.subscribe(Events.AUTH_CHANGED, this.setUser);
}
Expand Down
19 changes: 2 additions & 17 deletions frontend/src/infra/repositories/iam/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
import { User } from '../../../models/User';
import { IamRepository } from './IamRepository';

export type IamResponse = {
status: 'success' | 'cached';
user: User;
};

interface IIamRepository {
loginWithEmailPassword(email: string, password: string): Promise<User>;
registerWithEmailPassword(email: string, password: string): Promise<void>;
isAuthenticated(): boolean;
logout(): void;
getUser(): User | null;
setUser(user: User): void;
}

export { IamRepository } from './IamRepository';
export type { IIamRepository };
export default IamRepository;
Loading

0 comments on commit 1cced59

Please sign in to comment.