diff --git a/package-lock.json b/package-lock.json
index b109cef..2724b72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,9 +13,12 @@
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
+ "axios": "^1.3.6",
+ "numeral": "^2.0.6",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.13.8",
"styled-components": "^6.0.7",
"web-vitals": "^2.1.3"
}
@@ -4298,6 +4301,29 @@
"node": ">=12"
}
},
+ "node_modules/axios": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
+ "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -7260,9 +7286,9 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
},
"node_modules/follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
@@ -9898,6 +9924,14 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/numeral": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
+ "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
@@ -11633,6 +11667,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -11929,6 +11968,15 @@
}
}
},
+ "node_modules/react-spinners": {
+ "version": "0.13.8",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
+ "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@@ -17680,6 +17728,28 @@
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.2.tgz",
"integrity": "sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA=="
},
+ "axios": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
+ "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
+ "requires": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ }
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -19889,9 +19959,9 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
},
"follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
},
"fork-ts-checker-webpack-plugin": {
"version": "6.5.2",
@@ -21783,6 +21853,11 @@
"boolbase": "^1.0.0"
}
},
+ "numeral": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz",
+ "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="
+ },
"nwsapi": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
@@ -22897,6 +22972,11 @@
}
}
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -23117,6 +23197,12 @@
"workbox-webpack-plugin": "^6.4.1"
}
},
+ "react-spinners": {
+ "version": "0.13.8",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
+ "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
+ "requires": {}
+ },
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
diff --git a/package.json b/package.json
index e8bee81..c4e80c2 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,11 @@
"@testing-library/user-event": "^13.5.0",
"axios": "^1.3.6",
"numeral": "^2.0.6",
- "styled-components": "^6.0.7",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.13.8",
+ "styled-components": "^6.0.7",
"web-vitals": "^2.1.3"
},
"scripts": {
diff --git a/src/components/App.css b/src/components/App.css
new file mode 100644
index 0000000..f287d62
--- /dev/null
+++ b/src/components/App.css
@@ -0,0 +1,44 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
\ No newline at end of file
diff --git a/src/components/App.jsx b/src/components/App.jsx
index 87ee8aa..3291392 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -1,7 +1,8 @@
// import TweetsPage from 'pages/TweetsPage';
+import './App.css';
import Tweet from './Tweet/Tweet';
-export const App = () => {
+const App = () => {
return (
{/* */}
@@ -9,3 +10,5 @@ export const App = () => {
);
};
+
+export default App;
diff --git a/src/components/Layout/Layout.jsx b/src/components/Layout/Layout.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/Layout/Layout.styled.js b/src/components/Layout/Layout.styled.js
new file mode 100644
index 0000000..4b09935
--- /dev/null
+++ b/src/components/Layout/Layout.styled.js
@@ -0,0 +1,31 @@
+import { NavLink } from 'react-router-dom';
+import styled from '@emotion/styled';
+
+export const AppNav = styled.nav`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #333;
+ color: #fff;
+ padding: 10px 20px;
+ width: 1280px;
+`;
+
+export const NavLinks = styled.ul`
+ display: flex;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+`;
+
+export const NavItem = styled.li`
+ margin-right: 20px;
+`;
+
+export const NavLinkStyled = styled(NavLink)`
+ color: #fff;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+`;
diff --git a/src/components/StatisticsList/StatisticsList.jsx b/src/components/StatisticsList/StatisticsList.jsx
new file mode 100644
index 0000000..ac8cf35
--- /dev/null
+++ b/src/components/StatisticsList/StatisticsList.jsx
@@ -0,0 +1,13 @@
+import { List, ListItem } from './StatisticsList.styled';
+import numeral from 'numeral';
+
+const StatisticsList = ({ tweets, followers }) => {
+ return (
+
+ {numeral(tweets).format('0, 0')} tweets
+ {numeral(followers).format('0, 0')} followers
+
+ );
+};
+
+export default StatisticsList;
diff --git a/src/components/StatisticsList/StatisticsList.styled.js b/src/components/StatisticsList/StatisticsList.styled.js
new file mode 100644
index 0000000..8226b8e
--- /dev/null
+++ b/src/components/StatisticsList/StatisticsList.styled.js
@@ -0,0 +1,25 @@
+import styled from "@emotion/styled";
+
+export const List = styled.ul`
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ top: 284px;
+ width: 100%;
+ gap: 16px;
+`;
+
+export const ListItem = styled.li`
+ font-family: "Montserrat", sans-serif;
+ font-style: normal;
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 24px;
+ text-transform: uppercase;
+ color: #ebd8ff;
+`;
\ No newline at end of file
diff --git a/src/components/Tweet/Tweet.jsx b/src/components/Tweet/Tweet.jsx
index 163498f..1ce8d32 100644
--- a/src/components/Tweet/Tweet.jsx
+++ b/src/components/Tweet/Tweet.jsx
@@ -1,21 +1,47 @@
import { useState } from 'react';
import Avatar from '../Avatar/Avatar';
import Button from '../Button/Button';
+import { updateFollowers } from '../../utils/backend';
import { CardContainer, CardImage, Line, Logo } from './Tweet.styled';
+import StatisticsList from 'components/StatisticsList/StatisticsList';
const Tweet = ({ imageURL, folowers, tweets, id }) => {
- const [isActive, setIsActive] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
+ const TOKEN = 'followingsList';
+ const [isLoading, setIsLoading] = useState(false);
+ const [currentFollowers, setCurrentFollowers] = useState(folowers);
+ const [isActive, setIsActive] = useState(
+ JSON.parse(localStorage.getItem(TOKEN)) !== null &&
+ JSON.parse(localStorage.getItem(TOKEN)).find(user => user === id)
+ ? false
+ : true
+ );
+
+ const handleClick = async () => {
+ if (isActive) {
+ setIsLoading(true);
+ const storedData = JSON.parse(localStorage.getItem(TOKEN)) || [];
+ const updatedData = storedData.includes(id)
+ ? storedData.filter(user => user !== id)
+ : [...storedData, id];
+
+ localStorage.setItem(TOKEN, JSON.stringify(updatedData));
+ const { followers } = await updateFollowers(id, 'decrement');
+ setCurrentFollowers(followers);
+ setIsActive(false);
+ setIsLoading(false);
+ }
+ if (!isActive) {
+ setIsLoading(true);
+ const followingsList = JSON.parse(localStorage.getItem(TOKEN)) || [];
+ const updatedFollowingsList = followingsList.filter(user => user !== id);
- const handleClick = () => {};
- if (isActive) {
- setIsLoading(true);
- setIsActive(false);
- }
- // if (!isActive) {
- // setIsLoading(!isActive);
- // setIsActive(false);
- // }
+ localStorage.setItem(TOKEN, JSON.stringify(updatedFollowingsList));
+ const { followers } = await updateFollowers(id, 'decrement');
+ setCurrentFollowers(followers);
+ setIsActive(!isActive);
+ setIsLoading(false);
+ }
+ };
return (
<>
@@ -23,10 +49,11 @@ const Tweet = ({ imageURL, folowers, tweets, id }) => {
+
>
diff --git a/src/index.js b/src/index.js
index ea35e2d..42ad3dc 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
-import { App } from './components/App';
+import App from './components/App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render();
diff --git a/src/pages/TweetsPage.jsx b/src/pages/TweetsPage.jsx
index 1a3d9a9..1147d26 100644
--- a/src/pages/TweetsPage.jsx
+++ b/src/pages/TweetsPage.jsx
@@ -1,14 +1,61 @@
import UserList from 'components/UserList/UserList';
-import { useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+import { getUsers } from 'utils/backend';
+import { LoadBtn } from './TweetsPage.styled';
+import { RingLoader } from 'react-spinners';
const TweetsPage = () => {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
+ useEffect(() => {
+ const fetchUsers = async () => {
+ try {
+ setIsLoading(true);
+ const newUsers = await getUsers(page);
+ setUsers(prevUsers => [...prevUsers, ...newUsers]);
+ setIsLoading(false);
+ } catch (err) {
+ console.log('error:', err.message);
+ }
+ };
+ fetchUsers();
+ }, [page]);
+
+ const newPages = useMemo(() => {
+ const totalUsers = 20;
+ const selectedUsers = users.length;
+ return totalUsers > selectedUsers;
+ }, [users]);
+
+ const handlePage = () => {
+ setPage(prev => prev + 1);
+ };
+ // const location = useLocation();
+ // const backLinkHref = useRef(location.state?.from ?? `/`);
+
return (
<>
+ {newPages && users.length !== 0 && (
+
+ {!isLoading ? 'Load more' : '...'}
+
+ )}
+ {users.length === 0 && (
+
+ )}
>
);
};
diff --git a/src/pages/TweetsPage.styled.js b/src/pages/TweetsPage.styled.js
index e4fc0e5..a82efb0 100644
--- a/src/pages/TweetsPage.styled.js
+++ b/src/pages/TweetsPage.styled.js
@@ -1,18 +1,43 @@
import styled from 'styled-components';
-export const Wrapper = styled.div`
- width: 100%;
- height: 100%;
- padding-top: 168px;
- padding-bottom: 268px;
- padding-left: 435px;
- padding-right: 459px;
- background: white;
- justify-content:
- flex-start;
- align-items:
- flex-start;
- gap: 48px;
- display:
- inline-flex;
+// export const Wrapper = styled.div`
+// width: 100%;
+// height: 100%;
+// padding-top: 168px;
+// padding-bottom: 268px;
+// padding-left: 435px;
+// padding-right: 459px;
+// background: white;
+// justify-content:
+// flex-start;
+// align-items:
+// flex-start;
+// gap: 48px;
+// display:
+// inline-flex;
+// `;
+
+export const LoadBtn = styled.button`
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ padding: 14px 28px;
+ gap: 6px;
+ text-transform: uppercase;
+ width: 196px;
+ height: 50px;
+ margin: 20px auto;
+
+ background: #5cd3a8;
+ box-shadow: 0px 3.43693px 3.43693px rgba(0, 0, 0, 0.25);
+ border-radius: 10.3108px;
+
+ font-family: "Montserrat";
+ font-style: normal;
+ font-weight: 600;
+ font-size: 18px;
+ line-height: 22px;
+
+ text-transform: uppercase;
`;
\ No newline at end of file
diff --git a/src/utils/backend.js b/src/utils/backend.js
index 5cc04f8..1708c76 100644
--- a/src/utils/backend.js
+++ b/src/utils/backend.js
@@ -7,15 +7,15 @@ export const getUsers = async (page) => {
return response.data;
}
-export async function updateFolowers(userId, action) {
+export async function updateFollowers(userId, action) {
const url = `${BASE_URL}/users/${userId}`;
try {
const response = await axios.get(url);
const user = response.data;
- const updateFolowers = action === "increment" ? user.folowers + 1 : user.folowers - 1;
- const data = await axios.put(url, { ...user, folowers: updateFolowers });
+ const updatedFollowers = action === "increment" ? user.followers + 1 : user.followers - 1;
+ const data = await axios.put(url, { ...user, followers: updatedFollowers });
return data.data;
} catch (err) {
console.error(err);