Commit 276daafc authored by Ken's avatar Ken

feat: add news filter by category and abort signal for api

parent 6cf25b38
Pipeline #19395 canceled with stages
import { IPagination } from "pages/interface";
export interface BaseAPI {
signal?: AbortSignal;
}
export interface NewsAPI extends BaseAPI {
params: IPagination;
}
import { IPagination } from "pages/interface";
import axiosClient from "./axiosClient"; import axiosClient from "./axiosClient";
import { NewsAPI } from "./model";
const newsApi = { const newsApi = {
getNews: (params: IPagination) => { getNews: ({ params, signal }: NewsAPI) => {
const url = "/api/news"; const url = "/api/news";
return axiosClient.get(url, { params }); return axiosClient.get(url, { params, signal });
}, },
getNewsDetail: (id: string) => { getNewsDetail: (id: string) => {
const url = `/api/news/${id}`; const url = `/api/news/${id}`;
......
...@@ -10,15 +10,14 @@ import { useAppDispatch, useAppSelector } from "app/hooks"; ...@@ -10,15 +10,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";
import HeaderLogo from "./HeaderLogo"; import HeaderLogo from "./HeaderLogo";
import { handleSetFilter } from "pages/home/homePageSlice";
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));
...@@ -89,7 +88,7 @@ export default function Header() { ...@@ -89,7 +88,7 @@ export default function Header() {
> >
{categoryData.map((item) => ( {categoryData.map((item) => (
<Box <Box
onClick={() => navigate(item.link)} onClick={() => dispatch(handleSetFilter(item.link))}
component="li" component="li"
key={item.link} key={item.link}
sx={{ sx={{
......
...@@ -20,7 +20,8 @@ ...@@ -20,7 +20,8 @@
width: 50%; width: 50%;
min-height: 288px; min-height: 288px;
img { img,
video {
border: 0; border: 0;
display: block; display: block;
} }
...@@ -55,7 +56,8 @@ ...@@ -55,7 +56,8 @@
&-img { &-img {
width: 100%; width: 100%;
img { img,
video {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
......
...@@ -4,6 +4,7 @@ import { INewspaper } from "pages/interface"; ...@@ -4,6 +4,7 @@ import { INewspaper } from "pages/interface";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useWindowSize } from "usehooks-ts"; import { useWindowSize } from "usehooks-ts";
import { checkUrlLink } from "utils/helpers/checkUrlLink";
type Props = { type Props = {
data: INewspaper; data: INewspaper;
...@@ -32,7 +33,13 @@ const Newspaper = (props: Props) => { ...@@ -32,7 +33,13 @@ const Newspaper = (props: Props) => {
onClick={() => navigate(`${PageUrl.NEWS_ROOT}/${id}`)} onClick={() => navigate(`${PageUrl.NEWS_ROOT}/${id}`)}
> >
<div className="newspaper-img"> <div className="newspaper-img">
<img src={image} alt={title} /> {checkUrlLink(image) === "image" ? (
<img src={image} alt={title} />
) : (
<video autoPlay muted loop>
<source src={image} type="video/mp4" />
</video>
)}
</div> </div>
<div className="newspaper-content"> <div className="newspaper-content">
......
...@@ -10,7 +10,10 @@ import { ...@@ -10,7 +10,10 @@ import {
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { ICategory } from "components/interface"; import { ICategory } from "components/interface";
import { handleSetFilter } from "pages/home/homePageSlice";
import { useAppDispatch } from "app/hooks";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { PageUrl } from "configuration/enum";
type Props = { type Props = {
handleDrawerToggle: () => void; handleDrawerToggle: () => void;
...@@ -20,8 +23,13 @@ type Props = { ...@@ -20,8 +23,13 @@ type Props = {
const drawerWidth = 240; const drawerWidth = 240;
const Sidenav = (props: Props) => { const Sidenav = (props: Props) => {
const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { navItems, handleDrawerToggle, mobileOpen } = props; const { navItems, handleDrawerToggle, mobileOpen } = props;
const handleItemClick = (link: string) => {
navigate(PageUrl.HOMEPAGE);
dispatch(handleSetFilter(link));
};
return ( return (
<Box component="nav"> <Box component="nav">
...@@ -49,7 +57,7 @@ const Sidenav = (props: Props) => { ...@@ -49,7 +57,7 @@ const Sidenav = (props: Props) => {
<ListItem <ListItem
key={item.link} key={item.link}
disablePadding disablePadding
onClick={() => navigate(item.link)} onClick={() => handleItemClick(item.link)}
> >
<ListItemButton sx={{ textAlign: "center" }}> <ListItemButton sx={{ textAlign: "center" }}>
<ListItemText primary={item.label} /> <ListItemText primary={item.label} />
......
...@@ -5,3 +5,7 @@ export const enum PageUrl { ...@@ -5,3 +5,7 @@ export const enum PageUrl {
NEWS_ROOT = "/newspaper", NEWS_ROOT = "/newspaper",
ALL = "*", ALL = "*",
} }
export const enum PageSearchParams {
FILTER = "filterBy",
}
import React from "react";
const NewsByCategory = () => {
return <div>NewsByCategory</div>;
};
export default NewsByCategory;
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { NewsAPI } from "api/model";
import newsApi from "api/newsApi"; import newsApi from "api/newsApi";
import { INewspaper, IPagination } from "pages/interface"; import { INewspaper } from "pages/interface";
const initialState: { const initialState: {
newsData: INewspaper[]; newsData: INewspaper[];
currentPage: number; currentPage: number;
isMaxPage: boolean; isMaxPage: boolean;
currentFilter: string;
} = { } = {
newsData: [], newsData: [],
currentPage: 1, currentPage: 1,
isMaxPage: false, isMaxPage: false,
currentFilter: "",
}; };
export const getNews = createAsyncThunk( export const getNews = createAsyncThunk("home/news", async (data: NewsAPI) => {
"home/news", const res = await newsApi.getNews(data);
async (params: IPagination) => { return res;
const res = await newsApi.getNews(params); });
return res;
}
);
const home = createSlice({ const home = createSlice({
name: "home", name: "home",
initialState, initialState,
reducers: { reducers: {
handleResetNews: (state) => { handleResetNews: (state) => {
return initialState; return {
...initialState,
currentFilter: state.currentFilter,
};
}, },
handlePage: (state, action) => { handlePage: (state, action) => {
state.currentPage += action.payload; state.currentPage += action.payload;
...@@ -33,6 +36,13 @@ const home = createSlice({ ...@@ -33,6 +36,13 @@ const home = createSlice({
handleMaxPage: (state) => { handleMaxPage: (state) => {
state.isMaxPage = true; state.isMaxPage = true;
}, },
handleSetFilter: (state, action) => {
const filterValue = action.payload;
if (filterValue === "/") {
state.currentFilter = "";
} else state.currentFilter = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getNews.fulfilled, (state, action: PayloadAction<any>) => { builder.addCase(getNews.fulfilled, (state, action: PayloadAction<any>) => {
...@@ -46,5 +56,6 @@ const home = createSlice({ ...@@ -46,5 +56,6 @@ const home = createSlice({
}); });
const { reducer, actions } = home; const { reducer, actions } = home;
export const { handleResetNews, handlePage, handleMaxPage } = actions; export const { handleResetNews, handlePage, handleMaxPage, handleSetFilter } =
actions;
export default reducer; export default reducer;
...@@ -15,7 +15,8 @@ import { CircularProgress } from "@mui/material"; ...@@ -15,7 +15,8 @@ import { CircularProgress } from "@mui/material";
const HomePage = () => { const HomePage = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { newsData, currentPage, isMaxPage } = useAppSelector(homeSelector); const { newsData, currentPage, isMaxPage, currentFilter } =
useAppSelector(homeSelector);
const { ref, inView } = useInView({ const { ref, inView } = useInView({
threshold: 0.7, threshold: 0.7,
initialInView: false, initialInView: false,
...@@ -24,18 +25,23 @@ const HomePage = () => { ...@@ -24,18 +25,23 @@ const HomePage = () => {
const [isLocalLoading, setIsLocalLoading] = useState<boolean>(false); const [isLocalLoading, setIsLocalLoading] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
try { try {
if (!isMaxPage) { if (!isMaxPage) {
setIsLocalLoading(true); setIsLocalLoading(true);
setTimeout(() => { setTimeout(() => {
const newsParams: IPagination = { const newsParams: IPagination = {
Filters: "", Filters: currentFilter ? `categorylink==${currentFilter}` : "",
Sorts: "", Sorts: "-CreatedAt",
Page: currentPage, Page: currentPage,
PageSize: 10, PageSize: 10,
}; };
const fetchData = async () => { const fetchData = async () => {
const res: any = await dispatch(getNews(newsParams)).unwrap(); const res: any = await dispatch(
getNews({ params: newsParams, signal })
).unwrap();
if (res.data.collection.length === 0) { if (res.data.collection.length === 0) {
dispatch(handleMaxPage()); dispatch(handleMaxPage());
...@@ -51,8 +57,10 @@ const HomePage = () => { ...@@ -51,8 +57,10 @@ const HomePage = () => {
setIsLocalLoading(false); setIsLocalLoading(false);
} }
return () => controller.abort();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPage]); }, [currentPage, currentFilter]);
useEffect(() => { useEffect(() => {
if (inView && !isMaxPage) { if (inView && !isMaxPage) {
...@@ -64,7 +72,7 @@ const HomePage = () => { ...@@ -64,7 +72,7 @@ const HomePage = () => {
useEffect(() => { useEffect(() => {
dispatch(handleResetNews()); dispatch(handleResetNews());
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [currentFilter]);
return ( return (
<main className="homePage"> <main className="homePage">
......
export const checkUrlLink = (url: string) => {
if (url.endsWith("mp4")) return "video";
return "image";
};
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