From 9531f9222142751702d7da9cf6e00d757db787a7 Mon Sep 17 00:00:00 2001 From: Olesya Aldoshyna Date: Sun, 12 Nov 2023 20:36:26 +0200 Subject: [PATCH] added statistics and navigation layout --- package-lock.json | 98 +++++++++++++++++-- package.json | 3 +- src/components/App.css | 44 +++++++++ src/components/App.jsx | 5 +- src/components/Layout/Layout.jsx | 0 src/components/Layout/Layout.styled.js | 31 ++++++ .../StatisticsList/StatisticsList.jsx | 13 +++ .../StatisticsList/StatisticsList.styled.js | 25 +++++ src/components/Tweet/Tweet.jsx | 51 +++++++--- src/index.js | 2 +- src/pages/TweetsPage.jsx | 49 +++++++++- src/pages/TweetsPage.styled.js | 55 ++++++++--- src/utils/backend.js | 6 +- 13 files changed, 342 insertions(+), 40 deletions(-) create mode 100644 src/components/App.css create mode 100644 src/components/Layout/Layout.jsx create mode 100644 src/components/Layout/Layout.styled.js create mode 100644 src/components/StatisticsList/StatisticsList.jsx create mode 100644 src/components/StatisticsList/StatisticsList.styled.js 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 }) => { +