├── .gitignore ├── package-lock.json ├── package.json ├── public ├── img │ ├── auth-bg.png │ ├── covers │ │ ├── adamwathan.jpg │ │ ├── javalaves.jpg │ │ ├── siavash.jpg │ │ └── soh3il.jpg │ └── users │ │ ├── adamwathan.jpg │ │ ├── dan_abramov.jpg │ │ ├── guillermo_rauch.jpg │ │ ├── javalaves.jpg │ │ ├── neysidev.jpg │ │ ├── not_found.jpg │ │ ├── siavash.jpg │ │ └── soh3il.jpg └── index.html ├── src ├── assets │ └── images │ │ ├── logo.svg │ │ └── profile.jpg ├── components │ ├── Common │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── Navigation.tsx │ │ ├── Tweet.tsx │ │ ├── TwitterBox.tsx │ │ ├── TwitterButton.tsx │ │ ├── TwitterCard.tsx │ │ ├── TwitterContainer.tsx │ │ ├── TwitterFullscreen.tsx │ │ └── TwitterSpinner.tsx │ ├── Core │ │ ├── ProfileActions.tsx │ │ ├── TrendsForYou.tsx │ │ ├── UserActions.tsx │ │ ├── UserInfo.tsx │ │ └── YouShouldFollow.tsx │ ├── Forms │ │ └── EditProfileForm.tsx │ ├── Home │ │ └── WhatsHappening.tsx │ └── Skeleton │ │ └── TweetSkeleton.tsx ├── config │ └── axios.ts ├── constants │ ├── hashtags.tsx │ ├── navigation.tsx │ └── tweets.tsx ├── containers │ ├── App.tsx │ ├── EditProfile.tsx │ ├── Home.tsx │ ├── Login.tsx │ ├── Logout.tsx │ ├── Notifications.tsx │ ├── Profile.tsx │ ├── Register.tsx │ └── User.tsx ├── helpers │ └── button-component.tsx ├── hooks │ ├── useAppSelector.ts │ ├── useAuth.tsx │ └── useTweets.tsx ├── index.tsx ├── react-app-env.d.ts ├── services │ ├── auth.ts │ ├── notification.ts │ ├── profile.ts │ ├── tweet.ts │ └── user.ts ├── store │ ├── actions │ │ ├── auth.ts │ │ ├── notifications.ts │ │ ├── profile.ts │ │ └── tweets.ts │ ├── index.ts │ ├── reducers │ │ ├── auth.ts │ │ ├── notifications.ts │ │ ├── profile.ts │ │ └── tweet.ts │ └── types.ts ├── styles │ ├── GlobalStyles.tsx │ └── ThemeStyles.tsx ├── types │ └── schemas.ts └── utils │ └── routes.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # editors 13 | .vscode 14 | .idea 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-client", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "prettier": { 7 | "arrowParens": "avoid", 8 | "semi": false, 9 | "singleQuote": false, 10 | "trailingComma": "es5" 11 | }, 12 | "author": { 13 | "name": "Mehdi Neysi", 14 | "email": "dev.mehdineysi@gmail.com", 15 | "url": "https://github.com/neysidev" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject", 22 | "deploy": "liara deploy" 23 | }, 24 | "dependencies": { 25 | "axios": "^0.27.2", 26 | "formik": "^2.2.9", 27 | "react": "^17.0.2", 28 | "react-dom": "^17.0.2", 29 | "react-ionicons": "^4.2.0", 30 | "react-loading": "^2.0.3", 31 | "react-loading-skeleton": "^3.1.0", 32 | "react-redux": "^7.2.4", 33 | "react-router-dom": "^6.3.0", 34 | "react-scripts": "4.0.3", 35 | "react-spinners": "^0.11.0", 36 | "redux": "^4.1.1", 37 | "redux-devtools-extension": "^2.13.9", 38 | "redux-thunk": "^2.3.0", 39 | "simplebar-react": "^2.4.1", 40 | "styled-components": "^5.3.5", 41 | "typescript": "^4.7.2", 42 | "web-vitals": "^1.1.2", 43 | "yup": "^0.32.11" 44 | }, 45 | "eslintConfig": { 46 | "extends": [ 47 | "react-app", 48 | "react-app/jest" 49 | ] 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | }, 63 | "devDependencies": { 64 | "@testing-library/jest-dom": "^5.16.4", 65 | "@testing-library/react": "^13.2.0", 66 | "@testing-library/user-event": "^14.2.0", 67 | "@types/jest": "^27.5.1", 68 | "@types/node": "^17.0.35", 69 | "@types/react": "^18.0.9", 70 | "@types/react-dom": "^18.0.5", 71 | "@types/react-router-dom": "^5.3.3", 72 | "@types/styled-components": "^5.1.25", 73 | "@types/yup": "^0.29.14" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /public/img/auth-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/auth-bg.png -------------------------------------------------------------------------------- /public/img/covers/adamwathan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/covers/adamwathan.jpg -------------------------------------------------------------------------------- /public/img/covers/javalaves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/covers/javalaves.jpg -------------------------------------------------------------------------------- /public/img/covers/siavash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/covers/siavash.jpg -------------------------------------------------------------------------------- /public/img/covers/soh3il.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/covers/soh3il.jpg -------------------------------------------------------------------------------- /public/img/users/adamwathan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/adamwathan.jpg -------------------------------------------------------------------------------- /public/img/users/dan_abramov.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/dan_abramov.jpg -------------------------------------------------------------------------------- /public/img/users/guillermo_rauch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/guillermo_rauch.jpg -------------------------------------------------------------------------------- /public/img/users/javalaves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/javalaves.jpg -------------------------------------------------------------------------------- /public/img/users/neysidev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/neysidev.jpg -------------------------------------------------------------------------------- /public/img/users/not_found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/not_found.jpg -------------------------------------------------------------------------------- /public/img/users/siavash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/siavash.jpg -------------------------------------------------------------------------------- /public/img/users/soh3il.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/public/img/users/soh3il.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twitter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/239d534d144422757b076b9957dfa7eaa8ba65f4/src/assets/images/profile.jpg -------------------------------------------------------------------------------- /src/components/Common/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Search } from "react-ionicons" 4 | 5 | import theme from "../../styles/ThemeStyles" 6 | import TwitterContainer from "./TwitterContainer" 7 | import useAppSelector from "../../hooks/useAppSelector" 8 | 9 | export default function Header() { 10 | const { user }: any = useAppSelector(state => state.authorize) 11 | 12 | return ( 13 | 14 | 15 | 16 | About 17 | Help 18 | 19 | e.preventDefault()}> 20 | 21 | 22 | 23 | 24 | 25 | {user?.name} 26 | {user?.name} 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | const Wrapper = styled.header` 38 | padding: 1rem; 39 | user-select: none; 40 | background: ${theme.dark.backgroundBox}; 41 | position: fixed; 42 | top: 0; 43 | left: 15rem; 44 | right: 0; 45 | z-index: 99; 46 | 47 | & > div { 48 | display: flex; 49 | align-items: center; 50 | justify-content: space-between; 51 | } 52 | ` 53 | 54 | const Links = styled.div` 55 | width: 320px; 56 | display: flex; 57 | gap: 1.5rem; 58 | font-size: 0.8rem; 59 | 60 | a { 61 | font-weight: 300; 62 | transition: ${theme.transition.ease}; 63 | 64 | &:hover { 65 | opacity: 0.75; 66 | } 67 | } 68 | ` 69 | 70 | const SearchBox = styled.form` 71 | width: 100%; 72 | padding: 0.75rem 1rem; 73 | overflow: hidden; 74 | border-radius: 0.5rem; 75 | background: ${theme.dark.backgroundPrimary}; 76 | display: flex; 77 | align-items: center; 78 | gap: 1rem; 79 | 80 | svg { 81 | fill: ${theme.dark.text1}; 82 | color: ${theme.dark.text1}; 83 | } 84 | 85 | input { 86 | flex: 1; 87 | color: ${theme.dark.text1}; 88 | background: transparent; 89 | font-size: 0.8rem; 90 | 91 | &::placeholder { 92 | color: ${theme.dark.text2}; 93 | } 94 | } 95 | ` 96 | 97 | const Profile = styled.div` 98 | width: 320px; 99 | display: flex; 100 | justify-content: flex-end; 101 | transition: ${theme.transition.ease}; 102 | 103 | &:hover { 104 | opacity: 0.75; 105 | } 106 | 107 | a { 108 | display: flex; 109 | align-items: center; 110 | gap: 1rem; 111 | } 112 | 113 | span { 114 | font-size: 0.8rem; 115 | font-weight: 300; 116 | } 117 | 118 | img { 119 | width: 2rem; 120 | border-radius: 50%; 121 | } 122 | ` 123 | -------------------------------------------------------------------------------- /src/components/Common/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SimpleBar from 'simplebar-react' 3 | import styled from 'styled-components' 4 | 5 | import Header from './Header' 6 | import Navigation from './Navigation' 7 | 8 | type Props = { 9 | children: React.ReactNode 10 | } 11 | 12 | export default function Layout(props: Props) { 13 | return ( 14 | 15 | 16 | 17 | 18 |
19 |
{props.children}
20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | const Wrapper = styled.div` 27 | display: flex; 28 | max-width: 100%; 29 | min-height: 100vh; 30 | ` 31 | 32 | const Content = styled.div` 33 | flex: 1; 34 | display: flex; 35 | flex-direction: column; 36 | margin-left: 15rem; 37 | margin-top: 80px; 38 | ` 39 | 40 | const Main = styled.main` 41 | flex: 1; 42 | ` 43 | -------------------------------------------------------------------------------- /src/components/Common/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link, NavLink } from "react-router-dom" 3 | 4 | import logo from "../../assets/images/logo.svg" 5 | import theme from "../../styles/ThemeStyles" 6 | import navigation from "../../constants/navigation" 7 | import useAuth from "../../hooks/useAuth" 8 | 9 | import TwitterButton from "./TwitterButton" 10 | 11 | export default function Navigation() { 12 | const { unreadNotification } = useAuth() 13 | 14 | return ( 15 | 16 | 17 | 18 | Twitter Logo 19 | 20 | 21 | 22 | {navigation.map(link => ( 23 | 24 | (isActive ? "active" : "")} 27 | onClick={event => { 28 | if (link.disabled) event.preventDefault() 29 | }} 30 | > 31 | {link.haveBadge && 32 | link.path === "/notifications" && 33 | unreadNotification?.length && ( 34 | {unreadNotification.length} 35 | )} 36 | {link.icon} 37 | {link.name} 38 | 39 | 40 | ))} 41 | 42 | 43 | 44 | Tweet 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | type LinkProps = { 52 | isDisabled?: boolean 53 | } 54 | 55 | const Wrapper = styled.nav` 56 | width: 15rem; 57 | display: flex; 58 | flex-direction: column; 59 | background: ${theme.dark.backgroundBox}; 60 | box-shadow: 0 0 0.8rem rgba(38, 46, 54, 0.5); 61 | position: fixed; 62 | top: 0; 63 | left: 0; 64 | bottom: 0; 65 | user-select: none; 66 | ` 67 | 68 | const Logo = styled.div` 69 | padding: 1.5rem; 70 | 71 | img { 72 | width: 2rem; 73 | } 74 | ` 75 | 76 | const List = styled.ul` 77 | flex: 1; 78 | margin-top: 4rem; 79 | ` 80 | 81 | const Item = styled.li` 82 | position: relative; 83 | 84 | a { 85 | width: 100%; 86 | color: ${theme.dark.text2}; 87 | padding: 0.75rem 1.5rem; 88 | display: flex; 89 | align-items: center; 90 | gap: 0.75rem; 91 | font-weight: 500; 92 | transition: ${theme.transition.ease}; 93 | border-right: 1px solid transparent; 94 | 95 | &.active { 96 | color: ${theme.dark.primary}; 97 | border-right-color: ${theme.dark.primary}; 98 | 99 | svg { 100 | color: ${theme.dark.primary}; 101 | fill: ${theme.dark.primary}; 102 | } 103 | } 104 | 105 | ${props => 106 | !props.isDisabled 107 | ? `&:hover {opacity: 0.75;}` 108 | : `opacity: 0.5; pointer-events: none;`} 109 | } 110 | 111 | span { 112 | display: grid; 113 | place-items: center; 114 | } 115 | 116 | svg { 117 | color: ${theme.dark.text2}; 118 | fill: ${theme.dark.text2}; 119 | } 120 | ` 121 | 122 | const ButtonWrapper = styled.div` 123 | padding: 1.5rem; 124 | ` 125 | 126 | const Badge = styled.span` 127 | position: absolute; 128 | top: 9px; 129 | left: 35px; 130 | z-index: 99; 131 | width: 15px; 132 | height: 15px; 133 | font-size: 12px; 134 | border-radius: 50%; 135 | color: ${theme.dark.text1}; 136 | background: ${theme.colors.blue}; 137 | ` 138 | -------------------------------------------------------------------------------- /src/components/Common/Tweet.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components" 2 | import { useState } from "react" 3 | import { Link } from "react-router-dom" 4 | import { 5 | ChatboxOutline, 6 | Heart, 7 | HeartOutline, 8 | RepeatOutline, 9 | ShareSocialOutline, 10 | } from "react-ionicons" 11 | 12 | import * as tweetService from "../../services/tweet" 13 | import useAuth from "../../hooks/useAuth" 14 | import theme from "../../styles/ThemeStyles" 15 | import TwitterBox from "./TwitterBox" 16 | import TwitterCard from "./TwitterCard" 17 | 18 | interface ITweet { 19 | id: string 20 | username: string 21 | text: string 22 | image: string 23 | name: string 24 | likes: string[] 25 | replies: number 26 | retweet: number 27 | } 28 | 29 | type ActionProps = { 30 | isActive?: boolean 31 | actionColor: "red" | "green" | "blue" 32 | } 33 | 34 | export default function Tweet(props: ITweet) { 35 | const { user }: any = useAuth() 36 | const [liked, setLiked] = useState(props.likes?.includes(user._id)) 37 | 38 | const url = 39 | user.username === props.username ? "/profile" : `/user/${props.username}` 40 | 41 | const likeStatusTweet = async () => { 42 | await tweetService.updateLikeStatusTweet(props.id, liked) 43 | 44 | liked 45 | ? props.likes.splice(props.likes.indexOf(user._id), 1) 46 | : props.likes.push(user._id) 47 | 48 | setLiked(!liked) 49 | } 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | {props.name} 62 | @{props.username} 63 | 64 | 65 |
66 | {props.text} 67 |
68 | 69 | 70 | {props.replies} 71 | 72 | 73 | 74 | {props.retweet} 75 | 76 | 81 | {liked ? : } 82 | {props.likes?.length || 0} 83 | 84 | 85 | 86 | 87 |
88 |
89 |
90 |
91 | ) 92 | } 93 | 94 | const Wrapper = styled.li` 95 | & > div { 96 | display: flex; 97 | padding: 1rem; 98 | gap: 1rem; 99 | 100 | & > div { 101 | flex: 1; 102 | } 103 | } 104 | ` 105 | 106 | const Profile = styled.img` 107 | width: 3rem; 108 | height: 3rem; 109 | border-radius: 50%; 110 | object-fit: cover; 111 | transition: ${theme.transition.ease}; 112 | 113 | &:hover { 114 | opacity: 0.75; 115 | } 116 | ` 117 | 118 | const Header = styled.header` 119 | display: flex; 120 | align-items: center; 121 | justify-content: space-between; 122 | ` 123 | 124 | const User = styled.div` 125 | display: flex; 126 | align-items: center; 127 | gap: 0.5rem; 128 | ` 129 | 130 | const Name = styled.span` 131 | color: ${theme.dark.text1}; 132 | font-size: 1rem; 133 | ` 134 | 135 | const UserName = styled.span` 136 | color: ${theme.dark.text2}; 137 | font-size: 0.8rem; 138 | transform: translateY(0.5px); 139 | ` 140 | 141 | const Content = styled.p` 142 | padding: 1rem 0; 143 | font-weight: 300; 144 | line-height: 1.5; 145 | ` 146 | 147 | const Footer = styled.footer` 148 | display: flex; 149 | padding-bottom: 1rem; 150 | align-items: center; 151 | ` 152 | 153 | const Action = styled.div` 154 | flex: 1; 155 | gap: 0.5rem; 156 | display: flex; 157 | align-items: center; 158 | justify-content: center; 159 | user-select: none; 160 | cursor: pointer; 161 | transition: ${theme.transition.ease}; 162 | 163 | &:not(:last-child) { 164 | border-right: 1px solid ${theme.dark.backgroundPrimary}; 165 | } 166 | 167 | span { 168 | display: grid; 169 | place-items: center; 170 | text-align: center; 171 | 172 | &:last-child { 173 | min-width: 20px; 174 | } 175 | } 176 | 177 | svg { 178 | fill: ${theme.dark.text2}; 179 | color: ${theme.dark.text2}; 180 | transition: ${theme.transition.ease}; 181 | } 182 | 183 | &:hover { 184 | ${props => 185 | props.actionColor && 186 | props.actionColor === "red" && 187 | css` 188 | color: ${theme.colors.red}; 189 | 190 | svg { 191 | fill: ${theme.colors.red}; 192 | color: ${theme.colors.red}; 193 | } 194 | `} 195 | 196 | ${props => 197 | props.actionColor && 198 | props.actionColor === "blue" && 199 | css` 200 | color: ${theme.colors.blue}; 201 | 202 | svg { 203 | fill: ${theme.colors.blue}; 204 | color: ${theme.colors.blue}; 205 | } 206 | `} 207 | 208 | ${props => 209 | props.actionColor && 210 | props.actionColor === "green" && 211 | css` 212 | color: ${theme.colors.green}; 213 | 214 | svg { 215 | fill: ${theme.colors.green}; 216 | color: ${theme.colors.green}; 217 | } 218 | `} 219 | } 220 | 221 | ${props => 222 | props.isActive && 223 | props.actionColor === "red" && 224 | css` 225 | color: ${theme.colors.red}; 226 | 227 | svg { 228 | fill: ${theme.colors.red}; 229 | color: ${theme.colors.red}; 230 | } 231 | `} 232 | ` 233 | -------------------------------------------------------------------------------- /src/components/Common/TwitterBox.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import theme from '../../styles/ThemeStyles' 3 | 4 | interface ITwitterBox { 5 | isActive?: boolean 6 | isDisabled?: boolean 7 | 8 | color?: 'red' | 'blue' | 'green' 9 | variant?: 'solid' | 'outline' 10 | 11 | children: any 12 | onClick?: () => void 13 | } 14 | 15 | export default function TwitterBox(props: ITwitterBox) { 16 | return ( 17 | 24 | {props.children} 25 | 26 | ) 27 | } 28 | 29 | const Box = styled.div` 30 | padding: 0.5rem; 31 | border-radius: 0.5rem; 32 | position: relative; 33 | border: 1px solid transparent; 34 | transition: ${theme.transition.ease}; 35 | background: ${props => 36 | props.variant === 'solid' ? theme.dark.backgroundBox : 'transparent'}; 37 | 38 | ${props => 39 | props.variant === 'solid' && 40 | `box-shadow: 0 6px 12px rgba(38, 46, 54, 0.2);`} 41 | 42 | ${props => 43 | props.variant === 'outline' && 44 | css` 45 | border-color: ${theme.dark.backgroundCard}; 46 | 47 | &:hover { 48 | border-color: transparent; 49 | background: ${theme.dark.backgroundBox}; 50 | } 51 | `} 52 | 53 | ${props => 54 | props.isDisabled && 55 | css` 56 | opacity: 0.5; 57 | pointer-events: none; 58 | `} 59 | 60 | ${props => 61 | props.isActive && 62 | css` 63 | color: ${theme.colors.blue} !important; 64 | 65 | svg { 66 | fill: ${theme.colors.blue} !important; 67 | color: ${theme.colors.blue} !important; 68 | } 69 | `}; 70 | 71 | ${props => 72 | props.color === 'red' && 73 | css` 74 | &:hover { 75 | color: ${theme.dark.text1} !important; 76 | background: ${theme.colors.red}; 77 | 78 | svg { 79 | fill: ${theme.dark.text1} !important; 80 | } 81 | } 82 | `} 83 | 84 | ${props => 85 | props.color === 'green' && 86 | css` 87 | &:hover { 88 | color: ${theme.dark.text1} !important; 89 | background: ${theme.colors.green}; 90 | 91 | svg { 92 | fill: ${theme.dark.text1} !important; 93 | } 94 | } 95 | `} 96 | 97 | ${props => 98 | props.color === 'blue' && 99 | css` 100 | &:hover { 101 | color: ${theme.dark.text1} !important; 102 | background: ${theme.colors.blue}; 103 | 104 | svg { 105 | fill: ${theme.dark.text1} !important; 106 | } 107 | } 108 | `} 109 | ` 110 | -------------------------------------------------------------------------------- /src/components/Common/TwitterButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { getButtonColors } from '../../helpers/button-component' 3 | import theme from '../../styles/ThemeStyles' 4 | 5 | interface IButton { 6 | fluid?: boolean 7 | disabled?: boolean 8 | 9 | type?: 'submit' | 'button' 10 | variant: 'solid' | 'outline' | 'ghost' | 'link' 11 | 12 | children: any 13 | onClick?: () => void 14 | } 15 | 16 | type ButtonProps = { 17 | fluid?: boolean 18 | disabled?: boolean 19 | variant: 'solid' | 'outline' | 'ghost' | 'link' 20 | buttonColors: string[] 21 | } 22 | 23 | export default function TwitterButton(props: IButton) { 24 | const colors = getButtonColors(props.variant) 25 | 26 | return ( 27 | 37 | ) 38 | } 39 | 40 | const Button = styled.button` 41 | cursor: pointer; 42 | padding: 0.75rem 1.5rem; 43 | font-size: 0.9rem; 44 | border-radius: 99px; 45 | user-select: none; 46 | transition: ${theme.transition.ease}; 47 | 48 | border: 1px solid ${props => props.buttonColors[1]}; 49 | color: ${props => props.buttonColors[0]}; 50 | 51 | background: ${props => 52 | props.variant === 'solid' ? `${props.buttonColors[1]}` : 'transparent'}; 53 | 54 | &:hover { 55 | ${props => 56 | props.variant === 'solid' && `background: ${props.buttonColors[2]}`}; 57 | border-color: ${props => props.buttonColors[2]}; 58 | } 59 | 60 | &:active { 61 | ${props => 62 | props.variant === 'solid' && `background: ${props.buttonColors[3]}`}; 63 | border-color: ${props => props.buttonColors[3]}; 64 | } 65 | 66 | ${props => props.fluid && `width: 100%`}; 67 | ${props => props.disabled && `opacity: 0.75; pointer-events: none;`}; 68 | ` 69 | -------------------------------------------------------------------------------- /src/components/Common/TwitterCard.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import theme from '../../styles/ThemeStyles' 3 | 4 | interface ITwitterCard { 5 | children: any 6 | } 7 | 8 | export default function TwitterCard(props: ITwitterCard) { 9 | return {props.children} 10 | } 11 | 12 | const Card = styled.div` 13 | padding: 0.5rem; 14 | border-radius: 0.5rem; 15 | border-top-left-radius: 0; 16 | box-shadow: 0 6px 12px rgba(38, 46, 54, 0.2); 17 | background: ${theme.dark.backgroundCard}; 18 | ` 19 | -------------------------------------------------------------------------------- /src/components/Common/TwitterContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | interface ITwitterContainer { 4 | size: 'xs' | 'sm' | 'md' | 'lg' 5 | children: any 6 | } 7 | 8 | interface IContainer { 9 | widthContainer: number 10 | } 11 | 12 | export default function TwitterContainer(props: ITwitterContainer) { 13 | let widthContainer = 0 14 | if (props.size === 'lg') widthContainer = 1200 15 | else if (props.size === 'md') widthContainer = 960 16 | else if (props.size === 'sm') widthContainer = 768 17 | else if (props.size === 'xs') widthContainer = 540 18 | 19 | return {props.children} 20 | } 21 | 22 | const Container = styled.div` 23 | width: ${props => props.widthContainer}px; 24 | margin: 0 auto; 25 | max-width: 100%; 26 | ` 27 | -------------------------------------------------------------------------------- /src/components/Common/TwitterFullscreen.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import { Close } from 'react-ionicons' 3 | import theme from '../../styles/ThemeStyles' 4 | 5 | interface ITwitterFullscreen { 6 | isOpen: boolean 7 | type: 'cover' | 'profile' 8 | srcImg?: string 9 | altImg?: string 10 | onClose?: () => void 11 | } 12 | 13 | interface IWrapper { 14 | isOpen: boolean 15 | } 16 | 17 | interface IImage { 18 | imgType: 'cover' | 'profile' 19 | } 20 | 21 | export default function TwitterFullscreen(props: ITwitterFullscreen) { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | {props.altImg} 29 | 30 | ) 31 | } 32 | 33 | const Wrapper = styled.div` 34 | position: fixed; 35 | inset: 0; 36 | opacity: 0; 37 | z-index: 99; 38 | visibility: hidden; 39 | background: rgba(0, 0, 0, 0.75); 40 | transition: ${theme.transition.ease}; 41 | 42 | ${props => 43 | props.isOpen && 44 | css` 45 | opacity: 1; 46 | visibility: visible; 47 | `} 48 | ` 49 | 50 | const Overlay = styled.div` 51 | position: fixed; 52 | inset: 0; 53 | cursor: pointer; 54 | ` 55 | 56 | const CloseButton = styled.button` 57 | position: fixed; 58 | top: 2rem; 59 | left: 2rem; 60 | width: 2rem; 61 | height: 2rem; 62 | border-radius: 50%; 63 | transition: ${theme.transition.ease}; 64 | background: rgba(255, 255, 255, 0.2); 65 | 66 | &:hover { 67 | background: rgba(255, 255, 255, 0.3); 68 | } 69 | 70 | span { 71 | display: grid; 72 | place-items: center; 73 | } 74 | ` 75 | 76 | const Image = styled.img` 77 | position: fixed; 78 | top: 50%; 79 | left: 50%; 80 | transform: translate(-50%, -50%); 81 | 82 | ${props => 83 | props.imgType === 'profile' && 84 | css` 85 | width: 20rem; 86 | height: 20rem; 87 | border-radius: 50%; 88 | `}; 89 | 90 | ${props => 91 | props.imgType === 'cover' && 92 | css` 93 | height: 30rem; 94 | `} 95 | ` 96 | -------------------------------------------------------------------------------- /src/components/Common/TwitterSpinner.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import ReactLoading from 'react-loading' 3 | import theme from '../../styles/ThemeStyles' 4 | 5 | type Props = { 6 | size?: number 7 | } 8 | 9 | export default function TwitterSpinner(props: Props) { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | const Loading = styled.div` 18 | position: absolute; 19 | inset: 0; 20 | border-radius: 0.5rem; 21 | background: ${theme.dark.backgroundBox}; 22 | display: grid; 23 | place-items: center; 24 | ` 25 | -------------------------------------------------------------------------------- /src/components/Core/ProfileActions.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Eye, LogOut, People, Settings } from "react-ionicons" 4 | 5 | import theme from "../../styles/ThemeStyles" 6 | import TwitterBox from "../Common/TwitterBox" 7 | 8 | type Props = { 9 | activeLink?: "activity" | "moments" | "friends" | "edit" 10 | } 11 | 12 | const user_actions = [ 13 | { 14 | url: "", 15 | name: "activity", 16 | title: "Activity", 17 | icon: , 18 | }, 19 | { 20 | url: "/friends", 21 | name: "friends", 22 | title: "Friends", 23 | icon: , 24 | }, 25 | { 26 | url: "/logout", 27 | name: "logout", 28 | title: "Logout", 29 | icon: , 30 | }, 31 | { 32 | url: "/edit", 33 | name: "edit", 34 | title: "Edit Profile", 35 | icon: , 36 | }, 37 | ] 38 | 39 | export default function ProfileActions(props: Props) { 40 | return ( 41 | 42 | {user_actions.map(action => ( 43 | 44 | 48 | {action.icon} 49 | {action.title} 50 | 51 | 52 | ))} 53 | 54 | ) 55 | } 56 | 57 | const Grid = styled.div` 58 | display: grid; 59 | gap: 1rem; 60 | user-select: none; 61 | grid-template-columns: repeat(2, 1fr); 62 | 63 | div { 64 | gap: 0.5rem; 65 | height: 6rem; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | flex-direction: column; 70 | color: ${theme.dark.text1}; 71 | 72 | span { 73 | display: grid; 74 | place-items: center; 75 | 76 | svg { 77 | transition: ${theme.transition.ease}; 78 | fill: ${theme.dark.text2}; 79 | color: ${theme.dark.text2}; 80 | } 81 | } 82 | 83 | &:hover { 84 | color: ${theme.colors.blue}; 85 | 86 | svg { 87 | fill: ${theme.colors.blue}; 88 | color: ${theme.colors.blue}; 89 | } 90 | } 91 | } 92 | ` 93 | 94 | const Title = styled.h3` 95 | font-size: 0.8rem; 96 | font-weight: 300; 97 | ` 98 | -------------------------------------------------------------------------------- /src/components/Core/TrendsForYou.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { Link } from 'react-router-dom' 3 | import { Cog, EllipsisVertical } from 'react-ionicons' 4 | 5 | import theme from '../../styles/ThemeStyles' 6 | import TwitterBox from '../Common/TwitterBox' 7 | import { trends_hashtags } from '../../constants/hashtags' 8 | 9 | export default function TrendsForYou() { 10 | return ( 11 | 12 | 13 |
14 |

Trends for you

15 | 16 |
17 | 18 | {trends_hashtags.map(hashtag => ( 19 | 20 |
21 | #{hashtag.tag} 22 | {hashtag.count} tweeks 23 |
24 |
25 | 26 |
27 |
28 | ))} 29 |
30 | 31 | See all 32 | 33 |
34 |
35 | ) 36 | } 37 | 38 | const Wrapper = styled.div` 39 | & > div { 40 | padding: 1rem 0; 41 | } 42 | ` 43 | 44 | const Header = styled.header` 45 | padding: 0 1rem; 46 | display: flex; 47 | align-items: center; 48 | justify-content: space-between; 49 | border-bottom: 1px solid ${theme.dark.backgroundPrimary}; 50 | padding-bottom: 1rem; 51 | 52 | span { 53 | display: grid; 54 | place-items: center; 55 | } 56 | 57 | svg { 58 | fill: ${theme.dark.text2}; 59 | color: ${theme.dark.text2}; 60 | } 61 | ` 62 | 63 | const Hashtags = styled.ul` 64 | margin-top: 0.5rem; 65 | user-select: none; 66 | ` 67 | 68 | const Hashtag = styled.li` 69 | padding: 0.5rem 1rem; 70 | cursor: pointer; 71 | display: flex; 72 | align-items: center; 73 | justify-content: space-between; 74 | 75 | &:hover { 76 | opacity: 0.75; 77 | } 78 | 79 | div:first-child { 80 | display: flex; 81 | flex-direction: column; 82 | gap: 0.2rem; 83 | 84 | span:last-child { 85 | font-size: 0.8rem; 86 | color: ${theme.dark.text2}; 87 | } 88 | } 89 | 90 | div:last-child { 91 | span { 92 | display: grid; 93 | place-items: center; 94 | 95 | svg { 96 | fill: ${theme.dark.text2}; 97 | color: ${theme.dark.text2}; 98 | } 99 | } 100 | } 101 | ` 102 | 103 | const Action = styled.div` 104 | padding: 1rem 1rem 0 1rem; 105 | text-align: center; 106 | text-transform: uppercase; 107 | font-size: 0.8rem; 108 | 109 | a { 110 | color: ${theme.dark.text2}; 111 | transition: ${theme.transition.ease}; 112 | 113 | &:hover { 114 | color: ${theme.dark.text1}; 115 | } 116 | } 117 | ` 118 | -------------------------------------------------------------------------------- /src/components/Core/UserActions.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Chatbubble, Notifications, Person, Share } from "react-ionicons" 4 | 5 | import { IUser } from "../../types/schemas" 6 | import theme from "../../styles/ThemeStyles" 7 | import TwitterBox from "../Common/TwitterBox" 8 | 9 | type Props = { 10 | user: IUser 11 | follow: boolean 12 | 13 | followUser: () => void 14 | unfollowUser: () => void 15 | } 16 | 17 | export default function UserActions(props: Props) { 18 | const toggleFollow = async () => { 19 | props.follow ? props.unfollowUser() : props.followUser() 20 | } 21 | 22 | const shareProfile = () => { 23 | window.navigator.share({ 24 | text: props.user.name, 25 | title: "Share Profile", 26 | url: window.location.href, 27 | }) 28 | } 29 | 30 | // TODO: ADD SPINNER WHEN LOADING FOLLOW IS TRUE 31 | 32 | return ( 33 | 34 | 39 | 40 | {props.follow ? "Unfollow" : "Follow"} 41 | 42 | 43 | 44 | 45 | Message 46 | 47 | 48 | 49 | 50 | Notifications 51 | 52 | 53 | 54 | Share 55 | 56 | 57 | ) 58 | } 59 | 60 | const Grid = styled.div` 61 | display: grid; 62 | gap: 1rem; 63 | user-select: none; 64 | grid-template-columns: repeat(2, 1fr); 65 | 66 | div { 67 | gap: 0.5rem; 68 | height: 6rem; 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-direction: column; 73 | color: ${theme.dark.text1}; 74 | cursor: pointer; 75 | 76 | span { 77 | display: grid; 78 | place-items: center; 79 | 80 | svg { 81 | transition: ${theme.transition.ease}; 82 | fill: ${theme.dark.text2}; 83 | color: ${theme.dark.text2}; 84 | } 85 | } 86 | 87 | &:hover { 88 | color: ${theme.colors.blue}; 89 | 90 | svg { 91 | fill: ${theme.colors.blue}; 92 | color: ${theme.colors.blue}; 93 | } 94 | } 95 | } 96 | ` 97 | 98 | const Title = styled.h3` 99 | font-size: 0.8rem; 100 | font-weight: 300; 101 | ` 102 | -------------------------------------------------------------------------------- /src/components/Core/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import ReactLoading from "react-loading" 3 | import Skeleton from "react-loading-skeleton" 4 | import { LocationOutline } from "react-ionicons" 5 | 6 | import theme from "../../styles/ThemeStyles" 7 | import TwitterBox from "../Common/TwitterBox" 8 | 9 | import { IUser } from "../../types/schemas" 10 | import { useUsersTweets } from "../../hooks/useTweets" 11 | 12 | type Props = { 13 | user: IUser 14 | loading: boolean 15 | onOpen: () => void 16 | } 17 | 18 | export default function UserInfo(props: Props) { 19 | const { tweetsCount } = useUsersTweets() 20 | 21 | const handleClickAvatar = () => { 22 | if (!!props?.user?.image) props.onOpen() 23 | } 24 | 25 | return ( 26 | 27 | {props?.loading && ( 28 | 29 | 30 | 31 | )} 32 |
33 | 34 | {props?.user ? ( 35 | {`${props?.user?.name} 39 | ) : ( 40 | 41 | )} 42 | 43 | 44 |

{props?.user?.name}

45 |

@{props?.user?.username}

46 |
47 | {props?.user?.location && ( 48 | 49 | 50 |

{props?.user?.location}

51 |
52 | )} 53 | 54 | 55 | Tweets 56 | 57 | {props?.user ? props?.user?.tweets?.length : tweetsCount} 58 | 59 | 60 | 61 | Followers 62 | {props?.user && props?.user?.followers?.length} 63 | 64 | 65 | Following 66 | {props?.user && props?.user?.following?.length} 67 | 68 | 69 |
70 |
71 | ) 72 | } 73 | 74 | interface IAvatar { 75 | hasAvatar?: boolean 76 | } 77 | 78 | const Loading = styled.div` 79 | position: absolute; 80 | inset: 0; 81 | border-radius: 0.5rem; 82 | background: ${theme.dark.backgroundBox}; 83 | display: grid; 84 | place-items: center; 85 | ` 86 | 87 | const Center = styled.div` 88 | display: flex; 89 | align-items: center; 90 | flex-direction: column; 91 | padding: 1rem 0; 92 | gap: 1rem; 93 | ` 94 | 95 | const Avatar = styled.div` 96 | width: 5rem; 97 | height: 5rem; 98 | overflow: hidden; 99 | border-radius: 50%; 100 | 101 | ${props => props.hasAvatar && `cursor: pointer;`} 102 | 103 | img { 104 | width: 100%; 105 | height: 100%; 106 | object-fit: cover; 107 | } 108 | 109 | & > span { 110 | height: 100%; 111 | display: block; 112 | 113 | .react-loading-skeleton { 114 | height: 100%; 115 | transform: translateY(-2px); 116 | } 117 | } 118 | ` 119 | 120 | const Name = styled.div` 121 | gap: 0.2rem; 122 | display: grid; 123 | text-align: center; 124 | 125 | h2 { 126 | color: ${theme.dark.text1}; 127 | font-size: 1.2rem; 128 | } 129 | 130 | p { 131 | color: ${theme.dark.text2}; 132 | font-size: 0.8rem; 133 | } 134 | ` 135 | 136 | const Location = styled.div` 137 | gap: 0.2rem; 138 | display: flex; 139 | align-items: center; 140 | color: ${theme.dark.text1}; 141 | 142 | span { 143 | display: grid; 144 | place-items: center; 145 | } 146 | 147 | svg { 148 | fill: ${theme.dark.text2}; 149 | color: ${theme.dark.text2}; 150 | } 151 | 152 | p { 153 | font-size: 0.8rem; 154 | } 155 | ` 156 | 157 | const List = styled.ul` 158 | gap: 1rem; 159 | width: 100%; 160 | display: flex; 161 | padding: 0 0.5rem; 162 | ` 163 | 164 | const Item = styled.li` 165 | flex: 1; 166 | display: grid; 167 | gap: 0.3rem; 168 | text-align: center; 169 | 170 | &:not(:last-child) { 171 | padding-right: 1rem; 172 | border-right: 1px solid ${theme.dark.backgroundPrimary}; 173 | } 174 | ` 175 | const Title = styled.span` 176 | font-weight: 300; 177 | font-size: 0.8rem; 178 | color: ${theme.dark.text2}; 179 | ` 180 | 181 | const Value = styled.span` 182 | font-weight: 500; 183 | ` 184 | -------------------------------------------------------------------------------- /src/components/Core/YouShouldFollow.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Reload } from "react-ionicons" 4 | import { useEffect, useState } from "react" 5 | 6 | import * as userService from "../../services/user" 7 | import { IUser } from "../../types/schemas" 8 | import theme from "../../styles/ThemeStyles" 9 | import TwitterBox from "../Common/TwitterBox" 10 | import TwitterSpinner from "../Common/TwitterSpinner" 11 | 12 | export default function YouShouldFollow() { 13 | const randomUsersNumber = 3 14 | 15 | const [users, setUsers] = useState([]) 16 | const [loading, setLoading] = useState(false) 17 | 18 | useEffect(() => { 19 | getRandomUsers() 20 | }, []) 21 | 22 | const getRandomUsers = async () => { 23 | setLoading(true) 24 | setUsers([]) 25 | 26 | const res = await userService.randomUsers(randomUsersNumber) 27 | 28 | if (res.success) setUsers(res.users) 29 | setLoading(false) 30 | } 31 | 32 | let $users_content = null 33 | if (users) { 34 | $users_content = users.map(user => ( 35 | 36 | 37 | 38 | {user.name} 39 |
40 | {user.name} 41 | @{user.username} 42 |
43 |
44 | 45 |
46 | )) 47 | } 48 | 49 | return ( 50 | 51 | 52 | {loading && } 53 |
54 |

You should follow

55 | 56 |
57 | {$users_content} 58 | 59 | See all 60 | 61 |
62 |
63 | ) 64 | } 65 | 66 | const Wrapper = styled.div` 67 | & > div { 68 | padding: 1rem 0; 69 | 70 | & > div:first-child { 71 | height: 280.75px; 72 | } 73 | } 74 | ` 75 | 76 | const Header = styled.header` 77 | padding: 0 1rem; 78 | display: flex; 79 | align-items: center; 80 | justify-content: space-between; 81 | border-bottom: 1px solid ${theme.dark.backgroundPrimary}; 82 | padding-bottom: 1rem; 83 | 84 | span { 85 | display: grid; 86 | cursor: pointer; 87 | place-items: center; 88 | } 89 | 90 | svg { 91 | fill: ${theme.dark.text2}; 92 | color: ${theme.dark.text2}; 93 | } 94 | ` 95 | 96 | const Users = styled.ul` 97 | margin-top: 0.5rem; 98 | user-select: none; 99 | ` 100 | 101 | const User = styled.li` 102 | padding: 0.5rem 1rem; 103 | display: flex; 104 | align-items: center; 105 | justify-content: space-between; 106 | 107 | button { 108 | width: 6rem; 109 | padding: 0.5rem 1rem; 110 | } 111 | ` 112 | 113 | const UserInfo = styled.div` 114 | display: flex; 115 | gap: 0.5rem; 116 | 117 | & > div { 118 | display: flex; 119 | flex-direction: column; 120 | gap: 0.2rem; 121 | } 122 | 123 | span:last-child { 124 | font-size: 0.8rem; 125 | color: ${theme.dark.text2}; 126 | } 127 | 128 | img { 129 | width: 2.5rem; 130 | height: 2.5rem; 131 | border-radius: 50%; 132 | } 133 | ` 134 | 135 | const Action = styled.div` 136 | padding: 1rem 1rem 0 1rem; 137 | text-align: center; 138 | text-transform: uppercase; 139 | font-size: 0.8rem; 140 | user-select: none; 141 | 142 | a { 143 | color: ${theme.dark.text2}; 144 | transition: ${theme.transition.ease}; 145 | 146 | &:hover { 147 | color: ${theme.dark.text1}; 148 | } 149 | } 150 | ` 151 | -------------------------------------------------------------------------------- /src/components/Forms/EditProfileForm.tsx: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup" 2 | import styled from "styled-components" 3 | import { useDispatch } from "react-redux" 4 | import { Formik, Form, Field } from "formik" 5 | 6 | import * as profileAction from "../../store/actions/profile" 7 | import useAuth from "../../hooks/useAuth" 8 | import theme from "../../styles/ThemeStyles" 9 | import TwitterButton from "../Common/TwitterButton" 10 | 11 | import TwitterSpinner from "../Common/TwitterSpinner" 12 | import useAppSelector from "../../hooks/useAppSelector" 13 | 14 | const formSchema = Yup.object().shape({ 15 | name: Yup.string().required("Please enter your name").max(50), 16 | bio: Yup.string().max(160), 17 | website: Yup.string().url("Website must be a valid URL").max(100), 18 | location: Yup.string().max(30), 19 | }) 20 | 21 | export default function EditProfile() { 22 | const { user }: any = useAuth() 23 | const dispatch = useDispatch() 24 | 25 | const { loading } = useAppSelector(state => state.profile) 26 | 27 | const initialValues = { 28 | name: user.name || "", 29 | bio: user.bio || "", 30 | location: user.location || "", 31 | website: user.website || "", 32 | birthday: user.birthday || "", 33 | } 34 | 35 | return ( 36 | 37 | { 41 | dispatch(profileAction.updateUserProfile(user._id, values)) 42 | }} 43 | > 44 | {({ errors, touched }: any) => ( 45 |
46 | 47 | Name 48 | 54 | {errors.name && touched.name && {errors.name}} 55 | 56 | 57 | Bio 58 | 65 | {errors.bio && touched.bio && {errors.bio}} 66 | 67 | 68 | Location 69 | 75 | {errors.location && touched.location && ( 76 | {errors.location} 77 | )} 78 | 79 | 80 | Website 81 | 87 | {errors.website && touched.website && ( 88 | {errors.website} 89 | )} 90 | 91 | 92 | Birthday 93 | 99 | {errors.birthday && touched.birthday && ( 100 | {errors.birthday} 101 | )} 102 | 103 | 104 | 105 | {loading && } 106 | 112 | 113 |
114 | )} 115 |
116 |
117 | ) 118 | } 119 | 120 | const Wrapper = styled.div` 121 | padding: 0.5rem; 122 | 123 | form { 124 | display: flex; 125 | flex-direction: column; 126 | gap: 1rem; 127 | 128 | button { 129 | width: 100%; 130 | height: 100%; 131 | margin-left: auto; 132 | text-align: center; 133 | } 134 | } 135 | ` 136 | 137 | const FormControl = styled.div` 138 | display: flex; 139 | flex-direction: column; 140 | gap: 0.5rem; 141 | 142 | input, 143 | textarea { 144 | border: 1px solid ${theme.dark.backgroundCard}; 145 | padding: 0.75rem 1rem; 146 | color: ${theme.dark.text1}; 147 | background: ${theme.dark.backgroundCard}; 148 | border-radius: 0.5rem; 149 | transition: ${theme.transition.ease}; 150 | 151 | &:focus { 152 | border-color: ${theme.colors.blue}; 153 | box-shadow: 0 0 0 2px rgba(29, 162, 243, 0.5); 154 | } 155 | } 156 | 157 | textarea { 158 | resize: vertical; 159 | height: 100px; 160 | } 161 | 162 | small { 163 | color: ${theme.colors.red}; 164 | font-size: 0.8rem; 165 | } 166 | ` 167 | 168 | const FormLabel = styled.label`` 169 | 170 | const ButtonWrapper = styled.div` 171 | width: 7rem; 172 | overflow: hidden; 173 | height: 44px; 174 | position: relative; 175 | margin-left: auto; 176 | border-radius: 99px; 177 | ` 178 | -------------------------------------------------------------------------------- /src/components/Home/WhatsHappening.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { useState } from "react" 3 | import { Link } from "react-router-dom" 4 | import { useDispatch } from "react-redux" 5 | import { CalendarOutline, HappyOutline, ImageOutline } from "react-ionicons" 6 | 7 | import * as authAction from "../../store/actions/auth" 8 | import * as tweetService from "../../services/tweet" 9 | import * as notificationService from "../../services/notification" 10 | 11 | import theme from "../../styles/ThemeStyles" 12 | import TwitterBox from "../Common/TwitterBox" 13 | import TwitterButton from "../Common/TwitterButton" 14 | import useAppSelector from "../../hooks/useAppSelector" 15 | import { IUser } from "../../types/schemas" 16 | 17 | export default function WhatsHappening() { 18 | const dispatch = useDispatch() 19 | const { 20 | user, 21 | }: { 22 | user: IUser 23 | } = useAppSelector(state => state.authorize) 24 | 25 | const [text, setText] = useState("") 26 | const [loading, setLoading] = useState(false) 27 | 28 | const createTweet = async () => { 29 | setText("") 30 | setLoading(true) 31 | 32 | const { tweet } = await tweetService.createTweet(text) 33 | await notificationService.addNotification(text, tweet._id) 34 | 35 | setLoading(false) 36 | dispatch(authAction.getHomeTweets()) 37 | } 38 | 39 | return ( 40 | 41 | 42 | 43 | 47 | 48 |
49 |