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 (