├── .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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
70 |
71 |
72 | )
73 | }
74 |
75 | const Wrapper = styled.div`
76 | gap: 1rem;
77 | display: flex;
78 | padding: 0.5rem;
79 |
80 | & > div {
81 | flex: 1;
82 | display: flex;
83 | flex-direction: column;
84 | }
85 | `
86 |
87 | const Profile = styled.img`
88 | width: 3rem;
89 | height: 3rem;
90 | border-radius: 50%;
91 | object-fit: cover;
92 | transition: ${theme.transition.ease};
93 |
94 | &:hover {
95 | opacity: 0.75;
96 | }
97 | `
98 |
99 | const TextArea = styled.textarea`
100 | color: ${theme.dark.text1};
101 | width: 100%;
102 | min-height: 50px;
103 | margin-top: 0.5rem;
104 | background: transparent;
105 | resize: none;
106 |
107 | &::placeholder {
108 | color: ${theme.dark.text2};
109 | }
110 | `
111 |
112 | const Divider = styled.hr`
113 | border-top: 1px solid ${theme.dark.backgroundPrimary};
114 | margin: 0.5rem 0;
115 | `
116 |
117 | const Footer = styled.footer`
118 | display: flex;
119 | align-items: center;
120 | justify-content: space-between;
121 | `
122 |
123 | const Actions = styled.div`
124 | display: flex;
125 | align-items: center;
126 | gap: 0.5rem;
127 |
128 | span {
129 | cursor: pointer;
130 | transition: ${theme.transition.ease};
131 |
132 | &:hover {
133 | opacity: 0.75;
134 | }
135 | }
136 |
137 | svg {
138 | fill: ${theme.dark.primary};
139 | color: ${theme.dark.primary};
140 | }
141 | `
142 |
--------------------------------------------------------------------------------
/src/components/Skeleton/TweetSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import Skeleton from 'react-loading-skeleton'
3 | import TwitterBox from '../Common/TwitterBox'
4 |
5 | export default function TweetSkeleton() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | const Wrapper = styled.li`
21 | & > div {
22 | display: flex;
23 | padding: 1rem;
24 | gap: 1rem;
25 |
26 | & > span {
27 | flex: 1;
28 | }
29 | }
30 | `
31 |
32 | const Profile = styled.div`
33 | width: 3rem;
34 | height: 3rem;
35 | overflow: hidden;
36 | border-radius: 50%;
37 |
38 | & > span {
39 | display: block;
40 | height: 100%;
41 |
42 | .react-loading-skeleton {
43 | height: 100%;
44 | transform: translateY(-2px);
45 | }
46 | }
47 | `
48 |
49 | const Content = styled.span`
50 | min-height: 7rem;
51 |
52 | & > span {
53 | display: block;
54 | height: 100%;
55 |
56 | .react-loading-skeleton {
57 | height: 100%;
58 | }
59 | }
60 | `
61 |
--------------------------------------------------------------------------------
/src/config/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 |
3 | const instance = axios.create({
4 | baseURL: "https://twitter-backend.iran.liara.run/api",
5 | withCredentials: true,
6 | })
7 |
8 | export default instance
9 |
--------------------------------------------------------------------------------
/src/constants/hashtags.tsx:
--------------------------------------------------------------------------------
1 | interface IHashtag {
2 | id: number
3 | tag: string
4 | count: string
5 | }
6 |
7 | export const trends_hashtags: IHashtag[] = [
8 | { id: 1, tag: 'girls', count: '4.2m' },
9 | { id: 2, tag: 'party', count: '2.7m' },
10 | { id: 3, tag: 'drinks', count: '420k' },
11 | { id: 4, tag: 'euphoria', count: '128k' }
12 | ]
13 |
--------------------------------------------------------------------------------
/src/constants/navigation.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Bookmark,
3 | Compass,
4 | Document,
5 | Home,
6 | Mail,
7 | Notifications,
8 | Person,
9 | } from "react-ionicons"
10 |
11 | interface INavLink {
12 | name: string
13 | path: string
14 | icon: any
15 | haveBadge?: boolean
16 | disabled?: boolean
17 | }
18 |
19 | const navigation: INavLink[] = [
20 | {
21 | name: "Home",
22 | path: "/",
23 | icon: ,
24 | },
25 | {
26 | name: "Explore",
27 | path: "/explore",
28 | icon: ,
29 | disabled: true,
30 | },
31 | {
32 | name: "Notifications",
33 | path: "/notifications",
34 | icon: ,
35 | haveBadge: true,
36 | disabled: false,
37 | },
38 | {
39 | name: "Messages",
40 | path: "/messages",
41 | icon: ,
42 | disabled: true,
43 | },
44 | {
45 | name: "Bookmarks",
46 | path: "/bookmarks",
47 | icon: ,
48 | disabled: true,
49 | },
50 | {
51 | name: "Lists",
52 | path: "/lists",
53 | icon: ,
54 | disabled: true,
55 | },
56 | {
57 | name: "Profile",
58 | path: "/profile",
59 | icon: ,
60 | disabled: false,
61 | },
62 | ]
63 |
64 | export default navigation
65 |
--------------------------------------------------------------------------------
/src/constants/tweets.tsx:
--------------------------------------------------------------------------------
1 | export const home_tweets = [
2 | {
3 | id: 1,
4 | retweet: 20,
5 | likes: 60,
6 | replies: 40,
7 | createdAt: '2021-08-18T13:04:43',
8 | text: 'The new Wealthfront website is a beautiful example of how Tailwind was designed to be used, with tons of customizations to really make it their own.',
9 | user: {
10 | name: 'Adam Wathan',
11 | username: 'adamwathan',
12 | image: 'adam_wathan.jpg'
13 | }
14 | },
15 | {
16 | id: 2,
17 | retweet: 9,
18 | likes: 145,
19 | replies: 8,
20 | createdAt: '2021-08-14T17:06:01',
21 | text: "I hadn't use npm directly in a while… it's great to see how much more robust and performant it's gotten with v7",
22 | user: {
23 | name: 'Guillermo Rauch',
24 | username: 'rauchg',
25 | image: 'guillermo_rauch.jpg'
26 | }
27 | },
28 | {
29 | id: 3,
30 | retweet: 16,
31 | likes: 104,
32 | replies: 101,
33 | createdAt: '2021-08-25T07:13:02',
34 | text: 'You can only listen to six albums for the next six months. What are you picks?',
35 | user: {
36 | name: 'Dan',
37 | username: 'dan_abramov',
38 | image: 'dan_abramov.jpg'
39 | }
40 | },
41 | {
42 | id: 4,
43 | retweet: 20,
44 | likes: 133,
45 | replies: 44,
46 | createdAt: '2021-08-22T04:08:43',
47 | text: 'I know what this is. It just my self talking to myself about myself',
48 | user: {
49 | name: 'Sohail',
50 | username: 'soh3il',
51 | image: 'soh3il.jpg'
52 | }
53 | }
54 | ]
55 |
--------------------------------------------------------------------------------
/src/containers/App.tsx:
--------------------------------------------------------------------------------
1 | import SimpleBar from "simplebar-react"
2 | import { useEffect, Suspense } from "react"
3 | import { useDispatch } from "react-redux"
4 | import { SkeletonTheme } from "react-loading-skeleton"
5 | import { BrowserRouter, Routes, Route } from "react-router-dom"
6 |
7 | import * as authAction from "../store/actions/auth"
8 | import * as notificationsAction from "../store/actions/notifications"
9 |
10 | import Layout from "../components/Common/Layout"
11 | import routes from "../utils/routes"
12 | import theme from "../styles/ThemeStyles"
13 | import useAuth from "../hooks/useAuth"
14 |
15 | import Login from "./Login"
16 | import Register from "./Register"
17 |
18 | export default function App() {
19 | const dispatch = useDispatch()
20 | const { loading, isLogin } = useAuth()
21 |
22 | useEffect(() => {
23 | dispatch(authAction.getUser())
24 | dispatch(authAction.getHomeTweets())
25 | dispatch(notificationsAction.getNotifications())
26 | // eslint-disable-next-line
27 | }, [])
28 |
29 | return (
30 |
31 |
35 |
36 |
37 |
38 | {loading ? (
39 | "Loading..."
40 | ) : isLogin ? (
41 |
42 | {routes.map((route, index) => (
43 | }
47 | />
48 | ))}
49 |
50 | ) : (
51 | <>
52 | } />
53 | } />
54 | >
55 | )}
56 |
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/src/containers/EditProfile.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { useEffect } from "react"
3 | import { useParams } from "react-router-dom"
4 | import { useDispatch } from "react-redux"
5 | import { Camera, CloseCircle } from "react-ionicons"
6 |
7 | import EditProfileForm from "../components/Forms/EditProfileForm"
8 | import TwitterBox from "../components/Common/TwitterBox"
9 | import TwitterContainer from "../components/Common/TwitterContainer"
10 | import UserActions from "../components/Core/ProfileActions"
11 |
12 | import * as userService from "../services/user"
13 | import * as profileAction from "../store/actions/profile"
14 | import theme from "../styles/ThemeStyles"
15 | import useAuth from "../hooks/useAuth"
16 |
17 | export default function EditProfile() {
18 | const { user }: any = useAuth()
19 | const dispatch = useDispatch()
20 | const params: any = useParams()
21 |
22 | useEffect(() => {
23 | dispatch(profileAction.getUserProfile(params.username))
24 | }, [dispatch, params.username])
25 |
26 | return (
27 |
28 |
29 |
30 | {user?.cover && (
31 |
32 | )}
33 |
34 |
35 | {
37 | if (!user.cover) return
38 |
39 | const res = await userService.removeCover(user?._id)
40 | if (res.success)
41 | dispatch(profileAction.getUserProfile(params.username))
42 | }}
43 | />
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
59 | @{user.username}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | type ICover = {
76 | hasCover?: boolean
77 | }
78 |
79 | const Wrapper = styled.div``
80 |
81 | const Cover = styled.div`
82 | width: 100%;
83 | height: 17rem;
84 | overflow: hidden;
85 | position: relative;
86 | background: ${theme.colors.blue};
87 |
88 | ${props => props.hasCover && `cursor: pointer;`}
89 |
90 | img {
91 | width: 100%;
92 | height: 100%;
93 | object-fit: cover;
94 | }
95 | `
96 |
97 | const CoverOverlay = styled.div`
98 | position: absolute;
99 | inset: 0;
100 | background: rgba(0, 0, 0, 0.3);
101 | `
102 |
103 | const CoverActions = styled.div`
104 | position: absolute;
105 | top: 50%;
106 | left: 50%;
107 | display: flex;
108 | gap: 1rem;
109 | transform: translate(-50%, -50%);
110 |
111 | span {
112 | cursor: pointer;
113 | width: 2.5rem;
114 | height: 2.5rem;
115 | border-radius: 50%;
116 | display: grid;
117 | place-items: center;
118 | transition: ${theme.transition.ease};
119 |
120 | svg {
121 | color: #ffffff;
122 | fill: #ffffff;
123 | }
124 |
125 | &:hover {
126 | background: rgba(255, 255, 255, 0.1);
127 | }
128 |
129 | &:active {
130 | background: rgba(255, 255, 255, 0.2);
131 | }
132 | }
133 | `
134 |
135 | const Content = styled.div`
136 | display: flex;
137 | gap: 1rem;
138 | transform: translateY(-3rem);
139 | `
140 |
141 | const Group = styled.div`
142 | display: flex;
143 | flex-direction: column;
144 | gap: 1rem;
145 | `
146 |
147 | const Center = styled.div`
148 | display: flex;
149 | align-items: center;
150 | flex-direction: column;
151 | padding: 1rem 0;
152 | gap: 1rem;
153 | `
154 |
155 | const Avatar = styled.div`
156 | width: 5rem;
157 | height: 5rem;
158 | overflow: hidden;
159 | border-radius: 50%;
160 | position: relative;
161 |
162 | img {
163 | width: 100%;
164 | height: 100%;
165 | object-fit: cover;
166 | }
167 |
168 | span {
169 | position: absolute;
170 | top: 50%;
171 | left: 50%;
172 | padding: 0.25rem;
173 | border-radius: 50%;
174 | cursor: pointer;
175 | transform: translate(-50%, -50%);
176 | transition: ${theme.transition.ease};
177 |
178 | svg {
179 | color: #ffffff;
180 | fill: #ffffff;
181 | }
182 |
183 | &:hover {
184 | background: rgba(255, 255, 255, 0.1);
185 | }
186 |
187 | &:active {
188 | background: rgba(255, 255, 255, 0.2);
189 | }
190 | }
191 | `
192 |
193 | const AvatarOverlay = styled.div`
194 | position: absolute;
195 | inset: 0;
196 | background: rgba(0, 0, 0, 0.3);
197 | `
198 |
199 | const Username = styled.p`
200 | text-align: center;
201 | font-size: 0.8rem;
202 | color: ${theme.dark.text2};
203 | `
204 |
--------------------------------------------------------------------------------
/src/containers/Home.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components"
2 | import { useEffect } from "react"
3 | import { People } from "react-ionicons"
4 | import { useDispatch } from "react-redux"
5 | import { Link } from "react-router-dom"
6 |
7 | import * as authAction from "../store/actions/auth"
8 | import { useHomeTweets } from "../hooks/useTweets"
9 | import TrendsForYou from "../components/Core/TrendsForYou"
10 | import Tweet from "../components/Common/Tweet"
11 | import TweetSkeleton from "../components/Skeleton/TweetSkeleton"
12 | import TwitterContainer from "../components/Common/TwitterContainer"
13 | import WhatsHappening from "../components/Home/WhatsHappening"
14 | import YouShouldFollow from "../components/Core/YouShouldFollow"
15 | import TwitterBox from "../components/Common/TwitterBox"
16 | import TwitterButton from "../components/Common/TwitterButton"
17 |
18 | export default function Home() {
19 | const dispatch = useDispatch()
20 | const { loading, tweets } = useHomeTweets()
21 |
22 | useEffect(() => {
23 | dispatch(authAction.getHomeTweets())
24 | }, [dispatch])
25 |
26 | let $tweets_content = null
27 | if (loading) {
28 | $tweets_content =
29 | } else {
30 | if (tweets.length === 0) {
31 | $tweets_content = (
32 |
33 |
34 |
35 | Do you like to follow your friends?
36 |
37 | Connect people
38 |
39 |
40 |
41 | )
42 | } else {
43 | $tweets_content = tweets
44 | .map(({ tweet, user }: any) => (
45 |
56 | ))
57 | .reverse()
58 | }
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 |
66 | {$tweets_content}
67 |
68 |
72 |
73 |
74 | )
75 | }
76 |
77 | const Wrapper = styled.div`
78 | display: flex;
79 | gap: 1rem;
80 | padding: 2rem 0;
81 | `
82 |
83 | const Content = styled.div`
84 | flex: 1;
85 | display: flex;
86 | flex-direction: column;
87 | gap: 1rem;
88 | `
89 |
90 | const Tweets = styled.ul`
91 | display: grid;
92 | gap: 1rem;
93 | `
94 |
95 | const Aside = styled.aside`
96 | width: 20rem;
97 | gap: 1rem;
98 | display: flex;
99 | flex-direction: column;
100 | position: sticky;
101 | `
102 |
103 | const TweetsEmpty = styled.li`
104 | text-align: center;
105 |
106 | & > div {
107 | padding: 3rem;
108 |
109 | h2 {
110 | margin-bottom: 0.75rem;
111 | }
112 |
113 | button {
114 | padding: 0.5rem 1rem;
115 | }
116 | }
117 | `
118 |
--------------------------------------------------------------------------------
/src/containers/Login.tsx:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup"
2 | import styled from "styled-components"
3 | import { useDispatch } from "react-redux"
4 | import { Link } from "react-router-dom"
5 | import { Formik, Form, Field } from "formik"
6 |
7 | import * as authAction from "../store/actions/auth"
8 | import theme from "../styles/ThemeStyles"
9 | import TwitterButton from "../components/Common/TwitterButton"
10 | import TwitterContainer from "../components/Common/TwitterContainer"
11 | import useAppSelector from "../hooks/useAppSelector"
12 |
13 | interface LoginFormValues {
14 | email: string
15 | password: string
16 | remberMe: boolean
17 | }
18 |
19 | const loginSchema = Yup.object().shape({
20 | email: Yup.string()
21 | .required("Please enter your email")
22 | .email("Your email is invalid"),
23 | password: Yup.string().min(8).required("Please enter your password"),
24 | })
25 |
26 | export default function Login() {
27 | const dispatch = useDispatch()
28 | const authorize: any = useAppSelector(state => state.authorize)
29 |
30 | const initialValues: LoginFormValues = {
31 | email: "",
32 | password: "",
33 | remberMe: false,
34 | }
35 |
36 | return (
37 |
38 |
39 |
46 |
47 |
48 |
49 | Login to Twitter
50 | {
54 | dispatch(
55 | authAction.loginUser({
56 | email: values.email,
57 | password: values.password,
58 | })
59 | )
60 | window.location.reload()
61 | }}
62 | >
63 | {({ errors, touched }) => (
64 |
114 | )}
115 |
116 |
117 |
118 |
119 | )
120 | }
121 |
122 | const Wrapper = styled.div`
123 | display: flex;
124 | min-height: 100vh;
125 | `
126 |
127 | const Image = styled.div`
128 | width: 50%;
129 | display: grid;
130 | place-items: center;
131 | backdrop-filter: blur(20px);
132 | background-image: url("/img/auth-bg.png");
133 |
134 | svg {
135 | width: 15rem;
136 | }
137 | `
138 |
139 | const Content = styled.div`
140 | width: 50%;
141 | padding: 5rem;
142 |
143 | form {
144 | gap: 1rem;
145 | display: grid;
146 | margin-top: 2rem;
147 | user-select: none;
148 |
149 | & > div {
150 | display: flex;
151 | flex-direction: column;
152 | gap: 0.5rem;
153 |
154 | input {
155 | border: 1px solid ${theme.dark.backgroundBox};
156 | padding: 0.75rem 1rem;
157 | color: ${theme.dark.text1};
158 | background: transparent;
159 | border-radius: 0.5rem;
160 | transition: ${theme.transition.ease};
161 |
162 | &:focus {
163 | border-color: ${theme.colors.blue};
164 | box-shadow: 0 0 0 2px rgba(29, 162, 243, 0.5);
165 | }
166 | }
167 |
168 | small {
169 | color: ${theme.colors.red};
170 | font-size: 0.8rem;
171 | }
172 | }
173 |
174 | a {
175 | color: ${theme.colors.blue};
176 | }
177 |
178 | button {
179 | margin-top: 1rem;
180 | }
181 | }
182 | `
183 |
184 | const Title = styled.h1`
185 | font-size: 2rem;
186 | font-weight: 600;
187 | `
188 |
--------------------------------------------------------------------------------
/src/containers/Logout.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import * as authService from "../services/auth"
3 |
4 | export default function Logout() {
5 | const [logouting, setLogouting] = useState(false)
6 |
7 | useEffect(() => {
8 | setLogouting(true)
9 | authService.logout().then(() => {
10 | setTimeout(() => {
11 | setLogouting(false)
12 | window.location.reload()
13 | }, 3000)
14 | })
15 | }, [])
16 |
17 | return {logouting && "Logging out..."}
18 | }
19 |
--------------------------------------------------------------------------------
/src/containers/Notifications.tsx:
--------------------------------------------------------------------------------
1 | export default function Notifications() {
2 | return Notifications
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/Profile.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components"
2 | import { useEffect, useState } from "react"
3 | import { useParams } from "react-router-dom"
4 | import { useDispatch } from "react-redux"
5 |
6 | import TwitterContainer from "../components/Common/TwitterContainer"
7 | import UserInfo from "../components/Core/UserInfo"
8 | import ProfileActions from "../components/Core/ProfileActions"
9 |
10 | import * as profileAction from "../store/actions/profile"
11 | import * as tweetsAction from "../store/actions/tweets"
12 |
13 | import theme from "../styles/ThemeStyles"
14 | import useAuth from "../hooks/useAuth"
15 | import TwitterBox from "../components/Common/TwitterBox"
16 | import Tweet from "../components/Common/Tweet"
17 | import { useUsersTweets } from "../hooks/useTweets"
18 | import TwitterFullscreen from "../components/Common/TwitterFullscreen"
19 |
20 | export default function Profile() {
21 | const [openCover, setOpenCover] = useState(false)
22 | const [openPicture, setOpenPicture] = useState(false)
23 | const [tab, setTab] = useState("tweets")
24 |
25 | const dispatch = useDispatch()
26 | const params: any = useParams()
27 |
28 | const { loading, user }: any = useAuth()
29 | const { tweets } = useUsersTweets()
30 |
31 | useEffect(() => {
32 | dispatch(profileAction.getUserProfile(params.username))
33 | dispatch(tweetsAction.getUserTweets())
34 | }, [dispatch, params.username])
35 |
36 | const handleCoverClick = () => {
37 | if (!!user.cover) setOpenCover(true)
38 | }
39 |
40 | return (
41 |
42 |
43 | {user?.cover && (
44 |
45 | )}
46 |
47 | setOpenCover(false)}
53 | />
54 | setOpenPicture(false)}
60 | />
61 |
62 |
63 |
64 | setOpenPicture(true)}
68 | />
69 |
70 |
71 |
72 |
73 |
74 |
75 | setTab("tweets")}
78 | >
79 | Tweets
80 |
81 | setTab("media")}
84 | >
85 | Media
86 |
87 | setTab("likes")}
90 | >
91 | Likes
92 |
93 |
94 |
95 |
96 | {tweets?.length !== 0 ? (
97 | tweets
98 | ?.map((tweet: any) => (
99 |
110 | ))
111 | .reverse()
112 | ) : (
113 | {user?.name} has not tweeted yet
114 | )}
115 |
116 |
117 |
118 |
119 |
120 |
121 | )
122 | }
123 |
124 | type ICover = {
125 | hasCover?: boolean
126 | }
127 |
128 | const Wrapper = styled.div``
129 |
130 | const Cover = styled.div`
131 | width: 100%;
132 | height: 17rem;
133 | overflow: hidden;
134 | background: ${theme.colors.blue};
135 |
136 | ${props => props.hasCover && `cursor: pointer;`}
137 |
138 | img {
139 | width: 100%;
140 | height: 100%;
141 | object-fit: cover;
142 | }
143 | `
144 |
145 | const Content = styled.div`
146 | display: flex;
147 | gap: 1rem;
148 | transform: translateY(-3rem);
149 | `
150 |
151 | const Group = styled.div`
152 | display: flex;
153 | flex-direction: column;
154 | gap: 1rem;
155 | `
156 |
157 | const Main = styled.div`
158 | flex: 1;
159 |
160 | & > div {
161 | padding: 0;
162 | overflow: hidden;
163 | }
164 | `
165 |
166 | const Header = styled.header`
167 | border-bottom: 1px solid ${theme.dark.backgroundPrimary};
168 | `
169 |
170 | const Tabs = styled.ul`
171 | display: flex;
172 | user-select: none;
173 | `
174 |
175 | type TabProps = {
176 | isActive?: boolean
177 | isDisabled?: boolean
178 | }
179 |
180 | const Tab = styled.li`
181 | cursor: pointer;
182 | padding: 1rem;
183 | color: ${theme.dark.text2};
184 | border-bottom: 1px solid transparent;
185 | transition: ${theme.transition.ease};
186 |
187 | ${props =>
188 | props.isActive &&
189 | css`
190 | color: ${theme.colors.blue};
191 | border-bottom-color: ${theme.colors.blue};
192 | `};
193 |
194 | ${props =>
195 | props.isDisabled &&
196 | css`
197 | opacity: 0.5;
198 | pointer-events: none;
199 | `}
200 | `
201 |
202 | const NotTwitted = styled.li`
203 | height: 15rem;
204 | display: grid;
205 | place-items: center;
206 | `
207 |
--------------------------------------------------------------------------------
/src/containers/Register.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 | import { Link, useNavigate } from "react-router-dom"
6 |
7 | import theme from "../styles/ThemeStyles"
8 | import * as authAction from "../store/actions/auth"
9 | import TwitterButton from "../components/Common/TwitterButton"
10 | import TwitterContainer from "../components/Common/TwitterContainer"
11 |
12 | interface RegisterFormValues {
13 | name: string
14 | username: string
15 | email: string
16 | password: string
17 | }
18 |
19 | const registerSchema = Yup.object().shape({
20 | name: Yup.string().required("Please enter your name"),
21 | username: Yup.string().required("Please enter your username"),
22 | email: Yup.string()
23 | .required("Please enter your emaill")
24 | .email("Your email is invalid"),
25 | password: Yup.string().min(8).required("Please enter your password"),
26 | })
27 |
28 | export default function Register() {
29 | const navigate = useNavigate()
30 | const dispatch = useDispatch()
31 |
32 | const initialValues: RegisterFormValues = {
33 | name: "",
34 | username: "",
35 | email: "",
36 | password: "",
37 | }
38 |
39 | return (
40 |
41 |
42 |
49 |
50 |
51 |
52 | Create an account
53 | {
57 | dispatch(
58 | authAction.registerUser({
59 | name: values.name,
60 | username: values.username,
61 | email: values.email,
62 | password: values.password,
63 | })
64 | )
65 |
66 | resetForm()
67 | navigate("/")
68 | }}
69 | >
70 | {({ errors, touched }) => (
71 |
127 | )}
128 |
129 |
130 |
131 |
132 | )
133 | }
134 |
135 | const Wrapper = styled.div`
136 | display: flex;
137 | min-height: 100vh;
138 | `
139 |
140 | const Image = styled.div`
141 | width: 50%;
142 | display: grid;
143 | place-items: center;
144 | background-image: url("/img/auth-bg.png");
145 |
146 | svg {
147 | width: 15rem;
148 | }
149 | `
150 |
151 | const Content = styled.div`
152 | width: 50%;
153 | padding: 5rem;
154 |
155 | form {
156 | gap: 1rem;
157 | display: grid;
158 | margin-top: 2rem;
159 | user-select: none;
160 |
161 | & > div {
162 | display: flex;
163 | flex-direction: column;
164 | gap: 0.5rem;
165 |
166 | input {
167 | border: 1px solid ${theme.dark.backgroundBox};
168 | padding: 0.75rem 1rem;
169 | color: ${theme.dark.text1};
170 | background: transparent;
171 | border-radius: 0.5rem;
172 | transition: ${theme.transition.ease};
173 |
174 | &:focus {
175 | border-color: ${theme.colors.blue};
176 | box-shadow: 0 0 0 2px rgba(29, 162, 243, 0.5);
177 | }
178 | }
179 |
180 | small {
181 | color: ${theme.colors.red};
182 | font-size: 0.8rem;
183 | }
184 | }
185 |
186 | a {
187 | color: ${theme.colors.blue};
188 | }
189 |
190 | button {
191 | margin-top: 1rem;
192 | }
193 | }
194 | `
195 |
196 | const Title = styled.h1`
197 | font-size: 2rem;
198 | font-weight: 600;
199 | `
200 |
--------------------------------------------------------------------------------
/src/containers/User.tsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components"
2 | import { useDispatch } from "react-redux"
3 | import { useEffect, useState } from "react"
4 | import { useNavigate, useParams } from "react-router-dom"
5 | import Skeleton from "react-loading-skeleton"
6 |
7 | import TwitterContainer from "../components/Common/TwitterContainer"
8 | import UserInfo from "../components/Core/UserInfo"
9 |
10 | import * as userService from "../services/user"
11 | import * as authAction from "../store/actions/auth"
12 | import useAuth from "../hooks/useAuth"
13 |
14 | import theme from "../styles/ThemeStyles"
15 | import TwitterBox from "../components/Common/TwitterBox"
16 | import Tweet from "../components/Common/Tweet"
17 | import TwitterFullscreen from "../components/Common/TwitterFullscreen"
18 | import TweetSkeleton from "../components/Skeleton/TweetSkeleton"
19 | import UserActions from "../components/Core/UserActions"
20 | import { IUser } from "../types/schemas"
21 |
22 | export default function User() {
23 | const auth: any = useAuth()
24 | const navigate = useNavigate()
25 | const dispatch = useDispatch()
26 |
27 | const [user, setUser] = useState({} as IUser)
28 | const [follow, setFollow] = useState(false)
29 | const [loading, setLoading] = useState(true)
30 |
31 | const [openCover, setOpenCover] = useState(false)
32 | const [openPicture, setOpenPicture] = useState(false)
33 | const [tab, setTab] = useState("tweets")
34 |
35 | const params: any = useParams()
36 |
37 | useEffect(() => {
38 | return () => {
39 | dispatch(authAction.getUser())
40 | }
41 | // eslint-disable-next-line
42 | }, [])
43 |
44 | useEffect(() => {
45 | if (params.username === auth.user.username) {
46 | navigate("/profile")
47 | } else {
48 | getUserProfile()
49 | }
50 | // eslint-disable-next-line
51 | }, [params.username])
52 |
53 | useEffect(() => {
54 | setFollow(auth.user?.following?.includes(user._id) || false)
55 | // eslint-disable-next-line
56 | }, [user])
57 |
58 | const getUserProfile = async () => {
59 | setLoading(true)
60 | const res = await userService.getUser(params.username)
61 | if (res.success) setUser({ ...res.user, tweets: res.tweets })
62 | setLoading(false)
63 | }
64 |
65 | const followUser = async () => {
66 | if (user?._id && user?.followers) {
67 | const res = await userService.followUser(auth.user._id, user._id)
68 |
69 | if (res.success) {
70 | user.followers.push(auth.user._id)
71 | setFollow(true)
72 | }
73 | }
74 | }
75 |
76 | const unfollowUser = async () => {
77 | if (user?._id && user?.followers) {
78 | const res = await userService.unfollowUser(auth.user._id, user._id)
79 |
80 | if (res.success) {
81 | user.followers.splice(user.followers.indexOf(auth.user._id), 1)
82 | setFollow(false)
83 | }
84 | }
85 | }
86 |
87 | const handleCoverClick = () => {
88 | if (!!user.cover) setOpenCover(true)
89 | }
90 |
91 | let $cover_content = null
92 | if (loading) {
93 | $cover_content =
94 | } else {
95 | if (user?.cover) {
96 | $cover_content = (
97 |
98 | )
99 | }
100 | }
101 |
102 | let $tweets_content = null
103 | if (loading) {
104 | $tweets_content = (
105 | <>
106 |
107 |
108 | >
109 | )
110 | } else {
111 | if (user?.tweets?.length !== 0) {
112 | $tweets_content = user?.tweets?.map(tweet => (
113 |
124 | ))
125 | } else {
126 | $tweets_content = (
127 | {user?.name} has not tweeted yet
128 | )
129 | }
130 | }
131 |
132 | return (
133 |
134 |
135 | {$cover_content}
136 |
137 | {user?.cover && (
138 | setOpenCover(false)}
144 | />
145 | )}
146 | {user?.image && (
147 | setOpenPicture(false)}
153 | />
154 | )}
155 |
156 |
157 |
158 | setOpenPicture(true)}
162 | />
163 |
169 |
170 |
171 |
172 |
173 |
174 | setTab("tweets")}
177 | >
178 | Tweets
179 |
180 | setTab("media")}
183 | >
184 | Media
185 |
186 | setTab("likes")}
189 | >
190 | Likes
191 |
192 |
193 |
194 | {$tweets_content}
195 |
196 |
197 |
198 |
199 |
200 | )
201 | }
202 |
203 | interface ICover {
204 | hasCover?: boolean
205 | }
206 |
207 | const Wrapper = styled.div``
208 |
209 | const Cover = styled.div`
210 | width: 100%;
211 | height: 17rem;
212 | overflow: hidden;
213 | background: ${theme.colors.blue};
214 |
215 | ${props => props.hasCover && `cursor: pointer;`}
216 |
217 | img {
218 | width: 100%;
219 | height: 100%;
220 | object-fit: cover;
221 | }
222 |
223 | & > span {
224 | display: block;
225 | height: 100%;
226 |
227 | .react-loading-skeleton {
228 | height: 100%;
229 | border-radius: 0;
230 | }
231 | }
232 | `
233 |
234 | const Content = styled.div`
235 | display: flex;
236 | gap: 1rem;
237 | transform: translateY(-3rem);
238 | `
239 |
240 | const Group = styled.div`
241 | display: flex;
242 | flex-direction: column;
243 | gap: 1rem;
244 | `
245 |
246 | const Main = styled.div`
247 | flex: 1;
248 |
249 | & > div {
250 | padding: 0;
251 | overflow: hidden;
252 | }
253 | `
254 |
255 | const NotTwitted = styled.li`
256 | height: 15rem;
257 | display: grid;
258 | place-items: center;
259 | `
260 |
261 | const Header = styled.header`
262 | border-bottom: 1px solid ${theme.dark.backgroundPrimary};
263 | `
264 |
265 | const Tabs = styled.ul`
266 | display: flex;
267 | user-select: none;
268 | `
269 |
270 | type TabProps = {
271 | isActive?: boolean
272 | isDisabled?: boolean
273 | }
274 |
275 | const Tab = styled.li`
276 | cursor: pointer;
277 | padding: 1rem;
278 | color: ${theme.dark.text2};
279 | border-bottom: 1px solid transparent;
280 | transition: ${theme.transition.ease};
281 |
282 | ${props =>
283 | props.isActive &&
284 | css`
285 | color: ${theme.colors.blue};
286 | border-bottom-color: ${theme.colors.blue};
287 | `};
288 |
289 | ${props =>
290 | props.isDisabled &&
291 | css`
292 | opacity: 0.5;
293 | pointer-events: none;
294 | `}
295 | `
296 |
297 | const Tweets = styled.ul`
298 | div {
299 | box-shadow: none;
300 | }
301 | `
302 |
--------------------------------------------------------------------------------
/src/helpers/button-component.tsx:
--------------------------------------------------------------------------------
1 | import theme from '../styles/ThemeStyles'
2 |
3 | type variant = 'solid' | 'outline' | 'ghost' | 'link'
4 |
5 | export function getButtonColors(variant: variant) {
6 | switch (variant) {
7 | case 'solid':
8 | return [
9 | theme.dark.text1,
10 | theme.dark.primary,
11 | theme.dark.primaryHover,
12 | theme.dark.primaryActive
13 | ]
14 | case 'outline':
15 | return [
16 | theme.dark.primary,
17 | theme.dark.primary,
18 | theme.dark.primaryHover,
19 | theme.dark.primaryActive
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useAppSelector.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useSelector } from "react-redux"
2 | import type { AppState } from "../store/index"
3 |
4 | const useAppSelector: TypedUseSelectorHook = useSelector
5 | export default useAppSelector
6 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.tsx:
--------------------------------------------------------------------------------
1 | import useAppSelector from "./useAppSelector"
2 |
3 | export default function useAuth() {
4 | const { loading, user, hasUser } = useAppSelector(state => state.authorize)
5 | const { notifications }: any = useAppSelector(state => state.notifications)
6 |
7 | const unreadNotification = notifications?.filter(
8 | (notification: any) => !notification.read
9 | )
10 |
11 | return {
12 | loading,
13 | user,
14 | isLogin: hasUser,
15 | notifications,
16 | unreadNotification,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/hooks/useTweets.tsx:
--------------------------------------------------------------------------------
1 | import useAppSelector from "./useAppSelector"
2 |
3 | export function useUsersTweets() {
4 | const userTweets: any = useAppSelector(state => state.userTweets)
5 |
6 | return {
7 | loading: userTweets.loading,
8 | error: userTweets.error,
9 | tweets: userTweets.tweets,
10 | tweetsCount: userTweets.tweets.length,
11 | }
12 | }
13 |
14 | export function useHomeTweets() {
15 | const homeTweets: any = useAppSelector(state => state.homeTweets)
16 |
17 | return {
18 | loading: homeTweets.loading,
19 | error: homeTweets.error,
20 | tweets: homeTweets.tweets,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import 'simplebar/dist/simplebar.min.css'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import { Provider } from 'react-redux'
6 |
7 | import App from './containers/App'
8 | import GlobalStyles from './styles/GlobalStyles'
9 | import store from './store'
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById('twitter-root')
19 | )
20 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/services/auth.ts:
--------------------------------------------------------------------------------
1 | import axios from "../config/axios"
2 | import { IUser } from "../types/schemas"
3 |
4 | interface IUserLogin {
5 | email: string
6 | password: string
7 | }
8 |
9 | interface IUserRegister {
10 | name: string
11 | username: string
12 | email: string
13 | password: string
14 | }
15 |
16 | export async function getLoggedInUser(): Promise<{
17 | success: boolean
18 | user?: IUser
19 | message?: any
20 | }> {
21 | try {
22 | const { data } = await axios.get("/user")
23 | return data.user
24 | ? { success: true, user: data.user }
25 | : { success: false, message: data.message }
26 | } catch (err) {
27 | return { success: false, message: err }
28 | }
29 | }
30 |
31 | export async function login({ email, password }: IUserLogin) {
32 | const user = { email, password }
33 |
34 | try {
35 | const { data } = await axios.post("/login", user)
36 |
37 | return data.error
38 | ? { success: false, message: data.message }
39 | : { success: true, user: data.user }
40 | } catch (err) {
41 | return err
42 | }
43 | }
44 |
45 | export async function logout() {
46 | try {
47 | const { data } = await axios.post("/logout")
48 |
49 | return data.error
50 | ? { success: false, message: data.message }
51 | : { success: true }
52 | } catch (err) {
53 | return err
54 | }
55 | }
56 |
57 | export async function register({
58 | name,
59 | username,
60 | email,
61 | password,
62 | }: IUserRegister) {
63 | const user = { name, email, username, password }
64 |
65 | try {
66 | const { data } = await axios.post("/register", user)
67 |
68 | return data.error
69 | ? { success: false, message: data.message }
70 | : { success: true, user: data.user }
71 | } catch (err) {
72 | return err
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/services/notification.ts:
--------------------------------------------------------------------------------
1 | import axios from "../config/axios"
2 |
3 | export async function getNotifications() {
4 | try {
5 | const { data } = await axios.get("/notifications")
6 |
7 | console.log(data)
8 |
9 | return data.success
10 | ? { success: true, notifications: data.notifications }
11 | : { success: false, message: data.message }
12 | } catch (err) {
13 | return { success: false, message: err }
14 | }
15 | }
16 |
17 | export async function addNotification(text: string, tweetId: string) {
18 | try {
19 | const { data } = await axios.post("/notifications", {
20 | verb: "notif",
21 | text,
22 | tweetId,
23 | })
24 |
25 | return data.success
26 | ? { success: true }
27 | : { success: false, message: data.message }
28 | } catch (err) {
29 | return { success: false, message: err }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/services/profile.ts:
--------------------------------------------------------------------------------
1 | import axios from "../config/axios"
2 |
3 | export async function get(username: string): Promise<{
4 | success: boolean
5 | user?: any
6 | message?: any
7 | }> {
8 | try {
9 | const { data } = await axios.get(`/users/${username}`)
10 |
11 | return data.error
12 | ? { success: false, message: data.message }
13 | : { success: true, user: data.user }
14 | } catch (err) {
15 | return { success: false, message: err }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/services/tweet.ts:
--------------------------------------------------------------------------------
1 | import axios from "../config/axios"
2 | import { ITweet } from "../types/schemas"
3 |
4 | export async function getUserTweets() {
5 | try {
6 | const { data } = await axios.get("/tweets")
7 |
8 | return data.error
9 | ? { success: false, message: data.error }
10 | : { success: true, tweets: data }
11 | } catch (err) {
12 | return { success: false, message: err }
13 | }
14 | }
15 |
16 | export async function getHomeTweets(): Promise<{
17 | success: boolean
18 | tweets?: ITweet[]
19 | message?: any
20 | }> {
21 | try {
22 | const { data } = await axios.get("/tweets/timeline")
23 |
24 | return data.error
25 | ? { success: false, message: data.error }
26 | : { success: true, tweets: data }
27 | } catch (err) {
28 | return { success: false, message: err }
29 | }
30 | }
31 |
32 | export async function createTweet(text: string) {
33 | try {
34 | const { data } = await axios.post("/tweets", { text })
35 |
36 | return data.error
37 | ? { success: false, message: data.error }
38 | : { success: true, tweet: data }
39 | } catch (err) {
40 | return { success: false, message: err }
41 | }
42 | }
43 |
44 | export async function updateLikeStatusTweet(tweetId: string, liked: boolean) {
45 | try {
46 | const { data } = await axios.post(`/tweets/${tweetId}/like`, { liked })
47 |
48 | return data.error
49 | ? { success: false, message: data.error }
50 | : { success: true }
51 | } catch (err) {
52 | return { success: false, message: err }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/services/user.ts:
--------------------------------------------------------------------------------
1 | import axios from '../config/axios'
2 |
3 | export async function getUser(username: string) {
4 | try {
5 | const { data } = await axios.get(`/users/${username}`)
6 |
7 | return data.error
8 | ? { success: false, message: data.error }
9 | : { success: true, user: data.user, tweets: data.tweets }
10 | } catch (err) {
11 | return { success: false, message: err }
12 | }
13 | }
14 |
15 | export async function updateUser(id: string, data: object) {
16 | try {
17 | const res = await axios.put(`/users/${id}`, data)
18 |
19 | return res.data.error
20 | ? { success: false, message: res.data.error }
21 | : { success: true, user: res.data.user }
22 | } catch (err) {
23 | return { success: false, message: err }
24 | }
25 | }
26 |
27 | export async function followUser(userId: string, followerId: string) {
28 | try {
29 | const { data } = await axios.post('/follow', { userId, followerId })
30 |
31 | return data.error
32 | ? { success: false, message: data.error }
33 | : { success: true }
34 | } catch (err) {
35 | return { success: false, message: err }
36 | }
37 | }
38 |
39 | export async function unfollowUser(userId: string, followerId: string) {
40 | try {
41 | const { data } = await axios.post('/unfollow', { userId, followerId })
42 |
43 | return data.error
44 | ? { success: false, message: data.error }
45 | : { success: true }
46 | } catch (err) {
47 | return { success: false, message: err }
48 | }
49 | }
50 |
51 | export async function randomUsers(number: number) {
52 | try {
53 | const { data } = await axios.get(`/users/random/${number}`)
54 |
55 | return data.error
56 | ? { success: false, message: data.error }
57 | : { success: true, users: data }
58 | } catch (err) {
59 | return { success: false, message: err }
60 | }
61 | }
62 |
63 | export async function removeCover(userId: string) {
64 | try {
65 | const { data } = await axios.delete(`/users/${userId}/remove-cover`)
66 |
67 | return data.error
68 | ? { success: false, message: data.error }
69 | : { success: true, user: data }
70 | } catch (err) {
71 | return { success: false, message: err }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/store/actions/auth.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "redux"
2 | import * as types from "../types"
3 |
4 | import * as authService from "../../services/auth"
5 | import * as tweetService from "../../services/tweet"
6 |
7 | interface User {
8 | email: string
9 | password: string
10 | }
11 |
12 | interface RegisterUser {
13 | name: string
14 | username: string
15 | email: string
16 | password: string
17 | }
18 |
19 | export const getUser = () => async (dispatch: Dispatch) => {
20 | dispatch({ type: types.GET_AUTHORIZE_USER_REQUEST })
21 |
22 | try {
23 | const res = await authService.getLoggedInUser()
24 |
25 | res.success
26 | ? dispatch({ type: types.GET_AUTHORIZE_USER_SUCCESS, user: res.user })
27 | : dispatch({ type: types.GET_AUTHORIZE_USER_FAILURE, error: res.message })
28 | } catch (error) {
29 | dispatch({ type: types.GET_AUTHORIZE_USER_FAILURE, error })
30 | }
31 | }
32 |
33 | export const getHomeTweets = () => async (dispatch: Dispatch) => {
34 | dispatch({ type: types.GET_HOME_TWEETS_REQUEST })
35 |
36 | try {
37 | const res = await tweetService.getHomeTweets()
38 |
39 | res.success
40 | ? dispatch({ type: types.GET_HOME_TWEETS_SUCCESS, tweets: res.tweets })
41 | : dispatch({ type: types.GET_HOME_TWEETS_FAILURE, error: res.message })
42 | } catch (error) {
43 | dispatch({ type: types.GET_HOME_TWEETS_FAILURE, error })
44 | }
45 | }
46 |
47 | export const loginUser = (user: User) => async (dispatch: Dispatch) => {
48 | const { email, password } = user
49 | dispatch({ type: types.LOGIN_USER_REQUEST })
50 |
51 | try {
52 | const { data }: any = await authService.login({ email, password })
53 | dispatch({ type: types.LOGIN_USER_SUCCESS, user: data.user })
54 | } catch (error) {
55 | dispatch({ type: types.LOGIN_USER_FAILURE, error })
56 | }
57 | }
58 |
59 | export const registerUser =
60 | (user: RegisterUser) => async (dispatch: Dispatch) => {
61 | const { name, username, email, password } = user
62 | dispatch({ type: types.REGISTER_USER_REQUEST })
63 |
64 | try {
65 | const res: any = await authService.register({
66 | name,
67 | username,
68 | email,
69 | password,
70 | })
71 |
72 | res.success
73 | ? dispatch({ type: types.REGISTER_USER_SUCCESS, user: res.user })
74 | : dispatch({ type: types.REGISTER_USER_FAILURE, error: res.message })
75 | } catch (error) {
76 | dispatch({ type: types.REGISTER_USER_FAILURE, error })
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/store/actions/notifications.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "redux"
2 | import * as types from "../types"
3 | import * as notificationService from "../../services/notification"
4 |
5 | export const getNotifications = () => async (dispatch: Dispatch) => {
6 | dispatch({ type: types.GET_NOTIFICATIONS_REQUEST })
7 |
8 | try {
9 | const res = await notificationService.getNotifications()
10 |
11 | res.success
12 | ? dispatch({
13 | type: types.GET_NOTIFICATIONS_SUCCESS,
14 | notifications: res.notifications,
15 | })
16 | : dispatch({ type: types.GET_NOTIFICATIONS_FAILURE, error: res.message })
17 | } catch (error) {
18 | dispatch({ type: types.GET_NOTIFICATIONS_FAILURE, error })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/store/actions/profile.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "redux"
2 | import * as types from "../types"
3 |
4 | import * as profileService from "../../services/profile"
5 | import * as userService from "../../services/user"
6 |
7 | export const getUserProfile =
8 | (username: string) => async (dispatch: Dispatch) => {
9 | dispatch({ type: types.GET_USER_PROFILE_REQUEST })
10 |
11 | try {
12 | const res = await profileService.get(username)
13 |
14 | res.success
15 | ? dispatch({ type: types.GET_USER_PROFILE_SUCCESS, user: res.user })
16 | : dispatch({ type: types.GET_USER_PROFILE_FAILURE, error: res.message })
17 | } catch (error) {
18 | dispatch({ type: types.GET_USER_PROFILE_FAILURE, error })
19 | }
20 | }
21 |
22 | export const updateUserProfile = (id: string, data: object) => {
23 | return async (dispatch: Dispatch) => {
24 | dispatch({ type: types.UPDATE_USER_PROFILE_REQUEST })
25 |
26 | try {
27 | const res = await userService.updateUser(id, data)
28 |
29 | res.success
30 | ? dispatch({ type: types.UPDATE_USER_PROFILE_SUCCESS, user: res.user })
31 | : dispatch({
32 | type: types.UPDATE_USER_PROFILE_FAILURE,
33 | error: res.message,
34 | })
35 | } catch (error) {
36 | dispatch({ type: types.UPDATE_USER_PROFILE_FAILURE, error })
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/store/actions/tweets.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux'
2 | import * as types from '../types'
3 | import * as tweetService from '../../services/tweet'
4 |
5 | export const getUserTweets = () => async (dispatch: Dispatch) => {
6 | dispatch({ type: types.GET_USER_TWEETS_REQUEST })
7 |
8 | try {
9 | const res = await tweetService.getUserTweets()
10 |
11 | res.success
12 | ? dispatch({ type: types.GET_USER_TWEETS_SUCCESS, tweets: res.tweets })
13 | : dispatch({ type: types.GET_USER_TWEETS_FAILURE, error: res.message })
14 | } catch (error) {
15 | dispatch({ type: types.GET_USER_TWEETS_FAILURE, error })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from "redux"
2 | import { composeWithDevTools } from "redux-devtools-extension"
3 | import thunk from "redux-thunk"
4 |
5 | import {
6 | authorizeReducer,
7 | loginReducer,
8 | registerReducer,
9 | } from "./reducers/auth"
10 | import { notificationsReducer } from "./reducers/notifications"
11 | import { profileReducer } from "./reducers/profile"
12 | import { userTweetsReducer, homeTweetsReducer } from "./reducers/tweet"
13 |
14 | const reducer = combineReducers({
15 | authorize: authorizeReducer,
16 | login: loginReducer,
17 | profile: profileReducer,
18 | register: registerReducer,
19 | userTweets: userTweetsReducer,
20 | homeTweets: homeTweetsReducer,
21 | notifications: notificationsReducer,
22 | })
23 |
24 | const initialState = {}
25 | const middleware = [thunk]
26 |
27 | const store = createStore(
28 | reducer,
29 | initialState,
30 | composeWithDevTools(applyMiddleware(...middleware))
31 | )
32 |
33 | export type AppState = ReturnType
34 | export type AppDispatch = typeof store.dispatch
35 |
36 | export default store
37 |
--------------------------------------------------------------------------------
/src/store/reducers/auth.ts:
--------------------------------------------------------------------------------
1 | import { IUser } from "../../types/schemas"
2 | import * as types from "../types"
3 |
4 | interface Action {
5 | type: string
6 | user: IUser
7 | hasUser: boolean
8 | error?: string
9 | }
10 |
11 | type InitialStateType = {
12 | user: IUser | null
13 | hasUser: boolean
14 | error: string | null
15 | loading: boolean
16 | }
17 |
18 | const initialState: InitialStateType = {
19 | loading: false,
20 | user: null,
21 | error: "",
22 | hasUser: false,
23 | }
24 |
25 | export function authorizeReducer(state = initialState, action: Action) {
26 | switch (action.type) {
27 | case types.GET_AUTHORIZE_USER_REQUEST:
28 | return { loading: true, user: null }
29 | case types.GET_AUTHORIZE_USER_SUCCESS:
30 | return { loading: false, user: action.user, hasUser: true }
31 | case types.GET_AUTHORIZE_USER_FAILURE:
32 | return { loading: false, user: null, error: action.error, hasUser: false }
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export function loginReducer(state = initialState, action: Action) {
39 | switch (action.type) {
40 | case types.LOGIN_USER_REQUEST:
41 | return { loading: true, user: {} }
42 | case types.LOGIN_USER_SUCCESS:
43 | return { loading: false, user: action.user }
44 | case types.LOGIN_USER_FAILURE:
45 | return { loading: false, user: {} }
46 | default:
47 | return state
48 | }
49 | }
50 |
51 | export function registerReducer(state = initialState, action: Action) {
52 | switch (action.type) {
53 | case types.REGISTER_USER_REQUEST:
54 | return { loading: true, user: {} }
55 | case types.REGISTER_USER_SUCCESS:
56 | return { loading: false, user: action.user }
57 | case types.REGISTER_USER_FAILURE:
58 | return { loading: false, user: {} }
59 | default:
60 | return state
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/store/reducers/notifications.ts:
--------------------------------------------------------------------------------
1 | import * as types from "../types"
2 |
3 | interface Action {
4 | type: string
5 | notifications: object[]
6 | error?: string
7 | }
8 |
9 | const initialState = { loading: false, error: "", notifications: [] }
10 |
11 | export function notificationsReducer(state = initialState, action: Action) {
12 | switch (action.type) {
13 | case types.GET_NOTIFICATIONS_REQUEST:
14 | return { loading: true }
15 | case types.GET_NOTIFICATIONS_SUCCESS:
16 | return { loading: false, notifications: action.notifications }
17 | case types.GET_NOTIFICATIONS_FAILURE:
18 | return { loading: false, notifications: [], error: action.error }
19 | default:
20 | return state
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/store/reducers/profile.ts:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 |
3 | interface Action {
4 | type: string
5 | user: object
6 | error?: string
7 | }
8 |
9 | const initialState = {
10 | loading: false,
11 | isExist: false,
12 | error: '',
13 | user: {}
14 | }
15 |
16 | export function profileReducer(state = initialState, action: Action) {
17 | switch (action.type) {
18 | case types.GET_USER_PROFILE_REQUEST:
19 | return { loading: true }
20 | case types.GET_USER_PROFILE_SUCCESS:
21 | return { loading: false, user: action.user, isExist: true }
22 | case types.GET_USER_PROFILE_FAILURE:
23 | return { loading: false, user: {}, error: action.error, isExist: false }
24 | case types.UPDATE_USER_PROFILE_REQUEST:
25 | return { loading: true }
26 | case types.UPDATE_USER_PROFILE_SUCCESS:
27 | return { loading: false, user: action.user }
28 | case types.UPDATE_USER_PROFILE_FAILURE:
29 | return { loading: false, error: action.error }
30 | default:
31 | return state
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/store/reducers/tweet.ts:
--------------------------------------------------------------------------------
1 | import * as types from '../types'
2 |
3 | interface Action {
4 | type: string
5 | error: ''
6 | tweets: object[]
7 | }
8 |
9 | interface State {
10 | loading: boolean
11 | tweets: object[]
12 | }
13 |
14 | const initialState: State = { loading: false, tweets: [] }
15 |
16 | export function homeTweetsReducer(state = initialState, action: Action) {
17 | switch (action.type) {
18 | case types.GET_HOME_TWEETS_REQUEST:
19 | return { loading: true, tweets: [] }
20 | case types.GET_HOME_TWEETS_SUCCESS:
21 | return { loading: false, tweets: action.tweets }
22 | case types.GET_HOME_TWEETS_FAILURE:
23 | return { loading: false, error: action.error }
24 | default:
25 | return state
26 | }
27 | }
28 |
29 | export function userTweetsReducer(state = initialState, action: Action) {
30 | switch (action.type) {
31 | case types.GET_USER_TWEETS_REQUEST:
32 | return { loading: true, tweets: [] }
33 | case types.GET_USER_TWEETS_SUCCESS:
34 | return { loading: false, tweets: action.tweets }
35 | case types.GET_USER_TWEETS_FAILURE:
36 | return { loading: false, error: action.error }
37 | default:
38 | return state
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/store/types.ts:
--------------------------------------------------------------------------------
1 | export const GET_USER_PROFILE_REQUEST = "GET_USER_PROFILE_REQUEST"
2 | export const GET_USER_PROFILE_SUCCESS = "GET_USER_PROFILE_SUCCESS"
3 | export const GET_USER_PROFILE_FAILURE = "GET_USER_PROFILE_FAILURE"
4 |
5 | export const UPDATE_USER_PROFILE_REQUEST = "UPDATE_USER_PROFILE_REQUEST"
6 | export const UPDATE_USER_PROFILE_SUCCESS = "UPDATE_USER_PROFILE_SUCCESS"
7 | export const UPDATE_USER_PROFILE_FAILURE = "UPDATE_USER_PROFILE_FAILURE"
8 |
9 | export const GET_AUTHORIZE_USER_REQUEST = "GET_AUTHORIZE_USER_REQUEST"
10 | export const GET_AUTHORIZE_USER_SUCCESS = "GET_AUTHORIZE_USER_SUCCESS"
11 | export const GET_AUTHORIZE_USER_FAILURE = "GET_AUTHORIZE_USER_FAILURE"
12 |
13 | export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST"
14 | export const LOGIN_USER_SUCCESS = "LOGIN_USER_SUCCESS"
15 | export const LOGIN_USER_FAILURE = "LOGIN_USER_FAILURE"
16 |
17 | export const REGISTER_USER_REQUEST = "REGISTER_USER_REQUEST"
18 | export const REGISTER_USER_SUCCESS = "REGISTER_USER_SUCCESS"
19 | export const REGISTER_USER_FAILURE = "REGISTER_USER_FAILURE"
20 |
21 | export const GET_USER_TWEETS_REQUEST = "GET_USER_TWEETS_REQUEST"
22 | export const GET_USER_TWEETS_SUCCESS = "GET_USER_TWEETS_SUCCESS"
23 | export const GET_USER_TWEETS_FAILURE = "GET_USER_TWEETS_FAILURE"
24 |
25 | export const GET_HOME_TWEETS_REQUEST = "GET_HOME_TWEETS_REQUEST"
26 | export const GET_HOME_TWEETS_SUCCESS = "GET_HOME_TWEETS_SUCCESS"
27 | export const GET_HOME_TWEETS_FAILURE = "GET_HOME_TWEETS_FAILURE"
28 |
29 | export const GET_NOTIFICATIONS_REQUEST = "GET_NOTIFICATIONS_REQUEST"
30 | export const GET_NOTIFICATIONS_SUCCESS = "GET_NOTIFICATIONS_SUCCESS"
31 | export const GET_NOTIFICATIONS_FAILURE = "GET_NOTIFICATIONS_FAILURE"
32 |
--------------------------------------------------------------------------------
/src/styles/GlobalStyles.tsx:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 | import theme from './ThemeStyles'
3 |
4 | const GlobalStyles = createGlobalStyle`
5 | * {
6 | padding: 0;
7 | margin: 0;
8 | border: 0;
9 | outline: 0;
10 | font: inherit;
11 | box-sizing: border-box;
12 | background: transparent;
13 | }
14 |
15 | body {
16 | color: ${props =>
17 | props.theme === 'dark' ? theme.dark.text1 : theme.light.text1};
18 | background: ${props =>
19 | props.theme === 'dark'
20 | ? theme.dark.backgroundPrimary
21 | : theme.light.backgroundPrimary};
22 | font-family: Inter, sans-serif;
23 | }
24 |
25 | a {
26 | color: inherit;
27 | display: inline-block;
28 | text-decoration: none;
29 | }
30 |
31 | ul,
32 | ol {
33 | list-style: none;
34 | }
35 |
36 | button {
37 | cursor: pointer;
38 | }
39 | `
40 |
41 | export default GlobalStyles
42 |
--------------------------------------------------------------------------------
/src/styles/ThemeStyles.tsx:
--------------------------------------------------------------------------------
1 | const ThemeStyles = {
2 | colors: {
3 | transparent: 'transparent',
4 | blue: '#1da2f3',
5 | red: '#e45251',
6 | green: '#32c594'
7 | },
8 | light: {
9 | primary: '#40aff6',
10 | primaryHover: '#109df4',
11 | primaryActive: '#097fc8',
12 | text1: '#303436',
13 | text2: '#8d8d8d',
14 | backgroundBox: '#ffffff',
15 | backgroundPrimary: '#f4f8fb'
16 | },
17 | dark: {
18 | primary: '#40aff6',
19 | primaryHover: '#109df4',
20 | primaryActive: '#097fc8',
21 | text1: '#ffffff',
22 | text2: '#93969e',
23 | backgroundCard: '#3b434b',
24 | backgroundBox: '#2f3740',
25 | backgroundPrimary: '#262e36'
26 | },
27 | transition: {
28 | ease: 'all 0.3s ease'
29 | }
30 | }
31 |
32 | export default ThemeStyles
33 |
--------------------------------------------------------------------------------
/src/types/schemas.ts:
--------------------------------------------------------------------------------
1 | export interface IUser {
2 | _id: string
3 | name: string
4 | email: string
5 | password: string
6 |
7 | bio?: string
8 | website?: string
9 | birthday?: string
10 | username?: string
11 | location?: string
12 |
13 | cover?: string
14 | image?: string
15 |
16 | tweets?: ITweet[]
17 | followers?: string[]
18 | following?: string[]
19 | }
20 |
21 | export interface ITweet {
22 | _id: string
23 | text: string
24 |
25 | likes: string[]
26 | replies: number
27 | retweet: number
28 | }
29 |
30 | export interface IHomeTweets {
31 | tweet: ITweet
32 | user: IUser
33 | }
34 |
35 | export interface INotification {
36 | read: boolean
37 | text: string
38 | to: IUser[]
39 | from: IUser
40 | verb: "notif" | "like" | "follow" | "mention"
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils/routes.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentType, lazy, LazyExoticComponent } from "react"
2 |
3 | interface IRoute {
4 | path: string
5 | component: LazyExoticComponent>
6 | }
7 |
8 | const routes: IRoute[] = [
9 | { path: "/", component: lazy(() => import("../containers/Home")) },
10 | {
11 | path: "/notifications",
12 | component: lazy(() => import("../containers/Notifications")),
13 | },
14 | {
15 | path: "/profile",
16 | component: lazy(() => import("../containers/Profile")),
17 | },
18 | {
19 | path: "/profile/edit",
20 | component: lazy(() => import("../containers/EditProfile")),
21 | },
22 | {
23 | path: "/profile/logout",
24 | component: lazy(() => import("../containers/Logout")),
25 | },
26 | {
27 | path: "/user/:username",
28 | component: lazy(() => import("../containers/User")),
29 | },
30 | ]
31 |
32 | export default routes
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------