diff --git a/.eslintignore b/.eslintignore index c71e206..5724ba5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,6 @@ /craco.config.js /__tests__ /babel.config.js +build/ +src/ +*.js \ No newline at end of file diff --git a/src/components/article/ButtonsWIthAccess.tsx b/src/components/article/ButtonsWIthAccess.tsx index 18c4536..5a88ca6 100644 --- a/src/components/article/ButtonsWIthAccess.tsx +++ b/src/components/article/ButtonsWIthAccess.tsx @@ -34,6 +34,14 @@ const ButtonsWIthAccess = ({ articleInfo }: IButtonsWIthAccessProps) => {   Edit Article    + +    diff --git a/src/components/article/Revision.tsx b/src/components/article/Revision.tsx new file mode 100644 index 0000000..13fda60 --- /dev/null +++ b/src/components/article/Revision.tsx @@ -0,0 +1,84 @@ +import { useRevertArticleUpdateMutation } from '@/queries/articles.query'; +import { useGetUserQuery } from '@/queries/user.query'; +import useInputs from '@/lib/hooks/useInputs'; +import queryClient from '@/queries/queryClient'; +import { QUERY_ARTICLE_REVISIONS_KEY, QUERY_COMMENTS_KEY } from '@/constants/query.constant'; +import convertToDate from '@/lib/utils/convertToDate'; +import { IArticleRevision } from '@/interfaces/main'; +import { useLocation, useNavigate } from 'react-router-dom'; + +interface IRevisionProps { + revisionsInfo: { + articlesRevisions: IArticleRevision[]; + articlesRevisionsCount: number; + }; + slug: string; +} + +const Revision = ({ revisionsInfo, slug }: IRevisionProps) => { + const { articlesRevisions } = revisionsInfo; + const revertArticleUpdateMutation = useRevertArticleUpdateMutation(); + const navigate = useNavigate(); + const onRevert = (slug: string, revision: number, newSlug: string) => { + revertArticleUpdateMutation.mutate( + { slug, revision }, + { + onSuccess: (_) => { + queryClient.invalidateQueries({ queryKey: [QUERY_ARTICLE_REVISIONS_KEY] }); + alert('Article reverted successfully!'); + navigate(`/article/${newSlug}`, { state: newSlug }); + }, + }, + ); + }; + return ( +
+

Updates History

+ {articlesRevisions.length === 0 ? ( +
Loading...
+ ) : ( + articlesRevisions?.map((revision, index) => ( +
+
+
Update {revision.id}
+

Date: {convertToDate(revision.createdAt)}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Title{revision.articleData.title}
Slug{revision.articleData.slug}
Body{revision.articleData.body}
Description{revision.articleData.description}
Created At{convertToDate(revision.articleData.createdAt)}
+
+
+ +
+
+ )) + )} +
+ ); +}; + +export default Revision; diff --git a/src/constants/query.constant.ts b/src/constants/query.constant.ts index db8ddc3..ac053a2 100644 --- a/src/constants/query.constant.ts +++ b/src/constants/query.constant.ts @@ -4,3 +4,4 @@ export const QUERY_ARTICLE_KEY = 'article'; export const QUERY_COMMENTS_KEY = 'comments'; export const QUERY_PROFILE_KEY = 'profile'; export const QUERY_TAG_KEY = 'tags'; +export const QUERY_ARTICLE_REVISIONS_KEY = 'revisions'; diff --git a/src/interfaces/main.d.ts b/src/interfaces/main.d.ts index 7ffc461..595b144 100644 --- a/src/interfaces/main.d.ts +++ b/src/interfaces/main.d.ts @@ -16,6 +16,22 @@ export interface IArticle { }; } +export interface IArticleRevision { + id: number; + articleId: number; + createdAt: string; + updatedAt: string; + articleData: { + id: number; + slug: string; + title: string; + description: string; + body: string; + createdAt: string; + updatedAt: string; + }; +} + export interface IComment { id: number; createdAt: string; diff --git a/src/lib/routerMeta.ts b/src/lib/routerMeta.ts index 39856e0..8eca098 100644 --- a/src/lib/routerMeta.ts +++ b/src/lib/routerMeta.ts @@ -42,6 +42,12 @@ const routerMeta: RouterMetaType = { path: '/article/:slug', isShow: false, }, + ArticleRevisionPage: { + name: 'Article Revisions', + path: '/article/:slug/revisions', + isShow: false, + isAuth: true, + }, ProfilePage: { name: 'Profile', path: '/profile/:username/*', @@ -59,6 +65,7 @@ const routerMeta: RouterMetaType = { isShow: true, isAuth: false, }, + NotFoundPage: { path: '/*', isShow: false, diff --git a/src/pages/ArticleRevisionPage.tsx b/src/pages/ArticleRevisionPage.tsx new file mode 100644 index 0000000..3dd1aa7 --- /dev/null +++ b/src/pages/ArticleRevisionPage.tsx @@ -0,0 +1,84 @@ +import { useGetArticleRevisionsQueries, useGetArticleQueries } from '@/queries/articles.query'; +import { Link, useLocation } from 'react-router-dom'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import ButtonSelector from '@/components/article/ButtonSelector'; +import { useContext } from 'react'; +import { UserContext } from '@/contexts/UserContextProvider'; +import Revision from '@/components/article/Revision'; +import routerMeta from '@/lib/routerMeta'; +import convertToDate from '@/lib/utils/convertToDate'; + +const ArticleRevisionPage = () => { + const { state } = useLocation(); + const [revisionInfo] = useGetArticleRevisionsQueries(state.slug); + + const { isLogin } = useContext(UserContext); + console.log(state); + console.log(revisionInfo); + + return ( +
+
+
+

{state.title}

+ +
+ + comment-author + + +
+ + {state.author.username} + + {convertToDate(state.updatedAt)} +
+
+
+
+ +
+
+
+ +
+
+ +
+ +
+
+ + profile + +
+ + {state.author.username} + + {convertToDate(state.updatedAt)} +
+ {/* {isLogin ? : <>} */} +
+
+ +
+
+ {isLogin ? ( + + ) : ( +

+ Sign in +  or  + Sign up +  to add comments on this article. +

+ )} +
+
+
+
+ ); +}; + +export default ArticleRevisionPage; diff --git a/src/queries/articles.query.ts b/src/queries/articles.query.ts index 55eeba4..5786e96 100644 --- a/src/queries/articles.query.ts +++ b/src/queries/articles.query.ts @@ -1,7 +1,14 @@ -import { QUERY_ARTICLES_KEY, QUERY_ARTICLE_KEY, QUERY_COMMENTS_KEY, QUERY_TAG_KEY } from '@/constants/query.constant'; +import { + QUERY_ARTICLES_KEY, + QUERY_ARTICLE_KEY, + QUERY_ARTICLE_REVISIONS_KEY, + QUERY_COMMENTS_KEY, + QUERY_TAG_KEY, +} from '@/constants/query.constant'; import { getArticle, getArticles, + getArticleRevisions, createArticle, updateArticle, deleteArticle, @@ -9,6 +16,7 @@ import { createComment, deleteComment, favoriteArticle, + restoreArticleRevision, unfavoriteArticle, } from '@/repositories/articles/articlesRepository'; import { getTags } from '@/repositories/tags/tagsRepository'; @@ -48,6 +56,18 @@ export const useGetArticleQueries = (slug: string) => { }); }; +export const useGetArticleRevisionsQueries = (slug: string) => { + return useQueries({ + queries: [ + { + queryKey: [QUERY_ARTICLE_KEY, slug], + queryFn: () => getArticleRevisions({ slug }).then((res) => res.data.revisions), + staleTime: 20000, + }, + ], + }); +}; + export const useCreateArticleMutation = () => useMutation(createArticle); export const useUpdateArticleMutation = () => useMutation(updateArticle); @@ -61,3 +81,5 @@ export const useDeleteCommentMutation = () => useMutation(deleteComment); export const useFavoriteArticleMutation = () => useMutation(favoriteArticle); export const useUnfavoriteArticleMutation = () => useMutation(unfavoriteArticle); + +export const useRevertArticleUpdateMutation = () => useMutation(restoreArticleRevision); diff --git a/src/repositories/apiClient.ts b/src/repositories/apiClient.ts index f42976f..2df8ddc 100644 --- a/src/repositories/apiClient.ts +++ b/src/repositories/apiClient.ts @@ -2,7 +2,7 @@ import { ACCESS_TOKEN_KEY } from '@/constants/token.contant'; import token from '@/lib/token'; import axios, { AxiosResponse, InternalAxiosRequestConfig, AxiosError } from 'axios'; -const host = 'https://api.realworld.io/api'; +const host = 'http://127.0.0.1:8000/api'; const apiClient = axios.create({ baseURL: host, @@ -19,7 +19,7 @@ apiClient.interceptors.request.use((request) => { const { method, url } = request; if (jwtToken) { - request.headers['Authorization'] = `Token ${jwtToken}`; + request.headers['Authorization'] = `Bearer ${jwtToken}`; } logOnDev(`🚀 [${method?.toUpperCase()}] ${url} | Request`, request); diff --git a/src/repositories/articles/articlesRepository.param.ts b/src/repositories/articles/articlesRepository.param.ts index d8309ab..5eed55b 100644 --- a/src/repositories/articles/articlesRepository.param.ts +++ b/src/repositories/articles/articlesRepository.param.ts @@ -46,3 +46,12 @@ export interface deleteCommentParam { export interface favoriteParam { slug: string; } + +export interface getArticleRevisionParam { + slug: string; + revision: number; +} + +export interface getArticleRevisionsParam { + slug: string; +} diff --git a/src/repositories/articles/articlesRepository.ts b/src/repositories/articles/articlesRepository.ts index acad4a3..8dccd51 100644 --- a/src/repositories/articles/articlesRepository.ts +++ b/src/repositories/articles/articlesRepository.ts @@ -9,6 +9,8 @@ import { createCommentParam, deleteCommentParam, favoriteParam, + getArticleRevisionsParam, + getArticleRevisionParam, } from './articlesRepository.param'; import { UNIT_PER_PAGE } from '@/constants/units.constants'; @@ -104,3 +106,24 @@ export const unfavoriteArticle = async ({ slug }: favoriteParam) => { url: `/articles/${slug}/favorite`, }); }; + +export const getArticleRevisions = async ({ slug }: getArticleRevisionsParam) => { + return await apiClient({ + method: 'get', + url: `/articles/${slug}/revisions`, + }); +}; + +export const getArticleRevision = async ({ slug, revision }: getArticleRevisionParam) => { + return await apiClient({ + method: 'get', + url: `/articles/${slug}/revisions/${revision}`, + }); +}; + +export const restoreArticleRevision = async ({ slug, revision }: getArticleRevisionParam) => { + return await apiClient({ + method: 'post', + url: `/articles/${slug}/revisions/${revision}/revert`, + }); +};