Commit 82cb0104 authored by Ken's avatar Ken

feat: add news detail page

parent daaa08d2
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^5.2.2", "bootstrap": "^5.2.2",
"dompurify": "^2.4.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"react": "^18.2.0", "react": "^18.2.0",
...@@ -32,6 +33,9 @@ ...@@ -32,6 +33,9 @@
"sass": "^1.55.0", "sass": "^1.55.0",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/dompurify": "^2.4.0"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
...@@ -4201,6 +4205,15 @@ ...@@ -4201,6 +4205,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/dompurify": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz",
"integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==",
"dev": true,
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.4.6", "version": "8.4.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
...@@ -6948,6 +6961,11 @@ ...@@ -6948,6 +6961,11 @@
} }
] ]
}, },
"node_modules/dompurify": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
"integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^5.2.2", "bootstrap": "^5.2.2",
"dompurify": "^2.4.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"react": "^18.2.0", "react": "^18.2.0",
...@@ -51,5 +52,8 @@ ...@@ -51,5 +52,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@types/dompurify": "^2.4.0"
} }
} }
import axiosClient from "./axiosClient"; import axiosClient from "./axiosClient";
const homeApi = { const newsApi = {
getNews: () => { getNews: () => {
const url = "/api/news"; const url = "/api/news";
return axiosClient.get(url); return axiosClient.get(url);
}, },
getNewsDetail: (id: string) => {
const url = `/api/news/${id}`;
return axiosClient.get(url);
},
}; };
export default homeApi; export default newsApi;
...@@ -5,3 +5,5 @@ export const homeSelector = (state: RootState) => state.home; ...@@ -5,3 +5,5 @@ export const homeSelector = (state: RootState) => state.home;
export const globalSelector = (state: RootState) => state.global; export const globalSelector = (state: RootState) => state.global;
export const headerSelector = (state: RootState) => state.header; export const headerSelector = (state: RootState) => state.header;
export const newsDetailSelector = (state: RootState) => state.newsDetail;
...@@ -2,11 +2,13 @@ import { configureStore } from "@reduxjs/toolkit"; ...@@ -2,11 +2,13 @@ import { configureStore } from "@reduxjs/toolkit";
import headerReducer from "components/Header/headerSlice"; import headerReducer from "components/Header/headerSlice";
import globalReducer from "./globalSlice"; import globalReducer from "./globalSlice";
import homeReducer from "pages/home/homePageSlice"; import homeReducer from "pages/home/homePageSlice";
import newsDetailReducer from "pages/NewsDetail/newsDetailSlice";
const rootReducer = { const rootReducer = {
header: headerReducer, header: headerReducer,
global: globalReducer, global: globalReducer,
home: homeReducer, home: homeReducer,
newsDetail: newsDetailReducer,
}; };
const store = configureStore({ reducer: rootReducer }); const store = configureStore({ reducer: rootReducer });
......
...@@ -12,12 +12,14 @@ import { useAppDispatch, useAppSelector } from "app/hooks"; ...@@ -12,12 +12,14 @@ import { useAppDispatch, useAppSelector } from "app/hooks";
import { getCategories } from "./headerSlice"; import { getCategories } from "./headerSlice";
import { handleLoading } from "app/globalSlice"; import { handleLoading } from "app/globalSlice";
import { headerSelector } from "app/selectors"; import { headerSelector } from "app/selectors";
import { useNavigate } from "react-router-dom";
export default function Header() { export default function Header() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const headerRef = useRef<HTMLElement>(null); const headerRef = useRef<HTMLElement>(null);
const [mobileOpen, setMobileOpen] = useState<boolean>(false); const [mobileOpen, setMobileOpen] = useState<boolean>(false);
const { data: categoryData } = useAppSelector(headerSelector); const { data: categoryData } = useAppSelector(headerSelector);
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
dispatch(handleLoading(true)); dispatch(handleLoading(true));
...@@ -86,6 +88,7 @@ export default function Header() { ...@@ -86,6 +88,7 @@ export default function Header() {
> >
{categoryData.map((item) => ( {categoryData.map((item) => (
<Box <Box
onClick={() => navigate(item.link)}
component="li" component="li"
key={item.link} key={item.link}
sx={{ sx={{
......
import { PageUrl } from "configuration/enum";
import moment from "moment"; import moment from "moment";
import { INewspaper } from "pages/interface"; import { INewspaper } from "pages/interface";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom";
type Props = { type Props = {
data: INewspaper; data: INewspaper;
...@@ -15,11 +17,16 @@ const randomTags = () => { ...@@ -15,11 +17,16 @@ const randomTags = () => {
const Newspaper = (props: Props) => { const Newspaper = (props: Props) => {
const { data, firstNews = false } = props; const { data, firstNews = false } = props;
const { image, title, createdAt, categorylinkNavigation, description } = data; const { image, title, createdAt, categorylinkNavigation, description, id } =
data;
const { label } = categorylinkNavigation; const { label } = categorylinkNavigation;
const navigate = useNavigate();
return ( return (
<div className={`newspaper ${firstNews ? "newspaper-first" : ""}`}> <div
className={`newspaper ${firstNews ? "newspaper-first" : ""}`}
onClick={() => navigate(`${PageUrl.NEWS_ROOT}/${id}`)}
>
<div className="newspaper-img"> <div className="newspaper-img">
<img src={image} alt={title} /> <img src={image} alt={title} />
</div> </div>
......
...@@ -2,5 +2,6 @@ export const enum PageUrl { ...@@ -2,5 +2,6 @@ export const enum PageUrl {
ROOT = "/", ROOT = "/",
HOMEPAGE = "/home", HOMEPAGE = "/home",
NEWS_DETAIL = "/newspaper/:id", NEWS_DETAIL = "/newspaper/:id",
NEWS_ROOT = "/newspaper",
ALL = "*", ALL = "*",
} }
import React from "react"; import { handleLoading } from "app/globalSlice";
import { useAppDispatch, useAppSelector } from "app/hooks";
import { newsDetailSelector } from "app/selectors";
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { purifyHTML } from "utils/helpers/purifyHTML";
import { getNewsDetail } from "./newsDetailSlice";
const NewsDetail = () => { const NewsDetail = () => {
return <div>NewsDetail</div>; const dispatch = useAppDispatch();
const { newsDetail } = useAppSelector(newsDetailSelector);
const { id } = useParams();
console.log(newsDetail);
useEffect(() => {
dispatch(handleLoading(true));
try {
if (id) {
const fetchData = async () => {
await dispatch(getNewsDetail(id));
};
fetchData();
}
} catch (err) {
console.error("ERROR: ", err);
} finally {
dispatch(handleLoading(false));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return (
<>
{newsDetail ? (
<div
dangerouslySetInnerHTML={{ __html: purifyHTML(newsDetail?.content) }}
></div>
) : (
<div></div>
)}
</>
);
}; };
export default NewsDetail; export default NewsDetail;
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import newsApi from "api/newsApi";
import { INewspaper } from "pages/interface";
const initialState: { newsDetail?: INewspaper } = {};
export const getNewsDetail = createAsyncThunk(
"news/detail",
async (id: string) => {
const res = await newsApi.getNewsDetail(id);
return res;
}
);
const news = createSlice({
name: "news",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(
getNewsDetail.fulfilled,
(state, action: PayloadAction<any>) => {
state.newsDetail = action.payload.data;
}
);
},
});
const { reducer } = news;
export default reducer;
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import homeApi from "api/homeApi"; import newsApi from "api/newsApi";
import { INewspaper } from "pages/interface"; import { INewspaper } from "pages/interface";
const initialState: { newsData: INewspaper[] } = { const initialState: { newsData: INewspaper[] } = {
...@@ -7,7 +7,7 @@ const initialState: { newsData: INewspaper[] } = { ...@@ -7,7 +7,7 @@ const initialState: { newsData: INewspaper[] } = {
}; };
export const getNews = createAsyncThunk("home/news", async () => { export const getNews = createAsyncThunk("home/news", async () => {
const res = await homeApi.getNews(); const res = await newsApi.getNews();
return res; return res;
}); });
......
import DOMPurify from "dompurify";
export const purifyHTML = (htmlString: string) =>
DOMPurify.sanitize(htmlString);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment