Skip to content

Commit

Permalink
Merge pull request #3 from atlp-rwanda/chore-Setup-Redux-#187419108
Browse files Browse the repository at this point in the history
Chore: React redux #187419108
  • Loading branch information
teerenzo authored Jun 11, 2024
2 parents e292e86 + b3dd852 commit a8a0113
Show file tree
Hide file tree
Showing 12 changed files with 534 additions and 82 deletions.
280 changes: 216 additions & 64 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.23.1",
"react-redux": "^9.1.2",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^16.0.0"
},
"devDependencies": {
},
"devDependencies": {
"@reduxjs/toolkit": "^2.2.5",
"@types/jest": "^29.5.12",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-redux": "^7.1.33",
"@types/react-router-dom": "^5.3.3",
"@types/redux": "^3.6.0",
"@types/redux-thunk": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
79 changes: 74 additions & 5 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
#root {
max-width: 1440px;
margin: 0 auto;
padding: 2rem;
text-align: center;
/* index.css */

body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
}

.Add-article {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
max-width: 500px;
margin: 20px auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.Add-article input {
width: calc(100% - 20px);
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}

.Add-article button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}

.Add-article button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}

.Article {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 20px;
max-width: 600px;
margin: 20px auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.Article h1 {
font-size: 24px;
margin: 0 0 10px;
}

.Article p {
font-size: 18px;
line-height: 1.5;
margin: 0 0 10px;
}

.Article button {
padding: 10px 20px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
42 changes: 36 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
import AppRoutes from "./routes/AppRoutes";
import * as React from "react";
import { useSelector, shallowEqual, useDispatch } from "react-redux";
import "./App.css";
import { Dispatch } from "redux";

const App = () => (
<div className="w-full min-h-screen">
<AppRoutes />
</div>
);
import { ArticleState, IArticle } from "../type";

import Article from "./components/Article";
import AddArticle from "./components/AddArticle";
import { addArticle, removeArticle } from "./store/actionCreators";

const App: React.FC = () => {
const articles: readonly IArticle[] = useSelector(
(state: ArticleState) => state.articles,
shallowEqual,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispatch: Dispatch<any> = useDispatch();

const saveArticle = React.useCallback(
(article: IArticle) => dispatch(addArticle(article)),
[dispatch],
);

return (
<main>
<AddArticle saveArticle={saveArticle} />
{articles.map((article: IArticle) => (
<Article
key={article.id}
article={article}
removeArticle={removeArticle}
/>
))}
</main>
);
};

export default App;
54 changes: 54 additions & 0 deletions src/components/AddArticle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from "react";

import { IArticle } from "../../type";

type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
saveArticle: (article: IArticle | any) => void;
};

const AddArticle: React.FC<Props> = ({ saveArticle }) => {
// Initialize the state with an empty IArticle object
const [article, setArticle] = React.useState<IArticle>({ title: "", body: "" });

const handleArticleData = (e: React.FormEvent<HTMLInputElement>) => {
const { id, value } = e.currentTarget;
setArticle((prevArticle) => ({
...prevArticle,
[id]: value,
}));
};

const addNewArticle = (e: React.FormEvent) => {
e.preventDefault();
if (article.title && article.body) {
saveArticle(article);
// Reset form after submission
setArticle({ title: "", body: "" });
}
};

return (
<form onSubmit={addNewArticle} className="Add-article">
<input
type="text"
id="title"
placeholder="Title"
value={article.title}
onChange={handleArticleData}
/>
<input
type="text"
id="body"
placeholder="Description"
value={article.body}
onChange={handleArticleData}
/>
<button type="submit" disabled={!article.title || !article.body}>
Add article
</button>
</form>
);
};

export default AddArticle;
33 changes: 33 additions & 0 deletions src/components/Article.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/button-has-type */
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from "react";
import { Dispatch } from "redux";
import { useDispatch } from "react-redux";

import { IArticle } from "../../type";

type Props = {
article: IArticle
removeArticle: (article: IArticle) => void
};

const Article: React.FC<Props> = ({ article, removeArticle }) => {
const dispatch: Dispatch<any> = useDispatch();

const deleteArticle = React.useCallback(
(article: IArticle) => dispatch(removeArticle(article)),
[dispatch, removeArticle],
);

return (
<div className="Article">
<div>
<h1>{article.title}</h1>
<p>{article.body}</p>
</div>
<button onClick={() => deleteArticle(article)}>Delete</button>
</div>
);
};
export default Article;
25 changes: 20 additions & 5 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";

import App from "./App.tsx";
// eslint-disable-next-line import/order
import App from "./App";

import "./index.css";
import { BrowserRouter as Router } from "react-router-dom";
import { createStore, applyMiddleware, Store } from "redux";
import { Provider } from "react-redux";
import { thunk } from "redux-thunk";

import { ArticleState, ArticleAction, DispatchType } from "../type";

import reducer from "./store/reducer";

const store: Store<ArticleState, ArticleAction> & {
dispatch: DispatchType;
} = createStore(reducer, applyMiddleware(thunk));

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Router>
<App />
</Router>
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>
</React.StrictMode>,
);
Empty file added src/reducers/index.ts
Empty file.
32 changes: 32 additions & 0 deletions src/store/actionCreators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ArticleAction, DispatchType, IArticle } from "../../type";

import * as actionTypes from "./actionTypes";

// Generic function to create an action
function createArticleAction(type: string, article: IArticle): ArticleAction {
return {
type,
article,
};
}

// Simulate HTTP request and dispatch action
export function simulateHttpRequest(action: ArticleAction) {
return (dispatch: DispatchType) => {
setTimeout(() => {
dispatch(action);
}, 500);
};
}

// Action creator for adding an article
export function addArticle(article: IArticle) {
const action = createArticleAction(actionTypes.ADD_ARTICLE, article);
return simulateHttpRequest(action);
}

// Action creator for removing an article
export function removeArticle(article: IArticle) {
const action = createArticleAction(actionTypes.REMOVE_ARTICLE, article);
return simulateHttpRequest(action);
}
2 changes: 2 additions & 0 deletions src/store/actionTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ADD_ARTICLE = "ADD_ARTICLE";
export const REMOVE_ARTICLE = "REMOVE_ARTICLE";
42 changes: 42 additions & 0 deletions src/store/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* eslint-disable no-case-declarations */
import { ArticleAction, ArticleState, IArticle } from "../../type";

import * as actionTypes from './actionTypes';

// Define the initial state
const initialState: ArticleState = {
articles: [],
};

const generateUniqueId = () => Math.floor(Math.random() * 1e9);

const reducer = (
// eslint-disable-next-line @typescript-eslint/default-param-last
state: ArticleState = initialState,
action: ArticleAction,
): ArticleState => {
switch (action.type) {
case actionTypes.ADD_ARTICLE:
const newArticle: IArticle = {
id: generateUniqueId(),
title: action.article.title,
body: action.article.body,
};
return {
...state,
articles: state.articles.concat(newArticle),
};
case actionTypes.REMOVE_ARTICLE:
const updatedArticles: IArticle[] = state.articles.filter(
(article) => article.id !== action.article.id,
);
return {
...state,
articles: updatedArticles,
};
default:
return state;
}
};

export default reducer;
16 changes: 16 additions & 0 deletions type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface IArticle {
id?: number
title: string
body: string
}

export type ArticleState = {
articles: IArticle[]
};

export type ArticleAction = {
type: string
article: IArticle
};

export type DispatchType = (args: ArticleAction) => ArticleAction;

0 comments on commit a8a0113

Please sign in to comment.