diff --git a/src/article/controller/articleViewModel.ts b/src/article/controller/articleViewModel.ts new file mode 100644 index 0000000..d8a79e1 --- /dev/null +++ b/src/article/controller/articleViewModel.ts @@ -0,0 +1,24 @@ +import type { ArticleStore } from "../domain/articleStore"; +import { getArticleUseCase } from "../useCases/getArticleUseCase"; +import { useCallback, useEffect } from "react"; + +function useArticleViewModel(store: ArticleStore) { + const _getArticle = useCallback( + (id: string) => getArticleUseCase(store.getArticle, store.setArticle, id), + [store.getArticle, store.setArticle] + ); + + useEffect(() => { + if (store.article != undefined) { + _getArticle(store.article.id); + } + }, [store.article?.id]); + + return { + article: store.article, + shouldShowLoading: typeof store.article === "undefined" || store.isLoading, + hasError: store.hasError, + }; +} + +export { useArticleViewModel }; diff --git a/src/article/data/articleAPIService.ts b/src/article/data/articleAPIService.ts new file mode 100644 index 0000000..a217f5b --- /dev/null +++ b/src/article/data/articleAPIService.ts @@ -0,0 +1,35 @@ +import axios from "axios"; +import { Article } from "../domain/articleEntity"; +import { create } from "../domain/articleModel"; +import { FetchArticleByIdDTO } from "./dto/fetch_article_by_id_dto"; +import Failure from "core/failure"; + +async function getArticle(id: string): Promise
{ + try { + // await new Promise((res, _) => { + // setTimeout(() => res(null), 2000); + // }); + const response = await axios.get( + `https://run.mocky.io/v3/62cd4581-d864-4d46-b1d6-02b45b5d1994/${id}` + // `https://jsonplaceholder.typicode.com/posts/${id}` + // `http://scipaper.ru/v1/papers/${id}` + ); + const dto = response.data; + return create({ + id: dto.id, + topic: dto.topic, + title: dto.title, + authors: dto.authors, + tags: dto.tags, + summary: dto.summary, + content: dto.content, + }); + } catch (reason) { + if (axios.isAxiosError(reason)) { + throw Failure.fromReason(reason, "failures.services.load"); + } + throw reason; + } +} + +export { getArticle }; diff --git a/src/article/data/articleActionTypes.ts b/src/article/data/articleActionTypes.ts new file mode 100644 index 0000000..a805584 --- /dev/null +++ b/src/article/data/articleActionTypes.ts @@ -0,0 +1,4 @@ +export const SET_ARTICLE = "SET_ARTICLE"; +export const GET_ARTICLE = "GET_ARTICLE"; +export const GET_ARTICLE_SUCCESS = "GET_ARTICLE.success"; +export const GET_ARTICLE_FAILURE = "GET_ARTICLE.failure"; diff --git a/src/article/data/articleActions.ts b/src/article/data/articleActions.ts new file mode 100644 index 0000000..79f6c7d --- /dev/null +++ b/src/article/data/articleActions.ts @@ -0,0 +1,23 @@ +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 getArticleAction = (id: string) => (dispatch: any) => { + dispatch({ type: actionTypes.GET_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; + }); +}; + +export { setArticleAction, getArticleAction }; diff --git a/src/article/data/articleCommonStateStore.ts b/src/article/data/articleCommonStateStore.ts new file mode 100644 index 0000000..bee210f --- /dev/null +++ b/src/article/data/articleCommonStateStore.ts @@ -0,0 +1,39 @@ +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 dispatch = useDispatch(); + + const getArticle = useCallback( + async (id: string) => { + setLoading(true); + try { + const article = await getArticleAPI(id); + setArticleState(article); + setLoading(false); + return article; + } catch (error) { + setError(true); + return null; + } + }, + [dispatch] + ); + + return { + article: article, + isLoading, + hasError, + setArticle: setArticleState, + getArticle, + }; +}; + +export { useArticleCommonStore }; diff --git a/src/article/data/articleReducer.ts b/src/article/data/articleReducer.ts new file mode 100644 index 0000000..491e8f1 --- /dev/null +++ b/src/article/data/articleReducer.ts @@ -0,0 +1,33 @@ +import { AnyAction } from "@reduxjs/toolkit"; +import { Article } from "article/domain/articleEntity"; +import type { ArticleStore } from "../domain/articleStore"; +import * as actionTypes from "./articleActionTypes"; + +type ArticleStoreState = Omit; + +const INITIAL_STATE: ArticleStoreState = { + article: undefined, + isLoading: false, + hasError: false, +}; + +const articleReducer = ( + state: ArticleStoreState = INITIAL_STATE, + action: AnyAction +): ArticleStoreState => { + switch (action.type) { + case actionTypes.SET_ARTICLE: + return { ...state, article: action.article }; + case actionTypes.GET_ARTICLE: + return { ...state, isLoading: true }; + case actionTypes.GET_ARTICLE_SUCCESS: + return { ...state, isLoading: false, article: action.payload }; + case actionTypes.GET_ARTICLE_FAILURE: + return { ...state, hasError: true, isLoading: false }; + default: + return state; + } +}; + +export { articleReducer }; +export type { ArticleStoreState }; diff --git a/src/article/data/articleStoreImplementation.ts b/src/article/data/articleStoreImplementation.ts new file mode 100644 index 0000000..8d19b97 --- /dev/null +++ b/src/article/data/articleStoreImplementation.ts @@ -0,0 +1,35 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { ArticleStore } from "../domain/articleStore"; +import type { Article } from "../domain/articleEntity"; +import type { ArticleStoreState } from "../data/articleReducer"; +import { getArticleAction, setArticleAction } from "./articleActions"; +import { RootState, useAppSelector } from "store"; + +const articleSelector = (state: RootState): ArticleStoreState => state.article; + +const useArticleStore = (): ArticleStore => { + const { isLoading, article, hasError } = useAppSelector(articleSelector); + + const dispatch = useDispatch(); + + const setArticle = useCallback( + (article: Article) => setArticleAction(article)(dispatch), + [dispatch] + ); + + const getArticle = useCallback( + (id: string) => getArticleAction(id)(dispatch), + [dispatch] + ); + + return { + article: article, + isLoading, + hasError, + setArticle, + getArticle, + }; +}; + +export { useArticleStore }; diff --git a/src/article/data/dto/fetch_article_by_id_dto.ts b/src/article/data/dto/fetch_article_by_id_dto.ts new file mode 100644 index 0000000..bdd867e --- /dev/null +++ b/src/article/data/dto/fetch_article_by_id_dto.ts @@ -0,0 +1,9 @@ +export interface FetchArticleByIdDTO { + id: string; + topic: string; + title: string; + authors: string[]; + tags: string[]; + summary: string; + content: string; +} diff --git a/src/article/domain/articleEntity.ts b/src/article/domain/articleEntity.ts new file mode 100644 index 0000000..69e9520 --- /dev/null +++ b/src/article/domain/articleEntity.ts @@ -0,0 +1,9 @@ +export interface Article { + id: string; + topic: string; + title: string; + authors: string[]; + tags: string[]; + summary: string; + content: string; +} diff --git a/src/article/domain/articleModel.ts b/src/article/domain/articleModel.ts new file mode 100644 index 0000000..ff1f1ac --- /dev/null +++ b/src/article/domain/articleModel.ts @@ -0,0 +1,14 @@ +import { CreateArticleParams } from "article/useCases/params/create_article_params"; +import { Article } from "./articleEntity"; + +const create = (props: CreateArticleParams): Article => ({ + id: props.id, + topic: props.topic, + title: props.title, + authors: props.authors, + tags: props.tags, + summary: props.summary, + content: props.content, +}); + +export { create }; diff --git a/src/article/domain/articleStore.ts b/src/article/domain/articleStore.ts new file mode 100644 index 0000000..ad7e3f3 --- /dev/null +++ b/src/article/domain/articleStore.ts @@ -0,0 +1,14 @@ +import type { Article } from "./articleEntity"; + +interface ArticleStore { + // State + article: Article | undefined; + isLoading: boolean; + hasError: boolean; + + // Actions + setArticle(article?: Article): void; + getArticle(identifier: string): Promise
; +} + +export type { ArticleStore }; diff --git a/src/article/useCases/getArticleUseCase.ts b/src/article/useCases/getArticleUseCase.ts new file mode 100644 index 0000000..9a691c2 --- /dev/null +++ b/src/article/useCases/getArticleUseCase.ts @@ -0,0 +1,16 @@ +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); + } + return article; +}; + +export { getArticleUseCase }; diff --git a/src/article/useCases/params/create_article_params.ts b/src/article/useCases/params/create_article_params.ts new file mode 100644 index 0000000..49b5545 --- /dev/null +++ b/src/article/useCases/params/create_article_params.ts @@ -0,0 +1,9 @@ +export interface CreateArticleParams { + id: string; + topic: string; + title: string; + authors: string[]; + tags: string[]; + summary: string; + content: string; +}