Skip to content

Commit a8a0113

Browse files
authored
Merge pull request #3 from atlp-rwanda/chore-Setup-Redux-#187419108
Chore: React redux #187419108
2 parents e292e86 + b3dd852 commit a8a0113

12 files changed

+534
-82
lines changed

Diff for: package-lock.json

+216-64
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,23 @@
2121
"react": "^18.2.0",
2222
"react-dom": "^18.2.0",
2323
"react-router-dom": "^6.23.1",
24+
"react-redux": "^9.1.2",
25+
"redux": "^5.0.1",
26+
"redux-thunk": "^3.1.0",
2427
"@testing-library/jest-dom": "^6.4.5",
2528
"@testing-library/react": "^16.0.0"
26-
},
27-
"devDependencies": {
29+
},
30+
"devDependencies": {
31+
"@reduxjs/toolkit": "^2.2.5",
2832
"@types/jest": "^29.5.12",
2933
"@commitlint/cli": "^19.3.0",
3034
"@commitlint/config-conventional": "^19.2.2",
3135
"@types/react": "^18.2.66",
3236
"@types/react-dom": "^18.2.22",
37+
"@types/react-redux": "^7.1.33",
3338
"@types/react-router-dom": "^5.3.3",
39+
"@types/redux": "^3.6.0",
40+
"@types/redux-thunk": "^2.1.0",
3441
"@typescript-eslint/eslint-plugin": "^7.2.0",
3542
"@typescript-eslint/parser": "^7.2.0",
3643
"@vitejs/plugin-react-swc": "^3.5.0",

Diff for: src/App.css

+74-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,75 @@
1-
#root {
2-
max-width: 1440px;
3-
margin: 0 auto;
4-
padding: 2rem;
5-
text-align: center;
1+
/* index.css */
2+
3+
body {
4+
font-family: Arial, sans-serif;
5+
margin: 0;
6+
padding: 0;
7+
background-color: #f8f9fa;
8+
color: #333;
9+
}
10+
11+
.Add-article {
12+
background: #fff;
13+
border: 1px solid #ddd;
14+
border-radius: 5px;
15+
padding: 20px;
16+
max-width: 500px;
17+
margin: 20px auto;
18+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
19+
}
20+
21+
.Add-article input {
22+
width: calc(100% - 20px);
23+
padding: 10px;
24+
margin: 10px 0;
25+
border: 1px solid #ccc;
26+
border-radius: 5px;
27+
font-size: 16px;
28+
}
29+
30+
.Add-article button {
31+
padding: 10px 20px;
32+
background-color: #007bff;
33+
color: white;
34+
border: none;
35+
border-radius: 5px;
36+
cursor: pointer;
37+
font-size: 16px;
38+
margin-top: 10px;
39+
}
40+
41+
.Add-article button:disabled {
42+
background-color: #6c757d;
43+
cursor: not-allowed;
44+
}
45+
46+
.Article {
47+
background: #fff;
48+
border: 1px solid #ddd;
49+
border-radius: 5px;
50+
padding: 20px;
51+
max-width: 600px;
52+
margin: 20px auto;
53+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
54+
}
55+
56+
.Article h1 {
57+
font-size: 24px;
58+
margin: 0 0 10px;
59+
}
60+
61+
.Article p {
62+
font-size: 18px;
63+
line-height: 1.5;
64+
margin: 0 0 10px;
65+
}
66+
67+
.Article button {
68+
padding: 10px 20px;
69+
background-color: #dc3545;
70+
color: white;
71+
border: none;
72+
border-radius: 5px;
73+
cursor: pointer;
74+
font-size: 16px;
675
}

Diff for: src/App.tsx

+36-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
1-
import AppRoutes from "./routes/AppRoutes";
1+
import * as React from "react";
2+
import { useSelector, shallowEqual, useDispatch } from "react-redux";
3+
import "./App.css";
4+
import { Dispatch } from "redux";
25

3-
const App = () => (
4-
<div className="w-full min-h-screen">
5-
<AppRoutes />
6-
</div>
7-
);
6+
import { ArticleState, IArticle } from "../type";
7+
8+
import Article from "./components/Article";
9+
import AddArticle from "./components/AddArticle";
10+
import { addArticle, removeArticle } from "./store/actionCreators";
11+
12+
const App: React.FC = () => {
13+
const articles: readonly IArticle[] = useSelector(
14+
(state: ArticleState) => state.articles,
15+
shallowEqual,
16+
);
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
const dispatch: Dispatch<any> = useDispatch();
19+
20+
const saveArticle = React.useCallback(
21+
(article: IArticle) => dispatch(addArticle(article)),
22+
[dispatch],
23+
);
24+
25+
return (
26+
<main>
27+
<AddArticle saveArticle={saveArticle} />
28+
{articles.map((article: IArticle) => (
29+
<Article
30+
key={article.id}
31+
article={article}
32+
removeArticle={removeArticle}
33+
/>
34+
))}
35+
</main>
36+
);
37+
};
838

939
export default App;

Diff for: src/components/AddArticle.tsx

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as React from "react";
2+
3+
import { IArticle } from "../../type";
4+
5+
type Props = {
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
saveArticle: (article: IArticle | any) => void;
8+
};
9+
10+
const AddArticle: React.FC<Props> = ({ saveArticle }) => {
11+
// Initialize the state with an empty IArticle object
12+
const [article, setArticle] = React.useState<IArticle>({ title: "", body: "" });
13+
14+
const handleArticleData = (e: React.FormEvent<HTMLInputElement>) => {
15+
const { id, value } = e.currentTarget;
16+
setArticle((prevArticle) => ({
17+
...prevArticle,
18+
[id]: value,
19+
}));
20+
};
21+
22+
const addNewArticle = (e: React.FormEvent) => {
23+
e.preventDefault();
24+
if (article.title && article.body) {
25+
saveArticle(article);
26+
// Reset form after submission
27+
setArticle({ title: "", body: "" });
28+
}
29+
};
30+
31+
return (
32+
<form onSubmit={addNewArticle} className="Add-article">
33+
<input
34+
type="text"
35+
id="title"
36+
placeholder="Title"
37+
value={article.title}
38+
onChange={handleArticleData}
39+
/>
40+
<input
41+
type="text"
42+
id="body"
43+
placeholder="Description"
44+
value={article.body}
45+
onChange={handleArticleData}
46+
/>
47+
<button type="submit" disabled={!article.title || !article.body}>
48+
Add article
49+
</button>
50+
</form>
51+
);
52+
};
53+
54+
export default AddArticle;

Diff for: src/components/Article.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/* eslint-disable @typescript-eslint/no-shadow */
2+
/* eslint-disable react/button-has-type */
3+
/* eslint-disable @typescript-eslint/no-explicit-any */
4+
import * as React from "react";
5+
import { Dispatch } from "redux";
6+
import { useDispatch } from "react-redux";
7+
8+
import { IArticle } from "../../type";
9+
10+
type Props = {
11+
article: IArticle
12+
removeArticle: (article: IArticle) => void
13+
};
14+
15+
const Article: React.FC<Props> = ({ article, removeArticle }) => {
16+
const dispatch: Dispatch<any> = useDispatch();
17+
18+
const deleteArticle = React.useCallback(
19+
(article: IArticle) => dispatch(removeArticle(article)),
20+
[dispatch, removeArticle],
21+
);
22+
23+
return (
24+
<div className="Article">
25+
<div>
26+
<h1>{article.title}</h1>
27+
<p>{article.body}</p>
28+
</div>
29+
<button onClick={() => deleteArticle(article)}>Delete</button>
30+
</div>
31+
);
32+
};
33+
export default Article;

Diff for: src/main.tsx

+20-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
import React from "react";
22
import ReactDOM from "react-dom/client";
3-
import { BrowserRouter as Router } from "react-router-dom";
43

5-
import App from "./App.tsx";
4+
// eslint-disable-next-line import/order
5+
import App from "./App";
6+
67
import "./index.css";
8+
import { BrowserRouter as Router } from "react-router-dom";
9+
import { createStore, applyMiddleware, Store } from "redux";
10+
import { Provider } from "react-redux";
11+
import { thunk } from "redux-thunk";
12+
13+
import { ArticleState, ArticleAction, DispatchType } from "../type";
14+
15+
import reducer from "./store/reducer";
16+
17+
const store: Store<ArticleState, ArticleAction> & {
18+
dispatch: DispatchType;
19+
} = createStore(reducer, applyMiddleware(thunk));
720

821
ReactDOM.createRoot(document.getElementById("root")!).render(
922
<React.StrictMode>
10-
<Router>
11-
<App />
12-
</Router>
23+
<Provider store={store}>
24+
<Router>
25+
<App />
26+
</Router>
27+
</Provider>
1328
</React.StrictMode>,
1429
);

Diff for: src/reducers/index.ts

Whitespace-only changes.

Diff for: src/store/actionCreators.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ArticleAction, DispatchType, IArticle } from "../../type";
2+
3+
import * as actionTypes from "./actionTypes";
4+
5+
// Generic function to create an action
6+
function createArticleAction(type: string, article: IArticle): ArticleAction {
7+
return {
8+
type,
9+
article,
10+
};
11+
}
12+
13+
// Simulate HTTP request and dispatch action
14+
export function simulateHttpRequest(action: ArticleAction) {
15+
return (dispatch: DispatchType) => {
16+
setTimeout(() => {
17+
dispatch(action);
18+
}, 500);
19+
};
20+
}
21+
22+
// Action creator for adding an article
23+
export function addArticle(article: IArticle) {
24+
const action = createArticleAction(actionTypes.ADD_ARTICLE, article);
25+
return simulateHttpRequest(action);
26+
}
27+
28+
// Action creator for removing an article
29+
export function removeArticle(article: IArticle) {
30+
const action = createArticleAction(actionTypes.REMOVE_ARTICLE, article);
31+
return simulateHttpRequest(action);
32+
}

Diff for: src/store/actionTypes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const ADD_ARTICLE = "ADD_ARTICLE";
2+
export const REMOVE_ARTICLE = "REMOVE_ARTICLE";

Diff for: src/store/reducer.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-disable no-case-declarations */
2+
import { ArticleAction, ArticleState, IArticle } from "../../type";
3+
4+
import * as actionTypes from './actionTypes';
5+
6+
// Define the initial state
7+
const initialState: ArticleState = {
8+
articles: [],
9+
};
10+
11+
const generateUniqueId = () => Math.floor(Math.random() * 1e9);
12+
13+
const reducer = (
14+
// eslint-disable-next-line @typescript-eslint/default-param-last
15+
state: ArticleState = initialState,
16+
action: ArticleAction,
17+
): ArticleState => {
18+
switch (action.type) {
19+
case actionTypes.ADD_ARTICLE:
20+
const newArticle: IArticle = {
21+
id: generateUniqueId(),
22+
title: action.article.title,
23+
body: action.article.body,
24+
};
25+
return {
26+
...state,
27+
articles: state.articles.concat(newArticle),
28+
};
29+
case actionTypes.REMOVE_ARTICLE:
30+
const updatedArticles: IArticle[] = state.articles.filter(
31+
(article) => article.id !== action.article.id,
32+
);
33+
return {
34+
...state,
35+
articles: updatedArticles,
36+
};
37+
default:
38+
return state;
39+
}
40+
};
41+
42+
export default reducer;

Diff for: type.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface IArticle {
2+
id?: number
3+
title: string
4+
body: string
5+
}
6+
7+
export type ArticleState = {
8+
articles: IArticle[]
9+
};
10+
11+
export type ArticleAction = {
12+
type: string
13+
article: IArticle
14+
};
15+
16+
export type DispatchType = (args: ArticleAction) => ArticleAction;

0 commit comments

Comments
 (0)