diff --git a/Makefile b/Makefile index 4384528..6170357 100755 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ dev: setup test: setup npm run test -container: setup +container: docker build -t ${PROJECT_NAME} . clean: diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 05eccfd..d6ba134 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,6 +1,9 @@ #! /bin/bash # no verbose set +x +# save env to file +printenv > .env.production + # config envFilename='.env.production' resolvingPath='/usr/share/nginx/html' diff --git a/src/article/controller/articleViewModel.ts b/src/article/controller/articleViewModel.ts index b17e336..3900868 100755 --- a/src/article/controller/articleViewModel.ts +++ b/src/article/controller/articleViewModel.ts @@ -1,44 +1,42 @@ import type { ArticleStore } from "../domain/articleStore"; import { useCallback, useEffect } from "react"; -import { getArticle } from "article/data/articleAPIService"; -import { Article } from "article/domain/articleEntity"; +import { GetArticleUseCase } from "article/useCases/getArticleUseCase"; +import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase"; +import { useParams } from "react-router-dom"; function useArticleViewModel( store: ArticleStore, - fetchArticleUseCase: ( - fetchArticleCallback: (id: string) => Promise
, - setArticle: ArticleStore["setArticle"], - id: string, - ) => Promise
, - getArticleUseCase: ( - getArticle: ArticleStore["getArticle"], - setArticle: ArticleStore["setArticle"], - id: Article["id"], - ) => Promise
, - id: string, + fetchArticleUseCase: FetchArticleUseCase, + getArticleUseCase: GetArticleUseCase, ) { - let article: Article | null | undefined; + const { id } = useParams(); - const _getArticle = useCallback( - (id: string) => { - getArticleUseCase(store.getArticle, store.setArticle, id).then(async (value) => { - if (value == null) { - article = await fetchArticleUseCase(getArticle, store.setArticle, id); - return; - } - article = value; - }); + const getArticle = useCallback( + ( + articleID: string, + getArticleUseCase: GetArticleUseCase, + fetchArticleUseCase: FetchArticleUseCase + ) => { + getArticleUseCase.call(articleID).catch((_) => fetchArticleUseCase.call(articleID)); }, - [store.setArticle] + [] ); - useEffect(() => { - _getArticle(id); - }, [article]); + useEffect( + () => { + getArticle(id ?? '', getArticleUseCase, fetchArticleUseCase); + }, + [ + id, + getArticle, + getArticleUseCase, + fetchArticleUseCase + ], + ); return { - article: store.article, - shouldShowLoading: typeof store.article === "undefined" || store.isLoading, + article: store.currentArticle, + shouldShowLoading: store.isLoading, hasError: store.hasError, }; } diff --git a/src/article/data/articleAPIService.ts b/src/article/data/articleAPIService.ts index e8c830b..d24bb38 100755 --- a/src/article/data/articleAPIService.ts +++ b/src/article/data/articleAPIService.ts @@ -7,7 +7,7 @@ import { integratorApiClient } from "core/httpClient"; const articleEndpoint = "/papers/" -async function getArticle(id: string): Promise
{ +async function fetchArticle(id: string): Promise
{ try { const response = await integratorApiClient.get( // `https://run.mocky.io/v3/62cd4581-d864-4d46-b1d6-02b45b5d1994/${id}` @@ -33,4 +33,4 @@ async function getArticle(id: string): Promise
{ } } -export { getArticle }; +export { fetchArticle }; diff --git a/src/article/data/articleActions.ts b/src/article/data/articleActions.ts index 0a56176..56dd7bf 100755 --- a/src/article/data/articleActions.ts +++ b/src/article/data/articleActions.ts @@ -1,22 +1,25 @@ import type { Article } from "../domain/articleEntity"; -import { getArticle as getArticleAPI } from "./articleAPIService"; import * as actionTypes from "./articleActionTypes"; import { dispatchStatus } from "../../store/index"; -const setArticleAction = (article: Article) => (dispatch: any) => - dispatch({ type: actionTypes.SET_ARTICLE, article }); +const setArticleAction = (article: Article, articlesList: Array
) => (dispatch: any) => { + if (!articlesList.includes(article)) { + const updatedList = articlesList.concat([article]); + dispatch({ type: actionTypes.SET_ARTICLE, article, updatedList }); + } +} -const getArticleAction = (id: string) => (dispatch: any) => { +const getArticleAction = (id: string, articles: Array
) => (dispatch: any) => { + const filteredArray = articles.filter((e) => e.id == id); + if (filteredArray.length > 0) { + const article = filteredArray[0]; + dispatchStatus(actionTypes.GET_ARTICLE, ".success", article)(dispatch); + return article; + } - return getArticleAPI(id) - .then((article) => { - dispatchStatus(actionTypes.GET_ARTICLE, ".success", article)(dispatch); - return article; - }) - .catch((reason) => { - dispatchStatus(actionTypes.GET_ARTICLE, ".failure", reason)(dispatch); - return reason; - }); + const reason = 'Article not in the store'; + dispatchStatus(actionTypes.GET_ARTICLE, ".failure", reason)(dispatch); + return null; }; export { setArticleAction, getArticleAction }; diff --git a/src/article/data/articleCommonStateStore.ts b/src/article/data/articleCommonStateStore.ts index bee210f..0081a0d 100755 --- a/src/article/data/articleCommonStateStore.ts +++ b/src/article/data/articleCommonStateStore.ts @@ -2,37 +2,58 @@ import React, { useCallback, useState } from "react"; import { useDispatch } from "react-redux"; import { ArticleStore } from "../domain/articleStore"; import type { Article } from "../domain/articleEntity"; -import { getArticle as getArticleAPI } from "./articleAPIService"; const useArticleCommonStore = (): ArticleStore => { const [isLoading, setLoading] = useState(false); const [hasError, setError] = useState(false); - const [article, setArticleState] = useState
(); + const [currentArticle, setCurrentArticle] = useState
(null); + const [articles, setArticlesState] = useState>([]); const dispatch = useDispatch(); const getArticle = useCallback( - async (id: string) => { + (id: string) => { setLoading(true); - try { - const article = await getArticleAPI(id); - setArticleState(article); + if (typeof currentArticle === undefined) { + const fromStore = findArticleFromStore(id); + if (typeof fromStore === undefined) { + setError(true); + return null; + } + setCurrentArticle(fromStore); setLoading(false); - return article; - } catch (error) { - setError(true); - return null; + return fromStore; } + setLoading(false); + return currentArticle; }, [dispatch] ); + const findArticleFromStore = (id: string): Article | null => { + const filteredArray = articles.filter((e) => e.id == id); + if (filteredArray.length > 0) { + return filteredArray[0]; + } + return null; + } + + const setNewArticle = (newArticle: Article) => { + setCurrentArticle(newArticle); + if (!articles.includes(newArticle)) { + setArticlesState(articles.concat([newArticle])); + } else if (articles.length == 0) { + setArticlesState([newArticle]); + } + } + return { - article: article, + articles: articles, + currentArticle: currentArticle, isLoading, hasError, - setArticle: setArticleState, - getArticle, + setArticle: setNewArticle, + getArticle: getArticle, }; }; diff --git a/src/article/data/articleReducer.ts b/src/article/data/articleReducer.ts index bdc5d45..5aa2b62 100755 --- a/src/article/data/articleReducer.ts +++ b/src/article/data/articleReducer.ts @@ -5,7 +5,8 @@ import * as actionTypes from "./articleActionTypes"; type ArticleStoreState = Omit; const INITIAL_STATE: ArticleStoreState = { - article: undefined, + articles: [], + currentArticle: null, isLoading: false, hasError: false, }; @@ -16,13 +17,13 @@ const articleReducer = ( ): ArticleStoreState => { switch (action.type) { case actionTypes.SET_ARTICLE: - return { ...state, article: action.article }; + return { ...state, articles: action.updatedList, currentArticle: action.article, hasError: typeof action.article === undefined, isLoading: false }; case actionTypes.GET_ARTICLE: return { ...state, isLoading: true }; case actionTypes.GET_ARTICLE_SUCCESS: - return { ...state, isLoading: false, article: action.payload }; + return { ...state, isLoading: false, currentArticle: action.payload }; case actionTypes.GET_ARTICLE_FAILURE: - return { ...state, hasError: true, isLoading: false }; + return { ...state, hasError: false, isLoading: true }; default: return state; } diff --git a/src/article/data/articleStoreImplementation.ts b/src/article/data/articleStoreImplementation.ts index 8d19b97..76c384e 100755 --- a/src/article/data/articleStoreImplementation.ts +++ b/src/article/data/articleStoreImplementation.ts @@ -9,22 +9,23 @@ import { RootState, useAppSelector } from "store"; const articleSelector = (state: RootState): ArticleStoreState => state.article; const useArticleStore = (): ArticleStore => { - const { isLoading, article, hasError } = useAppSelector(articleSelector); + const { isLoading, hasError, currentArticle, articles } = useAppSelector(articleSelector); const dispatch = useDispatch(); const setArticle = useCallback( - (article: Article) => setArticleAction(article)(dispatch), + (article: Article) => setArticleAction(article, articles)(dispatch), [dispatch] ); const getArticle = useCallback( - (id: string) => getArticleAction(id)(dispatch), + (id: string) => getArticleAction(id, articles)(dispatch), [dispatch] ); return { - article: article, + articles: articles, + currentArticle: currentArticle, isLoading, hasError, setArticle, diff --git a/src/article/domain/articleStore.ts b/src/article/domain/articleStore.ts index 4ec1805..68546e7 100755 --- a/src/article/domain/articleStore.ts +++ b/src/article/domain/articleStore.ts @@ -1,13 +1,14 @@ import { Article } from './articleEntity'; interface ArticleStore { // State - article: Article | undefined; + articles: Array
; + currentArticle: Article | null; isLoading: boolean; hasError: boolean; // Actions - setArticle(article?: Article): void; - getArticle(identifier: string): Promise
; + setArticle(article: Article): void; + getArticle(identifier: string): Article | null; } export type { ArticleStore }; diff --git a/src/article/useCases/fetchArticleUseCase.ts b/src/article/useCases/fetchArticleUseCase.ts index a3b646d..1ab8f64 100644 --- a/src/article/useCases/fetchArticleUseCase.ts +++ b/src/article/useCases/fetchArticleUseCase.ts @@ -1,16 +1,29 @@ import { Article } from "article/domain/articleEntity"; import { ArticleStore } from "article/domain/articleStore"; -const fetchArticleUseCase = async ( - fetchArticleCallback: (id: string) => Promise
, - setArticle: ArticleStore["setArticle"], - id: string, -): Promise
=> { - const article = await fetchArticleCallback(id); - if (article) { - await setArticle(article); - } - return article; -}; -export { fetchArticleUseCase }; +class FetchArticleUseCase { + /* ------------------------------ Dependencies ------------------------------ */ + _fetchArticleCallback: (id: string) => Promise
; + _store: ArticleStore; + /* -------------------------------------------------------------------------- */ + constructor( + fetchArticle: (id: string) => Promise
, + store: ArticleStore, + ) { + this._fetchArticleCallback = fetchArticle; + this._store = store; + } + /* ----------------------------- Implementation ----------------------------- */ + async call(id: string): Promise
{ + return this._fetchArticleCallback(id).then((article) => { + if (article != null) { + this._store.setArticle(article); + } + return article; + }) + } + /* -------------------------------------------------------------------------- */ +} + +export { FetchArticleUseCase }; diff --git a/src/article/useCases/getArticleUseCase.ts b/src/article/useCases/getArticleUseCase.ts index 9a691c2..68894d6 100755 --- a/src/article/useCases/getArticleUseCase.ts +++ b/src/article/useCases/getArticleUseCase.ts @@ -1,16 +1,25 @@ import { Article } from "article/domain/articleEntity"; import type { ArticleStore } from "../domain/articleStore"; -const getArticleUseCase = async ( - getArticle: ArticleStore["getArticle"], - setArticle: ArticleStore["setArticle"], - id: Article["id"] -): Promise
=> { - const article = await getArticle(id); - if (article) { - await setArticle(article); +class GetArticleUseCase { + /* ------------------------------ Dependencies ------------------------------ */ + _store: ArticleStore; + /* -------------------------------------------------------------------------- */ + constructor( + store: ArticleStore, + ) { + this._store = store; } - return article; -}; + /* ----------------------------- Implementation ----------------------------- */ + async call(id: string): Promise
{ + const storedArticle = this._store.getArticle(id); + if (storedArticle != null) { + this._store.setArticle(storedArticle); + return storedArticle; + } + throw new Error('Article not found'); + } + /* -------------------------------------------------------------------------- */ +} -export { getArticleUseCase }; +export { GetArticleUseCase }; diff --git a/src/components/fetchAnArticle/AnArticle.tsx b/src/components/fetchAnArticle/AnArticle.tsx index 5b680b5..98c3e36 100755 --- a/src/components/fetchAnArticle/AnArticle.tsx +++ b/src/components/fetchAnArticle/AnArticle.tsx @@ -10,27 +10,20 @@ import { SVGSearch } from "components/icons"; import BaseLayout from "components/BaseLayout"; import Typography from "components/typography/Typography"; import { useTranslation } from "react-i18next"; -import { fetchArticleUseCase } from "article/useCases/fetchArticleUseCase"; -import { getArticleUseCase } from "article/useCases/getArticleUseCase"; +import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase"; +import { GetArticleUseCase } from "article/useCases/getArticleUseCase"; +import { fetchArticle } from "article/data/articleAPIService"; const AnArticle = () => { const store = useArticleStore(); - const { id } = useParams(); const { article, hasError, shouldShowLoading } = useArticleViewModel( store, - fetchArticleUseCase, - getArticleUseCase, - id ?? '', + new FetchArticleUseCase(fetchArticle, store), + new GetArticleUseCase(store), ); + const { i18n, t } = useTranslation(); - // const { id } = useParams(); - // const newId = `${id}`; - - // useEffect(() => { - // store.getArticle(newId); - // }, [id]); - if (hasError) { return ; } diff --git a/src/components/fetchAnArticle/AnArticleBody.tsx b/src/components/fetchAnArticle/AnArticleBody.tsx index c849a6c..ed9efeb 100755 --- a/src/components/fetchAnArticle/AnArticleBody.tsx +++ b/src/components/fetchAnArticle/AnArticleBody.tsx @@ -13,21 +13,17 @@ import BaseLayout from "components/BaseLayout"; import Container from "components/Container"; import NotFound from "./NotFound"; import Markdown from "components/Markdown"; -import { fetchArticleUseCase } from "article/useCases/fetchArticleUseCase"; -import { getArticleUseCase } from "article/useCases/getArticleUseCase"; +import { FetchArticleUseCase } from "article/useCases/fetchArticleUseCase"; +import { GetArticleUseCase } from "article/useCases/getArticleUseCase"; +import { fetchArticle } from "article/data/articleAPIService"; const AnArticleBody = () => { const store = useArticleStore(); - const { id } = useParams(); const { article, hasError, shouldShowLoading } = useArticleViewModel( store, - fetchArticleUseCase, - getArticleUseCase, - id ?? '', + new FetchArticleUseCase(fetchArticle, store), + new GetArticleUseCase(store), ); - // useEffect(() => { - // store.getArticle(newId); - // }, [id]); if (hasError) ; return (