├── src ├── react-app-env.d.ts ├── assets │ └── images │ │ ├── profile.jpg │ │ └── logo.svg ├── containers │ ├── Notifications.tsx │ ├── Logout.tsx │ ├── App.tsx │ ├── Home.tsx │ ├── EditProfile.tsx │ ├── Login.tsx │ ├── Profile.tsx │ ├── Register.tsx │ └── User.tsx ├── config │ └── axios.ts ├── hooks │ ├── useAppSelector.ts │ ├── useAuth.tsx │ └── useTweets.tsx ├── constants │ ├── hashtags.tsx │ ├── navigation.tsx │ └── tweets.tsx ├── services │ ├── profile.ts │ ├── notification.ts │ ├── tweet.ts │ ├── auth.ts │ └── user.ts ├── components │ ├── Common │ │ ├── TwitterCard.tsx │ │ ├── TwitterSpinner.tsx │ │ ├── TwitterContainer.tsx │ │ ├── Layout.tsx │ │ ├── TwitterButton.tsx │ │ ├── TwitterFullscreen.tsx │ │ ├── TwitterBox.tsx │ │ ├── Header.tsx │ │ ├── Navigation.tsx │ │ └── Tweet.tsx │ ├── Skeleton │ │ └── TweetSkeleton.tsx │ ├── Core │ │ ├── ProfileActions.tsx │ │ ├── UserActions.tsx │ │ ├── TrendsForYou.tsx │ │ ├── YouShouldFollow.tsx │ │ └── UserInfo.tsx │ ├── Home │ │ └── WhatsHappening.tsx │ └── Forms │ │ └── EditProfileForm.tsx ├── index.tsx ├── helpers │ └── button-component.tsx ├── store │ ├── actions │ │ ├── tweets.ts │ │ ├── notifications.ts │ │ ├── profile.ts │ │ └── auth.ts │ ├── reducers │ │ ├── notifications.ts │ │ ├── profile.ts │ │ ├── tweet.ts │ │ └── auth.ts │ ├── index.ts │ └── types.ts ├── styles │ ├── ThemeStyles.tsx │ └── GlobalStyles.tsx ├── types │ └── schemas.ts └── utils │ └── routes.tsx ├── public ├── img │ ├── auth-bg.png │ ├── covers │ │ ├── soh3il.jpg │ │ ├── siavash.jpg │ │ ├── adamwathan.jpg │ │ └── javalaves.jpg │ └── users │ │ ├── siavash.jpg │ │ ├── soh3il.jpg │ │ ├── javalaves.jpg │ │ ├── neysidev.jpg │ │ ├── not_found.jpg │ │ ├── adamwathan.jpg │ │ ├── dan_abramov.jpg │ │ └── guillermo_rauch.jpg └── index.html ├── .gitignore ├── tsconfig.json └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/img/auth-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/auth-bg.png -------------------------------------------------------------------------------- /public/img/covers/soh3il.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/covers/soh3il.jpg -------------------------------------------------------------------------------- /public/img/users/siavash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/siavash.jpg -------------------------------------------------------------------------------- /public/img/users/soh3il.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/soh3il.jpg -------------------------------------------------------------------------------- /public/img/covers/siavash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/covers/siavash.jpg -------------------------------------------------------------------------------- /public/img/users/javalaves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/javalaves.jpg -------------------------------------------------------------------------------- /public/img/users/neysidev.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/neysidev.jpg -------------------------------------------------------------------------------- /public/img/users/not_found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/not_found.jpg -------------------------------------------------------------------------------- /src/assets/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/src/assets/images/profile.jpg -------------------------------------------------------------------------------- /public/img/covers/adamwathan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/covers/adamwathan.jpg -------------------------------------------------------------------------------- /public/img/covers/javalaves.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/covers/javalaves.jpg -------------------------------------------------------------------------------- /public/img/users/adamwathan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/adamwathan.jpg -------------------------------------------------------------------------------- /public/img/users/dan_abramov.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/dan_abramov.jpg -------------------------------------------------------------------------------- /src/containers/Notifications.tsx: -------------------------------------------------------------------------------- 1 | export default function Notifications() { 2 | return

Notifications

3 | } 4 | -------------------------------------------------------------------------------- /public/img/users/guillermo_rauch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neysidev/twitter/HEAD/public/img/users/guillermo_rauch.jpg -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/Common/TwitterFullscreen.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components' 2 | import { Close } from 'react-ionicons' 3 | import theme from '../../styles/ThemeStyles' 4 | 5 | interface ITwitterFullscreen { 6 | isOpen: boolean 7 | type: 'cover' | 'profile' 8 | srcImg?: string 9 | altImg?: string 10 | onClose?: () => void 11 | } 12 | 13 | interface IWrapper { 14 | isOpen: boolean 15 | } 16 | 17 | interface IImage { 18 | imgType: 'cover' | 'profile' 19 | } 20 | 21 | export default function TwitterFullscreen(props: ITwitterFullscreen) { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | {props.altImg} 29 | 30 | ) 31 | } 32 | 33 | const Wrapper = styled.div` 34 | position: fixed; 35 | inset: 0; 36 | opacity: 0; 37 | z-index: 99; 38 | visibility: hidden; 39 | background: rgba(0, 0, 0, 0.75); 40 | transition: ${theme.transition.ease}; 41 | 42 | ${props => 43 | props.isOpen && 44 | css` 45 | opacity: 1; 46 | visibility: visible; 47 | `} 48 | ` 49 | 50 | const Overlay = styled.div` 51 | position: fixed; 52 | inset: 0; 53 | cursor: pointer; 54 | ` 55 | 56 | const CloseButton = styled.button` 57 | position: fixed; 58 | top: 2rem; 59 | left: 2rem; 60 | width: 2rem; 61 | height: 2rem; 62 | border-radius: 50%; 63 | transition: ${theme.transition.ease}; 64 | background: rgba(255, 255, 255, 0.2); 65 | 66 | &:hover { 67 | background: rgba(255, 255, 255, 0.3); 68 | } 69 | 70 | span { 71 | display: grid; 72 | place-items: center; 73 | } 74 | ` 75 | 76 | const Image = styled.img` 77 | position: fixed; 78 | top: 50%; 79 | left: 50%; 80 | transform: translate(-50%, -50%); 81 | 82 | ${props => 83 | props.imgType === 'profile' && 84 | css` 85 | width: 20rem; 86 | height: 20rem; 87 | border-radius: 50%; 88 | `}; 89 | 90 | ${props => 91 | props.imgType === 'cover' && 92 | css` 93 | height: 30rem; 94 | `} 95 | ` 96 | -------------------------------------------------------------------------------- /src/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/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/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/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/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/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/Common/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Search } from "react-ionicons" 4 | 5 | import theme from "../../styles/ThemeStyles" 6 | import TwitterContainer from "./TwitterContainer" 7 | import useAppSelector from "../../hooks/useAppSelector" 8 | 9 | export default function Header() { 10 | const { user }: any = useAppSelector(state => state.authorize) 11 | 12 | return ( 13 | 14 | 15 | 16 | About 17 | Help 18 | 19 | e.preventDefault()}> 20 | 21 | 22 | 23 | 24 | 25 | {user?.name} 26 | {user?.name} 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | const Wrapper = styled.header` 38 | padding: 1rem; 39 | user-select: none; 40 | background: ${theme.dark.backgroundBox}; 41 | position: fixed; 42 | top: 0; 43 | left: 15rem; 44 | right: 0; 45 | z-index: 99; 46 | 47 | & > div { 48 | display: flex; 49 | align-items: center; 50 | justify-content: space-between; 51 | } 52 | ` 53 | 54 | const Links = styled.div` 55 | width: 320px; 56 | display: flex; 57 | gap: 1.5rem; 58 | font-size: 0.8rem; 59 | 60 | a { 61 | font-weight: 300; 62 | transition: ${theme.transition.ease}; 63 | 64 | &:hover { 65 | opacity: 0.75; 66 | } 67 | } 68 | ` 69 | 70 | const SearchBox = styled.form` 71 | width: 100%; 72 | padding: 0.75rem 1rem; 73 | overflow: hidden; 74 | border-radius: 0.5rem; 75 | background: ${theme.dark.backgroundPrimary}; 76 | display: flex; 77 | align-items: center; 78 | gap: 1rem; 79 | 80 | svg { 81 | fill: ${theme.dark.text1}; 82 | color: ${theme.dark.text1}; 83 | } 84 | 85 | input { 86 | flex: 1; 87 | color: ${theme.dark.text1}; 88 | background: transparent; 89 | font-size: 0.8rem; 90 | 91 | &::placeholder { 92 | color: ${theme.dark.text2}; 93 | } 94 | } 95 | ` 96 | 97 | const Profile = styled.div` 98 | width: 320px; 99 | display: flex; 100 | justify-content: flex-end; 101 | transition: ${theme.transition.ease}; 102 | 103 | &:hover { 104 | opacity: 0.75; 105 | } 106 | 107 | a { 108 | display: flex; 109 | align-items: center; 110 | gap: 1rem; 111 | } 112 | 113 | span { 114 | font-size: 0.8rem; 115 | font-weight: 300; 116 | } 117 | 118 | img { 119 | width: 2rem; 120 | border-radius: 50%; 121 | } 122 | ` 123 | -------------------------------------------------------------------------------- /src/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/components/Common/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link, NavLink } from "react-router-dom" 3 | 4 | import logo from "../../assets/images/logo.svg" 5 | import theme from "../../styles/ThemeStyles" 6 | import navigation from "../../constants/navigation" 7 | import useAuth from "../../hooks/useAuth" 8 | 9 | import TwitterButton from "./TwitterButton" 10 | 11 | export default function Navigation() { 12 | const { unreadNotification } = useAuth() 13 | 14 | return ( 15 | 16 | 17 | 18 | Twitter Logo 19 | 20 | 21 | 22 | {navigation.map(link => ( 23 | 24 | (isActive ? "active" : "")} 27 | onClick={event => { 28 | if (link.disabled) event.preventDefault() 29 | }} 30 | > 31 | {link.haveBadge && 32 | link.path === "/notifications" && 33 | unreadNotification?.length && ( 34 | {unreadNotification.length} 35 | )} 36 | {link.icon} 37 | {link.name} 38 | 39 | 40 | ))} 41 | 42 | 43 | 44 | Tweet 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | type LinkProps = { 52 | isDisabled?: boolean 53 | } 54 | 55 | const Wrapper = styled.nav` 56 | width: 15rem; 57 | display: flex; 58 | flex-direction: column; 59 | background: ${theme.dark.backgroundBox}; 60 | box-shadow: 0 0 0.8rem rgba(38, 46, 54, 0.5); 61 | position: fixed; 62 | top: 0; 63 | left: 0; 64 | bottom: 0; 65 | user-select: none; 66 | ` 67 | 68 | const Logo = styled.div` 69 | padding: 1.5rem; 70 | 71 | img { 72 | width: 2rem; 73 | } 74 | ` 75 | 76 | const List = styled.ul` 77 | flex: 1; 78 | margin-top: 4rem; 79 | ` 80 | 81 | const Item = styled.li` 82 | position: relative; 83 | 84 | a { 85 | width: 100%; 86 | color: ${theme.dark.text2}; 87 | padding: 0.75rem 1.5rem; 88 | display: flex; 89 | align-items: center; 90 | gap: 0.75rem; 91 | font-weight: 500; 92 | transition: ${theme.transition.ease}; 93 | border-right: 1px solid transparent; 94 | 95 | &.active { 96 | color: ${theme.dark.primary}; 97 | border-right-color: ${theme.dark.primary}; 98 | 99 | svg { 100 | color: ${theme.dark.primary}; 101 | fill: ${theme.dark.primary}; 102 | } 103 | } 104 | 105 | ${props => 106 | !props.isDisabled 107 | ? `&:hover {opacity: 0.75;}` 108 | : `opacity: 0.5; pointer-events: none;`} 109 | } 110 | 111 | span { 112 | display: grid; 113 | place-items: center; 114 | } 115 | 116 | svg { 117 | color: ${theme.dark.text2}; 118 | fill: ${theme.dark.text2}; 119 | } 120 | ` 121 | 122 | const ButtonWrapper = styled.div` 123 | padding: 1.5rem; 124 | ` 125 | 126 | const Badge = styled.span` 127 | position: absolute; 128 | top: 9px; 129 | left: 35px; 130 | z-index: 99; 131 | width: 15px; 132 | height: 15px; 133 | font-size: 12px; 134 | border-radius: 50%; 135 | color: ${theme.dark.text1}; 136 | background: ${theme.colors.blue}; 137 | ` 138 | -------------------------------------------------------------------------------- /src/components/Core/YouShouldFollow.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Link } from "react-router-dom" 3 | import { Reload } from "react-ionicons" 4 | import { useEffect, useState } from "react" 5 | 6 | import * as userService from "../../services/user" 7 | import { IUser } from "../../types/schemas" 8 | import theme from "../../styles/ThemeStyles" 9 | import TwitterBox from "../Common/TwitterBox" 10 | import TwitterSpinner from "../Common/TwitterSpinner" 11 | 12 | export default function YouShouldFollow() { 13 | const randomUsersNumber = 3 14 | 15 | const [users, setUsers] = useState([]) 16 | const [loading, setLoading] = useState(false) 17 | 18 | useEffect(() => { 19 | getRandomUsers() 20 | }, []) 21 | 22 | const getRandomUsers = async () => { 23 | setLoading(true) 24 | setUsers([]) 25 | 26 | const res = await userService.randomUsers(randomUsersNumber) 27 | 28 | if (res.success) setUsers(res.users) 29 | setLoading(false) 30 | } 31 | 32 | let $users_content = null 33 | if (users) { 34 | $users_content = users.map(user => ( 35 | 36 | 37 | 38 | {user.name} 39 |
40 | {user.name} 41 | @{user.username} 42 |
43 |
44 | 45 |
46 | )) 47 | } 48 | 49 | return ( 50 | 51 | 52 | {loading && } 53 |
54 |

You should follow

55 | 56 |
57 | {$users_content} 58 | 59 | See all 60 | 61 |
62 |
63 | ) 64 | } 65 | 66 | const Wrapper = styled.div` 67 | & > div { 68 | padding: 1rem 0; 69 | 70 | & > div:first-child { 71 | height: 280.75px; 72 | } 73 | } 74 | ` 75 | 76 | const Header = styled.header` 77 | padding: 0 1rem; 78 | display: flex; 79 | align-items: center; 80 | justify-content: space-between; 81 | border-bottom: 1px solid ${theme.dark.backgroundPrimary}; 82 | padding-bottom: 1rem; 83 | 84 | span { 85 | display: grid; 86 | cursor: pointer; 87 | place-items: center; 88 | } 89 | 90 | svg { 91 | fill: ${theme.dark.text2}; 92 | color: ${theme.dark.text2}; 93 | } 94 | ` 95 | 96 | const Users = styled.ul` 97 | margin-top: 0.5rem; 98 | user-select: none; 99 | ` 100 | 101 | const User = styled.li` 102 | padding: 0.5rem 1rem; 103 | display: flex; 104 | align-items: center; 105 | justify-content: space-between; 106 | 107 | button { 108 | width: 6rem; 109 | padding: 0.5rem 1rem; 110 | } 111 | ` 112 | 113 | const UserInfo = styled.div` 114 | display: flex; 115 | gap: 0.5rem; 116 | 117 | & > div { 118 | display: flex; 119 | flex-direction: column; 120 | gap: 0.2rem; 121 | } 122 | 123 | span:last-child { 124 | font-size: 0.8rem; 125 | color: ${theme.dark.text2}; 126 | } 127 | 128 | img { 129 | width: 2.5rem; 130 | height: 2.5rem; 131 | border-radius: 50%; 132 | } 133 | ` 134 | 135 | const Action = styled.div` 136 | padding: 1rem 1rem 0 1rem; 137 | text-align: center; 138 | text-transform: uppercase; 139 | font-size: 0.8rem; 140 | user-select: none; 141 | 142 | a { 143 | color: ${theme.dark.text2}; 144 | transition: ${theme.transition.ease}; 145 | 146 | &:hover { 147 | color: ${theme.dark.text1}; 148 | } 149 | } 150 | ` 151 | -------------------------------------------------------------------------------- /src/components/Home/WhatsHappening.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { useState } from "react" 3 | import { Link } from "react-router-dom" 4 | import { useDispatch } from "react-redux" 5 | import { CalendarOutline, HappyOutline, ImageOutline } from "react-ionicons" 6 | 7 | import * as authAction from "../../store/actions/auth" 8 | import * as tweetService from "../../services/tweet" 9 | import * as notificationService from "../../services/notification" 10 | 11 | import theme from "../../styles/ThemeStyles" 12 | import TwitterBox from "../Common/TwitterBox" 13 | import TwitterButton from "../Common/TwitterButton" 14 | import useAppSelector from "../../hooks/useAppSelector" 15 | import { IUser } from "../../types/schemas" 16 | 17 | export default function WhatsHappening() { 18 | const dispatch = useDispatch() 19 | const { 20 | user, 21 | }: { 22 | user: IUser 23 | } = useAppSelector(state => state.authorize) 24 | 25 | const [text, setText] = useState("") 26 | const [loading, setLoading] = useState(false) 27 | 28 | const createTweet = async () => { 29 | setText("") 30 | setLoading(true) 31 | 32 | const { tweet } = await tweetService.createTweet(text) 33 | await notificationService.addNotification(text, tweet._id) 34 | 35 | setLoading(false) 36 | dispatch(authAction.getHomeTweets()) 37 | } 38 | 39 | return ( 40 | 41 | 42 | 43 | 47 | 48 |
49 |