From fecce76fe130c2068ce382a8321a94c5f15b9f0f Mon Sep 17 00:00:00 2001 From: danysmall Date: Sat, 12 Nov 2022 23:41:31 +0300 Subject: [PATCH] Improved loading content of and article with saving them in redux store When article was loaded once than it will be cached in redux store. On the next loading of article content it will be get from store --- Dockerfile | 47 ++++++++++++------- scripts/entrypoint.sh | 3 ++ src/article/controller/articleViewModel.ts | 42 ++++++----------- src/article/data/articleAPIService.ts | 4 +- src/article/data/articleActions.ts | 29 +++++++----- src/article/data/articleCommonStateStore.ts | 47 ++++++++++++++----- src/article/data/articleReducer.ts | 7 +-- .../data/articleStoreImplementation.ts | 9 ++-- src/article/domain/articleStore.ts | 7 +-- src/article/useCases/fetchArticleUseCase.ts | 28 ++++++++++- src/article/useCases/getArticleUseCase.ts | 23 +++++++++ src/components/fetchAnArticle/AnArticle.tsx | 19 +++----- .../fetchAnArticle/AnArticleBody.tsx | 14 ++---- 13 files changed, 173 insertions(+), 106 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a98d4f..ec52764 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,43 @@ -# Install dependencies only when needed -FROM node:fermium-alpine AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +# Install dependencies of project +FROM node:fermium-alpine AS dependencies RUN apk add --no-cache libc6-compat WORKDIR /home/app/ -COPY package.json package-lock.json ./ -RUN npm ci +COPY package.json ./ +RUN npm i -# Bundle static assets with nginx -FROM node:fermium-alpine as production - -# Copy built assets from builder +# Build application to bunch of static files +FROM node:fermium-alpine AS builder WORKDIR /home/app/ -COPY --from=deps ./home/app/node_modules ./node_modules +COPY --from=dependencies ./home/app/node_modules ./node_modules COPY . . +RUN npm run build + + +# NGINX image +FROM nginx:1.21.6 as production +# Copy built assets from builder +COPY --from=builder /home/app/build /usr/share/nginx/html +# Add nginx.config +COPY nginx.conf /etc/nginx/conf.d/default.conf +# Copy setup nginx entrypoint file +COPY ./scripts/entrypoint.sh . # Expose ports -EXPOSE 3000 +EXPOSE 80 + +# Execute script +RUN chmod +x ./entrypoint.sh ENV NODE_ENV production -ENV USER_NAME=node_user USER_UID=2000 GROUP_NAME=node_group GROUP_UID=2000 +# ENV USER_NAME=node_user USER_UID=2000 GROUP_NAME=node_group GROUP_UID=2000 -RUN deluser --remove-home node \ - && addgroup --g ${GROUP_UID} -S ${GROUP_NAME} \ - && adduser -D -S -s /sbin/nologin -u ${USER_UID} -G ${GROUP_NAME} ${USER_NAME} -USER "${USER_NAME}" +# RUN deluser --remove-home node \ +# && addgroup --g ${GROUP_UID} -S ${GROUP_NAME} \ +# && adduser -D -S -s /sbin/nologin -u ${USER_UID} -G ${GROUP_NAME} ${USER_NAME} +# USER "${USER_NAME}" +ENTRYPOINT ["./entrypoint.sh"] -ENTRYPOINT [ "npm","start"] \ No newline at end of file +# Start serving +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file 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..c7340c6 100755 --- a/src/article/controller/articleViewModel.ts +++ b/src/article/controller/articleViewModel.ts @@ -1,44 +1,30 @@ 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( + () => { + getArticleUseCase.call(id ?? '').catch((_) => fetchArticleUseCase.call(id ?? '')); + console.log(id); }, - [store.setArticle] + [id] ); - useEffect(() => { - _getArticle(id); - }, [article]); + useEffect(getArticle, []); 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..fdf3cbd 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; + } setLoading(false); - return article; - } catch (error) { - setError(true); - return null; + setCurrentArticle(fromStore); + 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..b5ac846 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,11 +17,11 @@ const articleReducer = ( ): ArticleStoreState => { switch (action.type) { case actionTypes.SET_ARTICLE: - return { ...state, article: action.article }; + return { ...state, articles: action.updatedList, currentArticle: action.article }; 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 }; default: 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..30fb48f 100644 --- a/src/article/useCases/fetchArticleUseCase.ts +++ b/src/article/useCases/fetchArticleUseCase.ts @@ -1,6 +1,32 @@ import { Article } from "article/domain/articleEntity"; import { ArticleStore } from "article/domain/articleStore"; + +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; + }) + } + /* -------------------------------------------------------------------------- */ +} + + const fetchArticleUseCase = async ( fetchArticleCallback: (id: string) => Promise
, setArticle: ArticleStore["setArticle"], @@ -12,5 +38,5 @@ const fetchArticleUseCase = async ( } return article; }; - +export { FetchArticleUseCase }; export { fetchArticleUseCase }; diff --git a/src/article/useCases/getArticleUseCase.ts b/src/article/useCases/getArticleUseCase.ts index 9a691c2..e4d560e 100755 --- a/src/article/useCases/getArticleUseCase.ts +++ b/src/article/useCases/getArticleUseCase.ts @@ -1,6 +1,28 @@ import { Article } from "article/domain/articleEntity"; import type { ArticleStore } from "../domain/articleStore"; +class GetArticleUseCase { + /* ------------------------------ Dependencies ------------------------------ */ + _store: ArticleStore; + /* -------------------------------------------------------------------------- */ + constructor( + store: ArticleStore, + ) { + this._store = store; + } + /* ----------------------------- 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'); + } + /* -------------------------------------------------------------------------- */ +} + + const getArticleUseCase = async ( getArticle: ArticleStore["getArticle"], setArticle: ArticleStore["setArticle"], @@ -13,4 +35,5 @@ const getArticleUseCase = async ( return article; }; +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 (