diff --git a/package.json b/package.json index 52129cb..5f3ffa2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "cheerio": "^1.0.0-rc.10", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "dotenv": "^10.0.0", + "dotenv": "^16.1.4", "express": "^4.17.2", "express-validator": "^6.14.0", "jsonwebtoken": "^9.0.0", diff --git a/src/client/.eslintrc.json b/src/client/.eslintrc.json index 7050097..408a21a 100644 --- a/src/client/.eslintrc.json +++ b/src/client/.eslintrc.json @@ -4,7 +4,11 @@ "es6": true, "node": true }, - "extends": ["eslint:recommended", "plugin:react/recommended"], + "plugins": [ + "@typescript-eslint" +], + "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended"], "parserOptions": { "ecmaVersion": 2020, "sourceType": "module" diff --git a/src/client/package.json b/src/client/package.json index 2273591..c443ce9 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -7,15 +7,24 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", + "@types/jest": "^29.5.2", + "@types/node": "^20.2.5", + "@types/react": "^18.2.8", + "@types/react-dom": "^18.2.4", "http-proxy-middleware": "^2.0.6", - "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^8.0.5", "react-router-dom": "^6.6.2", "react-scripts": "5.0.1", + "typescript": "^5.1.3", "web-vitals": "^2.1.4" }, + "overrides": { + "react-scripts": { + "typescript": ">3.2.1" + } + }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", @@ -39,5 +48,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.59.9", + "@typescript-eslint/parser": "^5.59.9", + "eslint": "^8.42.0" } } diff --git a/src/client/src/App.jsx b/src/client/src/App.tsx similarity index 81% rename from src/client/src/App.jsx rename to src/client/src/App.tsx index 9d78445..7833aba 100644 --- a/src/client/src/App.jsx +++ b/src/client/src/App.tsx @@ -4,11 +4,11 @@ import { AppRoutes, ProtectedAppRoutes } from './features/AppRoutes'; import Header from './components/Header'; import Footer from './components/Footer'; import ProtectedRoute from './features/ProtectedRoute'; -import { useSelector } from 'react-redux'; import ErrorMessage from './components/ErrorMessage'; +import { useAppSelector } from './app/store'; function App() { - const user = useSelector(state => state.account.user) || null; + const user = useAppSelector(state => state.account.user) || null; return (
@@ -25,7 +25,7 @@ function App() { {element} } />; })} - } /> + } />
diff --git a/src/client/src/app/actions/account/addFavoriteRecipe.js b/src/client/src/app/actions/account/addFavoriteRecipe.js deleted file mode 100644 index ebc91f1..0000000 --- a/src/client/src/app/actions/account/addFavoriteRecipe.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const addFavoriteRecipe = createAsyncThunk( - 'account/addFavoriteRecipe', - async ({ recipe }, thunkAPI) => { - if (!thunkAPI.extra.userService.token) { - thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); - } - const thunk = thunkAPI.extra.thunkErrorWrapper( - thunkAPI.extra.userService.addFavoriteRecipe, - thunkAPI.rejectWithValue, - thunkAPI.extra.userService - ); - await thunk(recipe); - }, -); diff --git a/src/client/src/app/actions/account/addFavoriteRecipe.ts b/src/client/src/app/actions/account/addFavoriteRecipe.ts new file mode 100644 index 0000000..8727d5e --- /dev/null +++ b/src/client/src/app/actions/account/addFavoriteRecipe.ts @@ -0,0 +1,22 @@ +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { AuthStoreError } from '../../slices/types/Auth/AuthState'; +import { thunkErrorWrapper } from '../../utils/thunkErrorWrapper'; +import { RecipeModel } from '../../../services/models/RecipeModel'; + +export const addFavoriteRecipe = createAppAsyncThunk( + 'account/addFavoriteRecipe', + async ({ userId, recipe }, thunkAPI) => { + if (!thunkAPI.extra.userService.token) { + thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); + } + const thunk = thunkErrorWrapper( + thunkAPI.extra.userService.addFavoriteRecipe, + thunkAPI.rejectWithValue, + thunkAPI.extra.userService + ); + await thunk(userId, recipe.id); + return recipe; + }, + ); diff --git a/src/client/src/app/actions/account/deleteFavoriteRecipe.js b/src/client/src/app/actions/account/deleteFavoriteRecipe.js deleted file mode 100644 index d5748df..0000000 --- a/src/client/src/app/actions/account/deleteFavoriteRecipe.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const deleteFavoriteRecipe = createAsyncThunk( - 'account/deleteFavoriteRecipe', - async ({ recipe }, thunkAPI) => { - if (!thunkAPI.extra.userService.token) { - thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); - } - const thunk = thunkAPI.extra.thunkErrorWrapper( - thunkAPI.extra.userService.deleteFavoriteRecipe, - thunkAPI.rejectWithValue, - thunkAPI.extra.userService - ); - await thunk(recipe); - }, -); diff --git a/src/client/src/app/actions/account/deleteFavoriteRecipe.ts b/src/client/src/app/actions/account/deleteFavoriteRecipe.ts new file mode 100644 index 0000000..fb46831 --- /dev/null +++ b/src/client/src/app/actions/account/deleteFavoriteRecipe.ts @@ -0,0 +1,22 @@ +import { AuthStoreError } from '../../slices/types/Auth/AuthState'; +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { thunkErrorWrapper } from '../../utils/thunkErrorWrapper'; +import { RecipeModel } from '../../../services/models/RecipeModel'; + +export const deleteFavoriteRecipe = createAppAsyncThunk( + 'account/deleteFavoriteRecipe', + async ({ recipe }, thunkAPI) => { + if (!thunkAPI.extra.userService.token) { + thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); + } + const thunk = thunkErrorWrapper( + thunkAPI.extra.userService.deleteFavoriteRecipe, + thunkAPI.rejectWithValue, + thunkAPI.extra.userService + ); + await thunk(recipe.id); + return recipe; + }, + ); diff --git a/src/client/src/app/actions/account/loadFavoriteRecipes.js b/src/client/src/app/actions/account/loadFavoriteRecipes.js deleted file mode 100644 index b03703d..0000000 --- a/src/client/src/app/actions/account/loadFavoriteRecipes.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const loadFavoriteRecipes = createAsyncThunk( - 'account/loadUserFavorites', - async (_, thunkAPI) => { - if (!thunkAPI.extra.userService.token) { - thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); - } - const thunk = thunkAPI.extra.thunkErrorWrapper( - thunkAPI.extra.userService.getFavoriteRecipes, - thunkAPI.rejectWithValue, - thunkAPI.extra.userService - ); - await thunk(); - }, -); diff --git a/src/client/src/app/actions/account/loadFavoriteRecipes.ts b/src/client/src/app/actions/account/loadFavoriteRecipes.ts new file mode 100644 index 0000000..6819b94 --- /dev/null +++ b/src/client/src/app/actions/account/loadFavoriteRecipes.ts @@ -0,0 +1,21 @@ +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { RecipeModel } from '../../../services/models/RecipeModel'; +import { AuthStoreError } from '../../slices/types/Auth/AuthState'; +import { thunkErrorWrapper } from '../../utils/thunkErrorWrapper'; + +export const loadFavoriteRecipes = createAppAsyncThunk( + 'account/loadUserFavorites', + async (_, thunkAPI) => { + if (!thunkAPI.extra.userService.token) { + thunkAPI.extra.userService.setToken(thunkAPI.getState().account.token); + } + const thunk = thunkErrorWrapper( + thunkAPI.extra.userService.getFavoriteRecipes, + thunkAPI.rejectWithValue, + thunkAPI.extra.userService + ); + return await thunk(); + }, + ); diff --git a/src/client/src/app/actions/account/loginUser.js b/src/client/src/app/actions/account/loginUser.js deleted file mode 100644 index e0817d7..0000000 --- a/src/client/src/app/actions/account/loginUser.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const loginUser = createAsyncThunk( - 'account/login', - async ({ credentials }, thunkAPI) => { - const thunk = thunkAPI.extra.thunkErrorWrapper( - thunkAPI.extra.authService.login, - thunkAPI.rejectWithValue, - thunkAPI.extra.authService - ); - await thunk(credentials); - } -); diff --git a/src/client/src/app/actions/account/loginUser.ts b/src/client/src/app/actions/account/loginUser.ts new file mode 100644 index 0000000..4e72d2b --- /dev/null +++ b/src/client/src/app/actions/account/loginUser.ts @@ -0,0 +1,19 @@ +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { AuthStoreError } from '../../slices/types/Auth/AuthState'; +import { AuthFulfilled } from '../../slices/types/Auth/AuthFulfilled'; +import { UserDTO } from '../../../services/DTO/UserDTO'; +import { thunkErrorWrapper } from '../../utils/thunkErrorWrapper'; + +export const loginUser = createAppAsyncThunk( + 'account/login', + async ({ email, password }: UserDTO, thunkAPI) => { + const thunk = thunkErrorWrapper( + thunkAPI.extra.authService.login, + thunkAPI.rejectWithValue, + thunkAPI.extra.authService + ); + return await thunk(email, password); + } + ); diff --git a/src/client/src/app/actions/account/registerUser.js b/src/client/src/app/actions/account/registerUser.js deleted file mode 100644 index 15f7a62..0000000 --- a/src/client/src/app/actions/account/registerUser.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const registerUser = createAsyncThunk( - 'account/register', - async ({ credentials }, thunkAPI) => { - const thunk = thunkAPI.extra.thunkErrorWrapper( - thunkAPI.extra.authService.register, - thunkAPI.rejectWithValue, - thunkAPI.extra.authService - ); - await thunk(credentials); - }, -); diff --git a/src/client/src/app/actions/account/registerUser.ts b/src/client/src/app/actions/account/registerUser.ts new file mode 100644 index 0000000..d9950fa --- /dev/null +++ b/src/client/src/app/actions/account/registerUser.ts @@ -0,0 +1,18 @@ +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { AuthFulfilled } from '../../slices/types/Auth/AuthFulfilled'; +import { UserDTO } from '../../../services/DTO/UserDTO'; +import { AuthStoreError } from '../../slices/types/Auth/AuthState'; + +export const registerUser = createAppAsyncThunk( + 'account/register', + async ({ username, password }, thunkAPI) => { + const thunk = thunkAPI.extra.thunkErrorWrapper( + thunkAPI.extra.authService.register, + thunkAPI.rejectWithValue, + thunkAPI.extra.authService + ); + return await thunk(username, password); + } + ); diff --git a/src/client/src/app/actions/recipes/loadRecipes.js b/src/client/src/app/actions/recipes/loadRecipes.js deleted file mode 100644 index 27bf016..0000000 --- a/src/client/src/app/actions/recipes/loadRecipes.js +++ /dev/null @@ -1,29 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; - -export const loadRecipes = createAsyncThunk( - 'recipes/loadRecipes', - async ({ queryString }, { getState, rejectWithValue }) => { - const token = getState()?.account.token; - const headers = { - 'Content-Type': 'application/json', - 'authorization': token, - }; - const params = new URLSearchParams({ recipeName: queryString }); - try { - const response = await fetch('/recipes?' + params, { - method: 'GET', - headers, - }); - console.log(await response.json()); - if (!response.ok) { - return rejectWithValue(await response.json()); - } - return { - ...await response.json(), - }; - } catch (error) { - return rejectWithValue(error.response.data.message ?? error.message); - } - - }, -); diff --git a/src/client/src/app/actions/recipes/loadRecipes.ts b/src/client/src/app/actions/recipes/loadRecipes.ts new file mode 100644 index 0000000..3213afb --- /dev/null +++ b/src/client/src/app/actions/recipes/loadRecipes.ts @@ -0,0 +1,22 @@ +import { RecipeModel } from '../../../services/models/RecipeModel'; +import { createAppAsyncThunk } from '../../utils/createAppAsyncThunk'; +import { RecipeStoreError } from '../../slices/types/Recipe/RecipeState'; +import { thunkErrorWrapper } from '../../utils/thunkErrorWrapper'; + +export const loadRecipes = createAppAsyncThunk( + 'recipes/loadRecipes', + async (queryString: string, thunkAPI) => { + if (!thunkAPI.extra.recipeService.token) { + thunkAPI.extra.recipeService.setToken(thunkAPI.getState().account.token); + } + const thunk = thunkErrorWrapper( + thunkAPI.extra.recipeService.getRecipesByName, + thunkAPI.rejectWithValue, + thunkAPI.extra.recipeService + ); + const params = new URLSearchParams({ recipeName: queryString }); + return await thunk(params); + }, + ); diff --git a/src/client/src/app/slices/accountSlice.js b/src/client/src/app/slices/accountSlice.ts similarity index 53% rename from src/client/src/app/slices/accountSlice.js rename to src/client/src/app/slices/accountSlice.ts index a8c002c..0e6acf1 100644 --- a/src/client/src/app/slices/accountSlice.js +++ b/src/client/src/app/slices/accountSlice.ts @@ -1,31 +1,41 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { loginUser } from '../actions/account/loginUser'; import { registerUser } from '../actions/account/registerUser'; import { loadFavoriteRecipes } from '../actions/account/loadFavoriteRecipes'; import { addFavoriteRecipe } from '../actions/account/addFavoriteRecipe'; import { deleteFavoriteRecipe } from '../actions/account/deleteFavoriteRecipe'; +import { AuthState } from './types/Auth/AuthState'; +import { AuthFulfilled } from './types/Auth/AuthFulfilled'; -const initialState = { - user: JSON.parse(localStorage.getItem('cyberChefUser')), - token: localStorage.getItem('cyberChefToken'), +const storageNames = { + user: 'cyberChefUser', + token: 'cyberChefToken', +}; + +const initialState: AuthState = { + user: JSON.parse(localStorage.getItem(storageNames.user) ?? 'null'), + token: localStorage.getItem(storageNames.token), userRecipes: [], loading: false, error: null, }; -const storageNames = { - user: 'cyberChefUser', - token: 'cyberChefToken', -}; +const isRejectedAction = (action: Action) => action.type.endsWith('rejected'); +const isPendingAction = (action: Action) => action.type.endsWith('pending'); -const isRejectedAction = action => action.type.endsWith('rejected'); -const isPendingAction = action => action.type.endsWith('pending'); +const saveUser = (state: AuthState, action: PayloadAction) => { + state.loading = false; + state.user = action.payload.user; + state.token = action.payload.accessToken; + localStorage.setItem(storageNames.user, JSON.stringify(state.user)); + localStorage.setItem(storageNames.token, state.token as string); +}; const accountSlice = createSlice({ name: 'account', initialState, reducers: { - logout(state) { + logout(state: AuthState) { state.user = null; state.token = null; localStorage.removeItem(storageNames.user); @@ -34,26 +44,18 @@ const accountSlice = createSlice({ }, extraReducers: builder => { builder.addCase(loginUser.fulfilled, (state, action) => { - state.loading = false; - state.user = action.payload.user; - state.token = action.payload.accessToken; - localStorage.setItem(storageNames.user, JSON.stringify(state.user)); - localStorage.setItem(storageNames.token, state.token); + saveUser(state, action); }); - builder.addCase(registerUser.fulfilled, (state, action) => { - state.loading = false; - state.user = action.payload.user; - state.token = action.payload.accessToken; - localStorage.setItem(storageNames.user, JSON.stringify(state.user)); - localStorage.setItem(storageNames.token, state.token); + builder.addCase(registerUser.fulfilled, (state: AuthState, action) => { + saveUser(state, action); }); - builder.addCase(loadFavoriteRecipes.fulfilled, (state, action) => { + builder.addCase(loadFavoriteRecipes.fulfilled, (state: AuthState, action) => { state.userRecipes = action.payload; }); - builder.addCase(addFavoriteRecipe.fulfilled, (state, action) => { + builder.addCase(addFavoriteRecipe.fulfilled, (state: AuthState, action) => { state.userRecipes.push(action.payload); }); - builder.addCase(deleteFavoriteRecipe.fulfilled, (state, action) => { + builder.addCase(deleteFavoriteRecipe.fulfilled, (state: AuthState, action) => { state.userRecipes = state.userRecipes.filter(item => item.id !== action.payload.id); }); builder.addMatcher(isRejectedAction, (state, action) => { diff --git a/src/client/src/app/slices/recipesSlice.js b/src/client/src/app/slices/recipesSlice.ts similarity index 88% rename from src/client/src/app/slices/recipesSlice.js rename to src/client/src/app/slices/recipesSlice.ts index f51d90c..6157116 100644 --- a/src/client/src/app/slices/recipesSlice.js +++ b/src/client/src/app/slices/recipesSlice.ts @@ -1,7 +1,8 @@ import { createSlice } from '@reduxjs/toolkit'; import { loadRecipes } from '../actions/recipes/loadRecipes'; +import { RecipeState } from './types/Recipe/RecipeState'; -const initialState = { +const initialState: RecipeState = { recipes: [], loading: false, error: null, diff --git a/src/client/src/app/slices/types/Auth/AuthFulfilled.ts b/src/client/src/app/slices/types/Auth/AuthFulfilled.ts new file mode 100644 index 0000000..9c9516b --- /dev/null +++ b/src/client/src/app/slices/types/Auth/AuthFulfilled.ts @@ -0,0 +1,6 @@ +import { User } from '../../../../services/models/User'; + +export interface AuthFulfilled { + user: User, + accessToken: string, +} diff --git a/src/client/src/app/slices/types/Auth/AuthState.ts b/src/client/src/app/slices/types/Auth/AuthState.ts new file mode 100644 index 0000000..9ac30f4 --- /dev/null +++ b/src/client/src/app/slices/types/Auth/AuthState.ts @@ -0,0 +1,11 @@ +import { User } from '../../../../services/models/User'; +import { RecipeModel } from '../../../../services/models/RecipeModel'; + +export type AuthStoreError = Error | undefined | null; +export interface AuthState { + user: User | null, + token: string | null, + userRecipes: RecipeModel[], + loading: boolean, + error: AuthStoreError +} diff --git a/src/client/src/app/slices/types/Recipe/RecipeState.ts b/src/client/src/app/slices/types/Recipe/RecipeState.ts new file mode 100644 index 0000000..378b1eb --- /dev/null +++ b/src/client/src/app/slices/types/Recipe/RecipeState.ts @@ -0,0 +1,8 @@ +import { RecipeModel } from '../../../../services/models/RecipeModel'; + +export type RecipeStoreError = Error | null | undefined; +export interface RecipeState { + recipes: RecipeModel[], + loading: boolean, + error: RecipeStoreError +} diff --git a/src/client/src/app/store.js b/src/client/src/app/store.js deleted file mode 100644 index 948cf7f..0000000 --- a/src/client/src/app/store.js +++ /dev/null @@ -1,27 +0,0 @@ -import { combineReducers, configureStore } from '@reduxjs/toolkit'; -import accountReducer from './slices/accountSlice'; -import recipesReducer from './slices/recipesSlice'; -import AuthService from '../services/AuthService'; -import RecipeService from '../services/RecipeService'; -import { thunkErrorWrapper } from './utils/thunkErrorWrapper'; - -const recipeService = new RecipeService(process.env.BASE_URL); -const authService = new AuthService(process.env.BASE_URL); - -const rootReducer = combineReducers({ - account: accountReducer, - recipes: recipesReducer, -}); - - -const store = configureStore({ - reducer: rootReducer, - middleware: getDefaultMiddleware => - getDefaultMiddleware({ - thunk: { - extraArgument: { recipeService, authService, thunkErrorWrapper }, - }, - }), -}); - -export { store, recipeService, authService }; diff --git a/src/client/src/app/store.ts b/src/client/src/app/store.ts new file mode 100644 index 0000000..24f8a38 --- /dev/null +++ b/src/client/src/app/store.ts @@ -0,0 +1,47 @@ +import { Action, combineReducers, configureStore, ThunkAction } from '@reduxjs/toolkit'; +import accountReducer from './slices/accountSlice'; +import recipesReducer from './slices/recipesSlice'; +import AuthService from '../services/AuthService'; +import RecipeService from '../services/RecipeService'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import UserService from '../services/UserService'; + +const baseURL = process.env.REACT_APP_BASE_URL || 'http://localhost:3000/'; + +const recipeService = new RecipeService(baseURL); +const authService = new AuthService(baseURL); +const userService = new UserService(baseURL); + + +const rootReducer = combineReducers({ + account: accountReducer, + recipes: recipesReducer, +}); + + +const store = configureStore({ + reducer: rootReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + thunk: { + extraArgument: { recipeService, authService, userService }, + }, + }), +}); + +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType; +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; + +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +>; + +export { store, recipeService, authService, userService }; +export default store; + + diff --git a/src/client/src/app/utils/createAppAsyncThunk.ts b/src/client/src/app/utils/createAppAsyncThunk.ts new file mode 100644 index 0000000..9dd9d65 --- /dev/null +++ b/src/client/src/app/utils/createAppAsyncThunk.ts @@ -0,0 +1,12 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import RecipeService from '../../services/RecipeService'; +import AuthService from '../../services/AuthService'; +import UserService from '../../services/UserService'; +import { AppDispatch, RootState } from '../store'; + +export const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState, + dispatch: AppDispatch, + rejectValue: string, + extra: { recipeService: RecipeService, authService: AuthService, userService: UserService, thunkErrorWrapper: any } +}>(); diff --git a/src/client/src/app/utils/thunkErrorWrapper.js b/src/client/src/app/utils/thunkErrorWrapper.js deleted file mode 100644 index ed23936..0000000 --- a/src/client/src/app/utils/thunkErrorWrapper.js +++ /dev/null @@ -1,11 +0,0 @@ -export const thunkErrorWrapper = (method, rejectWithValue, context = null) => async (...args) => { - try { - const response = await method.apply(context, args); - if (response.error) { - return rejectWithValue(response.error); - } - return response; - } catch (error) { - return rejectWithValue(error.response.data.message ?? error.message); - } -}; diff --git a/src/client/src/app/utils/thunkErrorWrapper.ts b/src/client/src/app/utils/thunkErrorWrapper.ts new file mode 100644 index 0000000..5d7423c --- /dev/null +++ b/src/client/src/app/utils/thunkErrorWrapper.ts @@ -0,0 +1,17 @@ +import BaseService from '../../services/BaseService'; + +export function thunkErrorWrapper(method: (...args : Args[]) => ReturnType, + rejectWithValue: (value: Error) => any, + context: BaseService | null = null) { + return async (...args : Args[]) => { + try { + const response = await method.apply(context, args); + if ((response as any).error) { + return rejectWithValue((response as any).error); + } + return response; + } catch (error: any) { + return rejectWithValue(error.response.data.message ?? error.message); + } + }; +} diff --git a/src/client/src/components/Banner.jsx b/src/client/src/components/Banner.tsx similarity index 100% rename from src/client/src/components/Banner.jsx rename to src/client/src/components/Banner.tsx diff --git a/src/client/src/components/ErrorMessage.jsx b/src/client/src/components/ErrorMessage.tsx similarity index 58% rename from src/client/src/components/ErrorMessage.jsx rename to src/client/src/components/ErrorMessage.tsx index dca725b..d510dfc 100644 --- a/src/client/src/components/ErrorMessage.jsx +++ b/src/client/src/components/ErrorMessage.tsx @@ -1,12 +1,11 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { CSSProperties } from 'react'; -ErrorMessage.propTypes = { - error: PropTypes.object -}; +interface Props { + error: Error +} -function ErrorMessage({ error }) { - const styles = { +function ErrorMessage({ error }: Props) { + const styles: CSSProperties = { margin: '1rem', fontSize: '1.25rem', color: 'red', diff --git a/src/client/src/components/Footer.jsx b/src/client/src/components/Footer.tsx similarity index 100% rename from src/client/src/components/Footer.jsx rename to src/client/src/components/Footer.tsx diff --git a/src/client/src/components/Header.jsx b/src/client/src/components/Header.tsx similarity index 86% rename from src/client/src/components/Header.jsx rename to src/client/src/components/Header.tsx index f6ded98..c006fe1 100644 --- a/src/client/src/components/Header.jsx +++ b/src/client/src/components/Header.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { useSelector } from 'react-redux'; +import { useAppSelector } from '../app/store'; function Header() { - const user = useSelector(state => state.account.user); + const user = useAppSelector(state => state.account.user); return (
diff --git a/src/client/src/components/Loader.jsx b/src/client/src/components/Loader.tsx similarity index 100% rename from src/client/src/components/Loader.jsx rename to src/client/src/components/Loader.tsx diff --git a/src/client/src/components/Recipe.jsx b/src/client/src/components/Recipe.tsx similarity index 72% rename from src/client/src/components/Recipe.jsx rename to src/client/src/components/Recipe.tsx index 6ed2b5a..93aae30 100644 --- a/src/client/src/components/Recipe.jsx +++ b/src/client/src/components/Recipe.tsx @@ -1,19 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import { RecipeModel } from '../services/models/RecipeModel'; import { Link } from 'react-router-dom'; +interface Props { + recipe: RecipeModel, + clickHandler?: (recipe: RecipeModel) => void, + deleteHandler?: (recipe: RecipeModel) => void +} -Recipe.propTypes = { - recipe: PropTypes.shape({ - id: PropTypes.number, - name: PropTypes.string, - image_link: PropTypes.string, - products: PropTypes.string, - }), - clickHandler: PropTypes.func, - deleteHandler: PropTypes.func, -}; - -function Recipe({ recipe, clickHandler, deleteHandler }) { +function Recipe({ recipe, clickHandler, deleteHandler }: Props) { return (
diff --git a/src/client/src/components/Search.jsx b/src/client/src/components/Search.tsx similarity index 67% rename from src/client/src/components/Search.jsx rename to src/client/src/components/Search.tsx index 2bca826..bd0871a 100644 --- a/src/client/src/components/Search.jsx +++ b/src/client/src/components/Search.tsx @@ -1,18 +1,18 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -Search.propTypes = { - searchCallback: PropTypes.func, -}; +interface Props { + searchCallback: (event: React.FormEvent, formData: { queryString: string }) => void +} -function Search({ searchCallback }) { +function Search({ searchCallback } : Props) { const [formData, setFormData] = useState({ queryString: '', }); - const handleChange = e => { + const handleChange = (e: React.FormEvent) => { + const target = e.target as HTMLInputElement; setFormData({ ...formData, - [e.target.name]: e.target.value.trim(), + [target.name]: target.value.trim(), }); }; diff --git a/src/client/src/features/AppRoutes.js b/src/client/src/features/AppRoutes.tsx similarity index 100% rename from src/client/src/features/AppRoutes.js rename to src/client/src/features/AppRoutes.tsx diff --git a/src/client/src/features/ProtectedRoute.jsx b/src/client/src/features/ProtectedRoute.jsx deleted file mode 100644 index adc16ee..0000000 --- a/src/client/src/features/ProtectedRoute.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Navigate } from 'react-router-dom'; -import PropTypes from 'prop-types'; - -const ProtectedRoute = ({ user, children }) => { - if (!user) { - return ; - } - return children; -}; - -ProtectedRoute.propTypes = { - user: PropTypes.shape({ - userRecipes: PropTypes.array.isRequired, - username: PropTypes.string, - }), - children: PropTypes.element.isRequired, -}; - -export default ProtectedRoute; diff --git a/src/client/src/features/ProtectedRoute.tsx b/src/client/src/features/ProtectedRoute.tsx new file mode 100644 index 0000000..d80b9b7 --- /dev/null +++ b/src/client/src/features/ProtectedRoute.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { User } from '../services/models/User'; + +interface Props { + user: User | null, + children: React.FunctionComponent | React.JSX.Element +} + + +const ProtectedRoute = ({ user, children }: Props) => { + if (!user) { + return ; + } + return <>{children}; +}; + + +export default ProtectedRoute; diff --git a/src/client/src/features/ValidationError.js b/src/client/src/features/ValidationError.js deleted file mode 100644 index 43a15f5..0000000 --- a/src/client/src/features/ValidationError.js +++ /dev/null @@ -1,29 +0,0 @@ -export default class ValidationError { - constructor(type, description) { - this.type = type; - this.description = description; - } - static defaultPasswordLength = 8; - static validate(data) { - const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; - const validationErrors = []; - if (!emailRegex.test(data.email)) validationErrors.push(new ValidationError( - 'email', 'Invalid email', - )); - if (data.password.length < ValidationError.defaultPasswordLength) - validationErrors.push(new ValidationError( - 'password', 'Password is shorter than 8 symbols', - )); - if (!data.password || !data.password.length) validationErrors.push(new ValidationError( - 'password', 'Password is required', - )); - if (data.confirm && data.confirm !== data.password) validationErrors.push(new ValidationError( - 'confirm', 'Password does not match', - )); - return validationErrors; - } - - toString() { - return this.description; - } -} diff --git a/src/client/src/features/ValidationError.ts b/src/client/src/features/ValidationError.ts new file mode 100644 index 0000000..8137e31 --- /dev/null +++ b/src/client/src/features/ValidationError.ts @@ -0,0 +1,40 @@ +export enum ValidationErrors { + Email = 'email', + Password = 'password', + Confirm = 'confirm', +} + +export enum ValidationDescriptions { + Email = 'Invalid email', + PasswordShort = 'Password is shorter than 8 symbols', + PasswordEmpty = 'Password is required', + Confirm = 'Password does not match', +} + +export default class ValidationError { + type: string; + description: string; + constructor(type: ValidationErrors, description: ValidationDescriptions) { + this.type = type; + this.description = description; + } + static defaultPasswordLength = 8; + static validate(data: { email: string; password: string; confirm?: string; }) { + const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; + const validationErrors = []; + if (!emailRegex.test(data.email)) validationErrors.push(new ValidationError( + ValidationErrors.Email, ValidationDescriptions.Email, + )); + if (data.password.length < ValidationError.defaultPasswordLength) + validationErrors.push(new ValidationError( + ValidationErrors.Password, ValidationDescriptions.PasswordShort, + )); + if (!data.password || !data.password.length) validationErrors.push(new ValidationError( + ValidationErrors.Password, ValidationDescriptions.PasswordEmpty, + )); + if (data.confirm && data.confirm !== data.password) validationErrors.push(new ValidationError( + ValidationErrors.Confirm, ValidationDescriptions.Confirm, + )); + return validationErrors; + } +} diff --git a/src/client/src/index.js b/src/client/src/index.tsx similarity index 91% rename from src/client/src/index.js rename to src/client/src/index.tsx index d50b235..baf0c4a 100644 --- a/src/client/src/index.js +++ b/src/client/src/index.tsx @@ -7,7 +7,7 @@ import reportWebVitals from './reportWebVitals'; import './index.css'; import { BrowserRouter } from 'react-router-dom'; -const container = document.getElementById('root'); +const container = document.getElementById('root') as HTMLElement; const root = createRoot(container); root.render( diff --git a/src/client/src/pages/Home.jsx b/src/client/src/pages/Home.tsx similarity index 100% rename from src/client/src/pages/Home.jsx rename to src/client/src/pages/Home.tsx diff --git a/src/client/src/pages/Login.jsx b/src/client/src/pages/Login.tsx similarity index 81% rename from src/client/src/pages/Login.jsx rename to src/client/src/pages/Login.tsx index 9b6f1bf..59c073a 100644 --- a/src/client/src/pages/Login.jsx +++ b/src/client/src/pages/Login.tsx @@ -1,27 +1,28 @@ import React, { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; import ValidationError from '../features/ValidationError'; import { loginUser } from '../app/actions/account/loginUser'; import ErrorMessage from '../components/ErrorMessage'; +import { useAppDispatch, useAppSelector } from '../app/store'; function Login() { - const { error, user } = useSelector(state => state.account); + const { error, user } = useAppSelector(state => state.account); const [formData, setFormData] = useState({ email: '', password: '', }); - const [errors, setErrors] = useState([]); + const [errors, setErrors] = useState([]); const navigate = useNavigate(); - const dispatch = useDispatch(); - const handleChange = e => { + const dispatch = useAppDispatch(); + const handleChange = (event: React.FormEvent) => { + const target = event.target as HTMLInputElement; setFormData({ ...formData, - [e.target.name]: e.target.value.trim(), + [target.name]: target.value.trim(), }); }; - const handleSubmit = event => { + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); setErrors([]); const validationErrors = ValidationError.validate(formData); @@ -44,7 +45,7 @@ function Login() {

Login

- { error && } + { error && }

state.recipes.recipes); - const dispatch = useDispatch(); - const { error, loading } = useSelector(state => state.recipes.recipes); - const searchCallback = useCallback((event, { queryString }) => { - event.preventDefault(); - dispatch(loadRecipes(queryString)); - }, []); + const [recipes, setRecipes] = useState([]); + const { error, loading } = useAppSelector(state => state.recipes); + const storeRecipes = useAppSelector(state => state.recipes.recipes); + const dispatch = useAppDispatch(); + const searchCallback = useCallback( + (event: React.FormEvent, + { queryString }: { queryString: string }) => { + event.preventDefault(); + dispatch(loadRecipes(queryString)); + }, []); const clickCallback = useCallback( - recipe => dispatch(addFavoriteRecipe(recipe)), + (recipe: RecipeModel) => dispatch(addFavoriteRecipe(recipe)), [], ); @@ -37,7 +40,7 @@ function SearchRecipes() {
{recipes.map((recipe, index) => )} + key={index}/>)}
:

No recipes

}
diff --git a/src/client/src/pages/User.jsx b/src/client/src/pages/User.tsx similarity index 70% rename from src/client/src/pages/User.jsx rename to src/client/src/pages/User.tsx index c719b80..6dea36d 100644 --- a/src/client/src/pages/User.jsx +++ b/src/client/src/pages/User.tsx @@ -1,23 +1,24 @@ import React, { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import Recipe from '../components/Recipe'; import ErrorMessage from '../components/ErrorMessage'; import { loadFavoriteRecipes } from '../app/actions/account/loadFavoriteRecipes'; import Loader from '../components/Loader'; import { deleteFavoriteRecipe } from '../app/actions/account/deleteFavoriteRecipe'; +import { useAppDispatch, useAppSelector } from '../app/store'; +import { RecipeModel } from '../services/models/RecipeModel'; function User() { - const userRecipes = useSelector(state => state.account.userRecipes); - const { error, loading } = useSelector(state => state.account); - const dispatch = useDispatch(); - const deleteHandler = useCallback(recipe => { + const userRecipes = useAppSelector(state => state.account.userRecipes); + const { error, loading } = useAppSelector(state => state.account); + const dispatch = useAppDispatch(); + const deleteHandler = useCallback((recipe: RecipeModel) => { dispatch(deleteFavoriteRecipe(recipe)); }, [], ); useEffect(() => { - dispatch(loadFavoriteRecipes()); + dispatch(loadFavoriteRecipes(null)); }, []); if (loading) return ; diff --git a/src/client/src/reportWebVitals.js b/src/client/src/reportWebVitals.ts similarity index 67% rename from src/client/src/reportWebVitals.js rename to src/client/src/reportWebVitals.ts index 5253d3a..cfad7f0 100644 --- a/src/client/src/reportWebVitals.js +++ b/src/client/src/reportWebVitals.ts @@ -1,5 +1,7 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); diff --git a/src/client/src/services/AuthService.js b/src/client/src/services/AuthService.js deleted file mode 100644 index d6d8b0e..0000000 --- a/src/client/src/services/AuthService.js +++ /dev/null @@ -1,32 +0,0 @@ -import BaseService from './BaseService'; -import AuthError from './errors/AuthError'; - -export default class AuthService extends BaseService { - constructor(baseURL) { - super(baseURL); - } - - async register(userData) { - try { - return await this.request('/register', 'POST', userData); - } catch (error) { - throw new AuthError('Error registering: ' + error.message); - } - } - - async login(credentials) { - try { - const response = await this.request('/login', 'POST', credentials); - if (response.token) { - this.setToken(response.token); - } - return response; - } catch (error) { - throw new AuthError('Error logging in: ' + error.message); - } - } - - async logout() { - this.setToken(null); - } -} diff --git a/src/client/src/services/AuthService.ts b/src/client/src/services/AuthService.ts new file mode 100644 index 0000000..6ab190f --- /dev/null +++ b/src/client/src/services/AuthService.ts @@ -0,0 +1,30 @@ +import BaseService from './BaseService'; +import AuthError from './errors/AuthError'; +import { UserDTO } from './DTO/UserDTO'; +import { AuthFulfilled } from '../app/slices/types/Auth/AuthFulfilled'; + +export default class AuthService extends BaseService { + constructor(baseURL: string) { + super(baseURL); + } + + async register(userData: UserDTO): Promise { + try { + return await this.request('/register', 'POST', userData); + } catch (error) { + throw new AuthError('Error registering: ' + (error as Error).message); + } + } + + async login(username: string, password: string): Promise { + try { + const response = await this.request('/login', 'POST', { username, password }); + if (response.token) { + this.setToken(response.token); + } + return response; + } catch (error) { + throw new AuthError('Error logging in: ' + (error as Error).message); + } + } +} diff --git a/src/client/src/services/BaseService.js b/src/client/src/services/BaseService.ts similarity index 79% rename from src/client/src/services/BaseService.js rename to src/client/src/services/BaseService.ts index 5dd72fc..a3bdc75 100644 --- a/src/client/src/services/BaseService.js +++ b/src/client/src/services/BaseService.ts @@ -2,16 +2,18 @@ import { useDispatch } from 'react-redux'; import { logout as storeLogout } from '../app/slices/accountSlice'; export default class BaseService { - constructor(baseURL) { + public token: string | null; + private readonly baseURL: string; + constructor(baseURL: string) { this.baseURL = baseURL; this.token = null; } - setToken(token) { + setToken(token: string | null) { this.token = token; } - async request(endpoint, method, body = {}) { + async request(endpoint: string, method: string, body = {}) { const url = `${this.baseURL}${endpoint}`; try { diff --git a/src/client/src/services/DTO/RecipeDTO.ts b/src/client/src/services/DTO/RecipeDTO.ts new file mode 100644 index 0000000..012584f --- /dev/null +++ b/src/client/src/services/DTO/RecipeDTO.ts @@ -0,0 +1,7 @@ +export interface RecipeDTO { + readonly name: string; + readonly categoryId: number; + readonly products?: string; + readonly description?: string; + readonly imageLink?: string; +} diff --git a/src/client/src/services/DTO/UserDTO.ts b/src/client/src/services/DTO/UserDTO.ts new file mode 100644 index 0000000..ce457ef --- /dev/null +++ b/src/client/src/services/DTO/UserDTO.ts @@ -0,0 +1,6 @@ +export interface UserDTO { + email: string, + username?: string, + imageLink?: string, + password: string, +} diff --git a/src/client/src/services/RecipeService.js b/src/client/src/services/RecipeService.ts similarity index 50% rename from src/client/src/services/RecipeService.js rename to src/client/src/services/RecipeService.ts index 226697b..a5f530a 100644 --- a/src/client/src/services/RecipeService.js +++ b/src/client/src/services/RecipeService.ts @@ -1,48 +1,50 @@ import BaseService from './BaseService'; import RecipeError from './errors/RecipeError'; +import { RecipeModel } from './models/RecipeModel'; +import { RecipeDTO } from './DTO/RecipeDTO'; export default class RecipeService extends BaseService { - constructor(baseURL) { + constructor(baseURL: string) { super(baseURL); } - async createRecipe(recipeData) { + async createRecipe(recipeData: RecipeDTO) { try { return await this.request(`/recipes`, 'POST', recipeData); } catch (error) { - throw new RecipeError('Error creating recipe: ' + error.message); + throw new RecipeError('Error creating recipe: ' + (error as Error).message); } } - async getRecipe(recipeId) { + async getRecipe(recipeId: number): Promise { try { return await this.request(`/recipes/${recipeId}`, 'GET'); } catch (error) { - throw new RecipeError('Error getting recipe: ' + error.message); + throw new RecipeError('Error getting recipe: ' + (error as Error).message); } } - async getRecipesByName(query) { + async getRecipesByName(query: URLSearchParams): Promise { try { return await this.request(`/recipes?${query}`, 'GET'); } catch (error) { - throw new RecipeError('Error getting recipes by name: ' + error.message); + throw new RecipeError('Error getting recipes by name: ' + (error as Error).message); } } - async updateRecipe(recipeId, recipeData) { + async updateRecipe(recipeId: number, recipeData: RecipeDTO) { try { return await this.request(`/recipes/${recipeId}`, 'PUT', recipeData); } catch (error) { - throw new RecipeError('Error updating recipe: ' + error.message); + throw new RecipeError('Error updating recipe: ' + (error as Error).message); } } - async deleteRecipe(recipeId) { + async deleteRecipe(recipeId: number) { try { return await this.request(`/recipes/${recipeId}`, 'DELETE'); } catch (error) { - throw new RecipeError('Error deleting recipe: ' + error.message); + throw new RecipeError('Error deleting recipe: ' + (error as Error).message); } } diff --git a/src/client/src/services/UserService.js b/src/client/src/services/UserService.js deleted file mode 100644 index cd0d1b4..0000000 --- a/src/client/src/services/UserService.js +++ /dev/null @@ -1,42 +0,0 @@ -import BaseService from './BaseService'; -import UserError from './errors/UserError'; - -export default class UserService extends BaseService { - constructor(baseURL) { - super(baseURL); - } - - async getUserRecipes() { - try { - return await this.request(`/user/authored`, 'GET'); - } catch (error) { - throw new UserError('Error getting authored recipes: ' + error.message); - } - } - - async getFavoriteRecipes() { - try { - return await this.request(`/user`, 'GET'); - } catch (error) { - throw new UserError('Error getting favorite recipes: ' + error.message); - } - } - - async addFavoriteRecipe(userId, recipeId) { - try { - return await this.request(`/user`, 'POST', { userId, recipeId }); - } catch (error) { - throw new UserError('Error adding favorite recipe: ' + error.message); - } - } - async deleteFavoriteRecipe(recipeId) { - try { - return await this.request(`/user`, 'DELETE', recipeId); - } catch (error) { - throw new UserError('Error deleting favorite recipe: ' + error.message); - } - } - - - -} diff --git a/src/client/src/services/UserService.ts b/src/client/src/services/UserService.ts new file mode 100644 index 0000000..369a2e6 --- /dev/null +++ b/src/client/src/services/UserService.ts @@ -0,0 +1,43 @@ +import BaseService from './BaseService'; +import UserError from './errors/UserError'; +import { RecipeModel } from './models/RecipeModel'; + +export default class UserService extends BaseService { + constructor(baseURL: string) { + super(baseURL); + } + + async getUserRecipes(): Promise { + try { + return await this.request(`/user/authored`, 'GET'); + } catch (error) { + throw new UserError('Error getting authored recipes: ' + (error as Error).message); + } + } + + async getFavoriteRecipes(): Promise { + try { + return await this.request(`/user`, 'GET'); + } catch (error) { + throw new UserError('Error getting favorite recipes: ' + (error as Error).message); + } + } + + async addFavoriteRecipe(userId: number, recipeId: number): Promise { + try { + return await this.request(`/user`, 'POST', { userId, recipeId }); + } catch (error) { + throw new UserError('Error adding favorite recipe: ' + (error as Error).message); + } + } + async deleteFavoriteRecipe(recipeId: number): Promise { + try { + return await this.request(`/user`, 'DELETE', recipeId); + } catch (error) { + throw new UserError('Error deleting favorite recipe: ' + (error as Error).message); + } + } + + + +} diff --git a/src/client/src/services/errors/AuthError.js b/src/client/src/services/errors/AuthError.ts similarity index 77% rename from src/client/src/services/errors/AuthError.js rename to src/client/src/services/errors/AuthError.ts index e4e65c6..5de2056 100644 --- a/src/client/src/services/errors/AuthError.js +++ b/src/client/src/services/errors/AuthError.ts @@ -1,7 +1,7 @@ import BaseError from './BaseError'; export default class AuthError extends BaseError { - constructor(message) { + constructor(message: string) { super(message); } } diff --git a/src/client/src/services/errors/BaseError.js b/src/client/src/services/errors/BaseError.ts similarity index 83% rename from src/client/src/services/errors/BaseError.js rename to src/client/src/services/errors/BaseError.ts index e61281b..b40508b 100644 --- a/src/client/src/services/errors/BaseError.js +++ b/src/client/src/services/errors/BaseError.ts @@ -1,5 +1,5 @@ export default class BaseError extends Error { - constructor(message) { + constructor(message: string) { super(message); this.name = this.constructor.name; Error.captureStackTrace(this, this.constructor); diff --git a/src/client/src/services/errors/RecipeError.js b/src/client/src/services/errors/RecipeError.ts similarity index 78% rename from src/client/src/services/errors/RecipeError.js rename to src/client/src/services/errors/RecipeError.ts index 5a70c8e..1d6746b 100644 --- a/src/client/src/services/errors/RecipeError.js +++ b/src/client/src/services/errors/RecipeError.ts @@ -1,7 +1,7 @@ import BaseError from './BaseError'; export default class RecipeError extends BaseError { - constructor(message) { + constructor(message: string) { super(message); } } diff --git a/src/client/src/services/errors/UserError.js b/src/client/src/services/errors/UserError.ts similarity index 77% rename from src/client/src/services/errors/UserError.js rename to src/client/src/services/errors/UserError.ts index fec4517..5d2a3d3 100644 --- a/src/client/src/services/errors/UserError.js +++ b/src/client/src/services/errors/UserError.ts @@ -1,7 +1,7 @@ import BaseError from './BaseError'; export default class UserError extends BaseError { - constructor(message) { + constructor(message: string) { super(message); } } diff --git a/src/client/src/services/models/RecipeModel.ts b/src/client/src/services/models/RecipeModel.ts new file mode 100644 index 0000000..d284cb2 --- /dev/null +++ b/src/client/src/services/models/RecipeModel.ts @@ -0,0 +1,10 @@ +export interface RecipeModel { + id: number, + category_id: number, + creator_id: number, + name: string, + image_link: string, + products: string, + description: string, + +} diff --git a/src/client/src/services/models/User.ts b/src/client/src/services/models/User.ts new file mode 100644 index 0000000..80bfb5a --- /dev/null +++ b/src/client/src/services/models/User.ts @@ -0,0 +1,6 @@ +export interface User { + id: number, + username: string, + email: string, + image_link: string, +} diff --git a/src/client/src/setupProxy.js b/src/client/src/setupProxy.ts similarity index 65% rename from src/client/src/setupProxy.js rename to src/client/src/setupProxy.ts index edb6f2c..58795dd 100644 --- a/src/client/src/setupProxy.js +++ b/src/client/src/setupProxy.ts @@ -1,8 +1,9 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires const { createProxyMiddleware } = require('http-proxy-middleware'); import * as dotenv from 'dotenv'; dotenv.config(); -module.exports = function(app) { +module.exports = function(app: { use: (arg0: string, arg1: any) => void; }) { app.use( '/api', createProxyMiddleware({ diff --git a/src/client/src/setupTests.js b/src/client/src/setupTests.ts similarity index 100% rename from src/client/src/setupTests.js rename to src/client/src/setupTests.ts diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json new file mode 100644 index 0000000..a621af3 --- /dev/null +++ b/src/client/tsconfig.json @@ -0,0 +1,119 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "esnext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, + /* Skip type checking all .d.ts files. */ + "jsx": "react" + }, + "include": [ + "src", + "custom.d.ts" +] +}