├── .env ├── src ├── react-app-env.d.ts ├── util │ ├── popup │ │ └── index.tsx │ └── hooks.ts ├── App.test.tsx ├── setupTests.ts ├── index.css ├── routes │ ├── routes.ts │ └── route-generator.ts ├── reportWebVitals.ts ├── apollo │ └── provider.ts ├── pages │ ├── friends │ │ ├── request │ │ │ ├── index.tsx │ │ │ └── modules │ │ │ │ └── friends-request-map.tsx │ │ ├── modules │ │ │ └── friends-map.tsx │ │ └── index.tsx │ ├── index │ │ └── index.tsx │ ├── login │ │ └── index.tsx │ ├── register │ │ └── index.tsx │ └── posts │ │ └── [postId].tsx ├── components │ ├── like-button │ │ └── index.tsx │ ├── post-card │ │ └── index.tsx │ ├── post-form │ │ └── index.tsx │ ├── delete-button │ │ └── index.tsx │ └── add-friend-button │ │ └── index.tsx ├── context │ └── auth.tsx ├── ui │ └── menu-bar │ │ └── index.tsx ├── index.tsx └── graphql │ └── queries.ts ├── .prettierrc ├── public ├── robots.txt └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/util/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Popup } from 'semantic-ui-react' 3 | 4 | function MyPopup({ content, children }: any) { 5 | return 6 | } 7 | 8 | export default MyPopup 9 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | 4 | test('renders learn react link', () => { 5 | const linkElement = screen.getByText(/learn react/i) 6 | expect(linkElement).toBeInTheDocument() 7 | }) 8 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .page-title { 2 | display: block !important; 3 | text-align: center; 4 | font-size: 2rem; 5 | margin-top: 10px; 6 | } 7 | 8 | .form-container { 9 | width: 400px; 10 | margin: auto; 11 | } 12 | 13 | .ui.teal.button:focus, 14 | .ui.teal.buttons .button:focus { 15 | background-color: #00b5ad; 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | export const AuthedRoutes = { 2 | index: '/', 3 | singlePost: '/posts/:postId', 4 | friends: '/friends', 5 | friendsRequest: '/friends/request', 6 | } 7 | 8 | export const UnAuthedRoutes = { 9 | index: '/', 10 | register: '/register', 11 | login: '/login', 12 | singlePost: '/posts/:postId', 13 | } 14 | 15 | export const routes = { ...AuthedRoutes, ...UnAuthedRoutes } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/util/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export const useForm = (callback: any, initialState = {}) => { 4 | const [values, setValues] = useState(initialState) 5 | 6 | const onChange = (event: any) => { 7 | setValues({ ...values, [event?.target.name]: event.target.value }) 8 | } 9 | 10 | const onSubmit = (event: any) => { 11 | event.preventDefault() 12 | 13 | callback() 14 | } 15 | 16 | return { 17 | onChange, 18 | onSubmit, 19 | values, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/apollo/provider.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryCache, ApolloClient, createHttpLink } from '@apollo/client' 2 | import { setContext } from '@apollo/client/link/context' 3 | 4 | const httpLink = createHttpLink({ 5 | uri: 'http://localhost:3002/graphql', 6 | }) 7 | 8 | const authLink = setContext(() => { 9 | const token = localStorage.getItem('jwtToken') 10 | return { 11 | headers: { 12 | Authorization: token ? 'Bearer ' + token : '', 13 | }, 14 | } 15 | }) 16 | 17 | const client = new ApolloClient({ 18 | link: authLink.concat(httpLink), 19 | cache: new InMemoryCache(), 20 | }) 21 | 22 | export default client 23 | -------------------------------------------------------------------------------- /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 | "baseUrl": "./src" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/friends/request/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Grid } from 'semantic-ui-react' 3 | import { useQuery } from '@apollo/client' 4 | import { GET_FRIEND_REQUESTS_QUERY } from 'graphql/queries' 5 | import FriendsRequestMap from './modules/friends-request-map' 6 | 7 | export default function FriendsRequestPage() { 8 | const { data } = useQuery(GET_FRIEND_REQUESTS_QUERY) 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | {data && 17 | data.getFriendRequests.map((item: any) => { 18 | return ( 19 | <> 20 | 21 | 22 | ) 23 | })} 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/friends/modules/friends-map.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AddFriendButton from 'components/add-friend-button' 3 | import moment from 'moment' 4 | import { Card, Image } from 'semantic-ui-react' 5 | import { AuthContext } from 'context/auth' 6 | 7 | export default function FriendsMap({ item }: any) { 8 | const { user } = React.useContext(AuthContext) 9 | return ( 10 | <> 11 | 12 | 15 |
16 | 25 |
26 |
27 | {item.username} 28 | {moment(item.createdAt).fromNow()} 29 |
30 |
31 | 32 |
33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/index/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client' 2 | import PostCard from 'components/post-card' 3 | import { FETCH_POSTS_QUERY } from 'graphql/queries' 4 | import { Grid, Transition } from 'semantic-ui-react' 5 | import React from 'react' 6 | import { AuthContext } from 'context/auth' 7 | import PostForm from 'components/post-form' 8 | 9 | function IndexPage() { 10 | const { user } = React.useContext(AuthContext) 11 | let { loading, data } = useQuery(FETCH_POSTS_QUERY) 12 | 13 | return ( 14 | <> 15 | 16 | 17 |

Recent Posts

18 |
19 | 20 | {user && ( 21 | 22 | 23 | 24 | )} 25 | {loading ? ( 26 |

Loading posts..

27 | ) : ( 28 | 29 | {data && 30 | data.getPosts.map((post: any) => ( 31 | 32 | 33 | 34 | ))} 35 | 36 | )} 37 |
38 |
39 | 40 | ) 41 | } 42 | 43 | export default IndexPage 44 | -------------------------------------------------------------------------------- /src/routes/route-generator.ts: -------------------------------------------------------------------------------- 1 | import lazy from 'react-lazy-with-preload' 2 | import { routes } from './routes' 3 | 4 | export const AuthedRoutesData = [ 5 | { 6 | title: 'Index', 7 | path: routes.index, 8 | component: lazy(() => import('pages/index')), 9 | }, 10 | { 11 | title: 'Posts Detail', 12 | path: routes.singlePost, 13 | component: lazy(() => import('pages/posts/[postId]')), 14 | }, 15 | { 16 | title: 'Friends', 17 | path: routes.friends, 18 | component: lazy(() => import('pages/friends')), 19 | }, 20 | { 21 | title: 'Friends Request', 22 | path: routes.friendsRequest, 23 | component: lazy(() => import('pages/friends/request')), 24 | }, 25 | ] 26 | 27 | export const UnAuthedRoutesData = [ 28 | { 29 | title: 'Index', 30 | path: routes.index, 31 | component: lazy(() => import('pages/index')), 32 | }, 33 | { 34 | title: 'Register', 35 | path: routes.register, 36 | component: lazy(() => import('pages/register')), 37 | }, 38 | { 39 | title: 'Login', 40 | path: routes.login, 41 | component: lazy(() => import('pages/login')), 42 | }, 43 | { 44 | title: 'Posts Detail', 45 | path: routes.singlePost, 46 | component: lazy(() => import('pages/posts/[postId]')), 47 | }, 48 | ] 49 | 50 | export const authRoutes = [...AuthedRoutesData] 51 | export const unAuthRoutes = [...UnAuthedRoutesData] 52 | -------------------------------------------------------------------------------- /src/components/like-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Icon, Label } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | import { useMutation } from '@apollo/client' 5 | import { LIKE_POST_MUTATION } from 'graphql/queries' 6 | 7 | export default function LikeButton({ 8 | post: { id, likeCount, likes }, 9 | user, 10 | }: any) { 11 | const [liked, setLiked] = React.useState(false) 12 | 13 | React.useEffect(() => { 14 | if (user && likes.find((like: any) => like.username === user.username)) { 15 | setLiked(true) 16 | } else setLiked(false) 17 | }, [user, likes]) 18 | 19 | const [likePost] = useMutation(LIKE_POST_MUTATION, { 20 | variables: { postId: id }, 21 | }) 22 | 23 | const likeButton = user ? ( 24 | liked ? ( 25 | 28 | ) : ( 29 | 32 | ) 33 | ) : ( 34 | 37 | ) 38 | 39 | return ( 40 | <> 41 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-social-network", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.5.7", 7 | "@testing-library/jest-dom": "^5.16.1", 8 | "@testing-library/react": "^12.1.2", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.4.0", 11 | "@types/node": "^16.11.21", 12 | "@types/react": "^17.0.38", 13 | "@types/react-dom": "^17.0.11", 14 | "graphql": "^16.2.0", 15 | "jwt-decode": "^3.1.2", 16 | "moment": "^2.29.1", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-lazy-with-preload": "^2.0.1", 20 | "react-router-dom": "^6.2.1", 21 | "react-scripts": "5.0.0", 22 | "semantic-ui-css": "^2.4.1", 23 | "semantic-ui-react": "^2.0.4", 24 | "typescript": "^4.5.5", 25 | "web-vitals": "^2.1.4" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/friends/request/modules/friends-request-map.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@apollo/client' 2 | import { ACCEPT_FRIEND_REQUEST_MUTATION } from 'graphql/queries' 3 | import moment from 'moment' 4 | import React from 'react' 5 | import { Button, Card, Image } from 'semantic-ui-react' 6 | 7 | export default function FriendsRequestMap({ item }: any) { 8 | const [accept] = useMutation(ACCEPT_FRIEND_REQUEST_MUTATION, { 9 | variables: { 10 | friendId: item._id, 11 | }, 12 | }) 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 23 | {item.username} 24 | Sent you a friend request 25 | 26 | {moment(item.createdAt).fromNow()} 27 | 28 | 29 | 30 |
31 | 34 | 37 |
38 |
39 |
40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/friends/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Grid } from 'semantic-ui-react' 2 | import { useQuery } from '@apollo/client' 3 | import { GET_USERS_QUERY } from 'graphql/queries' 4 | import FriendsMap from './modules/friends-map' 5 | import { Link } from 'react-router-dom' 6 | 7 | export default function FriendsPage() { 8 | const { data } = useQuery(GET_USERS_QUERY) 9 | 10 | if (data) { 11 | console.log(data) 12 | } 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 33 | 34 | 35 | 36 | 37 |

You may be familiar with

38 | {data?.getUsers?.map((item: any) => { 39 | return ( 40 | <> 41 | 42 | 43 | ) 44 | })} 45 |
46 |
47 |
48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/context/auth.tsx: -------------------------------------------------------------------------------- 1 | import jwtDecode from 'jwt-decode' 2 | import React from 'react' 3 | 4 | const initialState = { 5 | user: null, 6 | } 7 | 8 | if (localStorage.getItem('jwtToken')) { 9 | // @ts-ignore 10 | const decodedToken = jwtDecode(localStorage.getItem('jwtToken')) 11 | 12 | // @ts-ignore 13 | if (decodedToken.exp * 1000 < Date.now()) { 14 | localStorage.removeItem('jwtToken') 15 | } else { 16 | // @ts-ignore 17 | initialState.user = decodedToken 18 | } 19 | } 20 | 21 | const AuthContext = React.createContext({ 22 | user: null, 23 | login: (data: any) => {}, 24 | logout: () => {}, 25 | }) 26 | 27 | function authReducer(state: any, action: any) { 28 | switch (action.type) { 29 | case 'LOGIN': 30 | return { 31 | ...state, 32 | user: action.payload, 33 | } 34 | case 'LOGOUT': 35 | return { 36 | ...state, 37 | user: null, 38 | } 39 | default: 40 | return state 41 | } 42 | } 43 | 44 | function AuthProvider(props: any) { 45 | const [state, dispatch] = React.useReducer(authReducer, initialState) 46 | 47 | function login(userData: any) { 48 | localStorage.setItem('jwtToken', userData.token) 49 | dispatch({ 50 | type: 'LOGIN', 51 | payload: userData, 52 | }) 53 | } 54 | 55 | function logout() { 56 | localStorage.removeItem('jwtToken') 57 | dispatch({ 58 | type: 'LOGOUT', 59 | }) 60 | } 61 | 62 | return ( 63 | 67 | ) 68 | } 69 | 70 | export { AuthContext, AuthProvider } 71 | -------------------------------------------------------------------------------- /src/ui/menu-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Menu } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | import { AuthContext } from 'context/auth' 5 | 6 | export default function MenuBar() { 7 | const { user, logout } = React.useContext(AuthContext) 8 | const pathname = window.location.pathname 9 | const path = pathname === '/' ? 'home' : pathname.substr(1) 10 | 11 | const [activeItem, setActiveItem] = React.useState(path) 12 | 13 | const menuBar = user ? ( 14 |
15 | 16 | 17 | {/* */} 18 | 19 | 20 | 21 | 22 |
23 | ) : ( 24 |
25 | 26 | setActiveItem('home')} 30 | as={Link} 31 | to="/" 32 | /> 33 | 34 | setActiveItem('login')} 38 | as={Link} 39 | to="/login" 40 | /> 41 | setActiveItem('register')} 45 | as={Link} 46 | to="/register" 47 | /> 48 | 49 | 50 |
51 | ) 52 | 53 | return menuBar 54 | } 55 | -------------------------------------------------------------------------------- /src/components/post-card/index.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import React from 'react' 3 | import { Link } from 'react-router-dom' 4 | import { Button, Card, Icon, Image, Label } from 'semantic-ui-react' 5 | import { AuthContext } from 'context/auth' 6 | import LikeButton from 'components/like-button' 7 | import DeleteButton from 'components/delete-button' 8 | import MyPopup from 'util/popup' 9 | 10 | export default function PostCard({ 11 | post: { body, createdAt, id, username, likeCount, commentCount, likes }, 12 | }: any) { 13 | const { user } = React.useContext(AuthContext) 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 24 | {username} 25 | 26 | {moment(createdAt).fromNow(true)} 27 | 28 | {body} 29 | 30 | 31 | 32 | 33 | 34 | 38 | 41 | 42 | 43 | {user && user.username === username && } 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/post-form/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Form } from 'semantic-ui-react' 3 | import { useForm } from 'util/hooks' 4 | import { useMutation } from '@apollo/client' 5 | import { CREATE_POST_MUTATION, FETCH_POSTS_QUERY } from 'graphql/queries' 6 | 7 | export default function PostForm() { 8 | const { values, onChange, onSubmit } = useForm(createPostCallBack, { 9 | body: '', 10 | }) 11 | 12 | const [createPost, { error }] = useMutation(CREATE_POST_MUTATION, { 13 | variables: values, 14 | update(proxy, result) { 15 | const data: any = proxy.readQuery({ 16 | query: FETCH_POSTS_QUERY, 17 | }) 18 | 19 | let newData = [...data.getPosts] 20 | newData = [result.data.createPost, ...newData] 21 | proxy.writeQuery({ 22 | query: FETCH_POSTS_QUERY, 23 | data: { 24 | ...data, 25 | getPosts: { 26 | newData, 27 | }, 28 | }, 29 | }) 30 | values.body = '' 31 | }, 32 | }) 33 | 34 | function createPostCallBack() { 35 | createPost() 36 | } 37 | return ( 38 | <> 39 |
40 |

Create a post:

41 | 42 | 49 | 52 | 53 |
54 | {error && ( 55 | <> 56 |
57 |
    58 |
  • {error.graphQLErrors[0].message}
  • 59 |
60 |
61 | 62 | )} 63 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import reportWebVitals from './reportWebVitals' 5 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' 6 | import { authRoutes, unAuthRoutes } from 'routes/route-generator' 7 | import MenuBar from 'ui/menu-bar' 8 | import { Container } from 'semantic-ui-react' 9 | import { ApolloProvider } from '@apollo/client' 10 | import client from 'apollo/provider' 11 | import { AuthProvider, AuthContext } from 'context/auth' 12 | 13 | const IndexPage = () => { 14 | const context = React.useContext(AuthContext) 15 | 16 | if (context.user === null && !localStorage.getItem('jwtToken')) { 17 | return ( 18 | 19 | {unAuthRoutes.map((el) => { 20 | return ( 21 | } /> 22 | ) 23 | })} 24 | 25 | ) 26 | } 27 | 28 | return ( 29 | 30 | {authRoutes.map((el) => { 31 | return } /> 32 | })} 33 | } /> 34 | 35 | ) 36 | } 37 | 38 | ReactDOM.render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | , 53 | document.getElementById('root') 54 | ) 55 | 56 | // If you want to start measuring performance in your app, pass a function 57 | // to log results (for example: reportWebVitals(console.log)) 58 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 59 | reportWebVitals() 60 | -------------------------------------------------------------------------------- /src/components/delete-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Confirm, Icon } from 'semantic-ui-react' 3 | import { useMutation } from '@apollo/client' 4 | import { 5 | DELETE_POST_MUTATION, 6 | FETCH_POSTS_QUERY, 7 | DELETE_COMMENT_MUTATION, 8 | } from 'graphql/queries' 9 | import MyPopup from 'util/popup' 10 | 11 | export default function DeleteButton({ 12 | postId, 13 | commentId, 14 | callback, 15 | }: { 16 | postId?: string 17 | commentId?: string 18 | callback?: () => void 19 | }) { 20 | const [confirmOpen, setConfirmOpen] = React.useState(false) 21 | 22 | const mutation = commentId ? DELETE_COMMENT_MUTATION : DELETE_POST_MUTATION 23 | 24 | const [deletePostOrMutation] = useMutation(mutation, { 25 | update(proxy) { 26 | setConfirmOpen(false) 27 | 28 | if (!commentId) { 29 | const data: any = proxy.readQuery({ 30 | query: FETCH_POSTS_QUERY, 31 | }) 32 | 33 | let newData = [...data.getPosts] 34 | newData = data.getPosts.filter((post: any) => post.id !== postId) 35 | proxy.writeQuery({ 36 | query: FETCH_POSTS_QUERY, 37 | data: { 38 | ...data, 39 | getPosts: { 40 | newData, 41 | }, 42 | }, 43 | }) 44 | } 45 | 46 | if (callback) callback() 47 | }, 48 | variables: { 49 | postId, 50 | commentId, 51 | }, 52 | }) 53 | 54 | return ( 55 | <> 56 | 57 | 65 | 66 | setConfirmOpen(false)} 69 | // @ts-ignore 70 | onConfirm={deletePostOrMutation} 71 | /> 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Form } from 'semantic-ui-react' 3 | import { useMutation } from '@apollo/client' 4 | import { LOGIN_USER } from 'graphql/queries' 5 | import { useNavigate } from 'react-router-dom' 6 | import { useForm } from 'util/hooks' 7 | import { AuthContext } from 'context/auth' 8 | 9 | export default function LoginPage() { 10 | const context = React.useContext(AuthContext) 11 | const navigate = useNavigate() 12 | 13 | const [errors, setErrors] = React.useState({}) 14 | 15 | const { onChange, onSubmit, values } = useForm(registerUser, { 16 | username: '', 17 | password: '', 18 | }) 19 | 20 | const [loginUser, { loading }] = useMutation(LOGIN_USER, { 21 | update(proxy, { data: { login: userData } }) { 22 | context.login(userData) 23 | navigate('/') 24 | }, 25 | onError(err) { 26 | // @ts-ignore 27 | setErrors(err.graphQLErrors[0].extensions.errors) 28 | }, 29 | variables: values, 30 | }) 31 | 32 | function registerUser() { 33 | loginUser() 34 | } 35 | return ( 36 | <> 37 |
38 |
43 |

Login

44 | {/* react-hook-form */} 45 | 55 | 65 | 68 | 69 | {Object.keys(errors).length > 0 && ( 70 |
71 |
    72 | {Object.values(errors).map((value: any) => ( 73 |
  • {value}
  • 74 | ))} 75 |
76 |
77 | )} 78 |
79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /src/components/add-friend-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'semantic-ui-react' 3 | import { useMutation } from '@apollo/client' 4 | import { 5 | ADD_FRIEND_MUTATION, 6 | GET_USERS_QUERY, 7 | REMOVE_FRIEND_MUTATION, 8 | } from 'graphql/queries' 9 | 10 | export default function AddFriendButton({ 11 | item: { id: userId, friends }, 12 | user, 13 | }: any) { 14 | const [requested, setRequested] = React.useState(false) 15 | 16 | React.useEffect(() => { 17 | if ( 18 | user && 19 | friends.find((friend: any) => friend.username === user.username) 20 | ) { 21 | setRequested(true) 22 | } else setRequested(false) 23 | }, [user, friends]) 24 | 25 | const [addFriend] = useMutation(ADD_FRIEND_MUTATION, { 26 | update(proxy, result) { 27 | const data: any = proxy.readQuery({ 28 | query: GET_USERS_QUERY, 29 | }) 30 | 31 | let newData = [...data.getUsers] 32 | newData = [result.data.getUsers, ...newData] 33 | proxy.writeQuery({ 34 | query: GET_USERS_QUERY, 35 | data: { 36 | ...data, 37 | getUsers: { 38 | newData, 39 | }, 40 | }, 41 | }) 42 | }, 43 | variables: { 44 | userId, 45 | }, 46 | }) 47 | 48 | const [removeFriendOrDecline] = useMutation(REMOVE_FRIEND_MUTATION, { 49 | update(proxy, result) { 50 | const data: any = proxy.readQuery({ 51 | query: GET_USERS_QUERY, 52 | }) 53 | 54 | let newData = [...data.getUsers] 55 | newData = [result.data.getUsers, ...newData] 56 | proxy.writeQuery({ 57 | query: GET_USERS_QUERY, 58 | data: { 59 | ...data, 60 | getUsers: { 61 | newData, 62 | }, 63 | }, 64 | }) 65 | }, 66 | variables: { 67 | friendId: friends[0]?._id, 68 | userId: userId, 69 | }, 70 | }) 71 | 72 | return ( 73 | <> 74 | {requested ? ( 75 | 89 | ) : ( 90 | 102 | )} 103 | 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/register/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Form } from 'semantic-ui-react' 3 | import { useMutation } from '@apollo/client' 4 | import { REGISTER_USER } from 'graphql/queries' 5 | import { useNavigate } from 'react-router-dom' 6 | import { useForm } from 'util/hooks' 7 | import { AuthContext } from 'context/auth' 8 | 9 | export default function RegisterPage() { 10 | const context = React.useContext(AuthContext) 11 | const navigate = useNavigate() 12 | 13 | const [errors, setErrors] = React.useState({}) 14 | 15 | const { onChange, onSubmit, values } = useForm(registerUser, { 16 | username: '', 17 | password: '', 18 | email: '', 19 | confirmPassword: '', 20 | }) 21 | 22 | const [addUser, { loading }] = useMutation(REGISTER_USER, { 23 | update(proxy, { data: { register: userData } }) { 24 | context.login(userData) 25 | navigate('/') 26 | }, 27 | onError(err) { 28 | // @ts-ignore 29 | setErrors(err.graphQLErrors[0].extensions.errors) 30 | }, 31 | variables: values, 32 | }) 33 | 34 | function registerUser() { 35 | addUser() 36 | } 37 | return ( 38 | <> 39 |
40 |
45 |

Register

46 | {/* react-hook-form */} 47 | 57 | 67 | 77 | 87 | 90 | 91 | {Object.keys(errors).length > 0 && ( 92 |
93 |
    94 | {Object.values(errors).map((value: any) => ( 95 |
  • {value}
  • 96 | ))} 97 |
98 |
99 | )} 100 |
101 | 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /src/graphql/queries.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client' 2 | 3 | export const FETCH_POSTS_QUERY = gql` 4 | { 5 | getPosts { 6 | id 7 | body 8 | createdAt 9 | username 10 | likeCount 11 | likes { 12 | username 13 | } 14 | commentCount 15 | comments { 16 | id 17 | username 18 | createdAt 19 | body 20 | } 21 | } 22 | } 23 | ` 24 | 25 | export const REGISTER_USER = gql` 26 | mutation register( 27 | $username: String! 28 | $email: String! 29 | $password: String! 30 | $confirmPassword: String! 31 | ) { 32 | register( 33 | registerInput: { 34 | username: $username 35 | email: $email 36 | password: $password 37 | confirmPassword: $confirmPassword 38 | } 39 | ) { 40 | id 41 | email 42 | username 43 | createdAt 44 | token 45 | } 46 | } 47 | ` 48 | 49 | export const LOGIN_USER = gql` 50 | mutation login($username: String!, $password: String!) { 51 | login(username: $username, password: $password) { 52 | id 53 | email 54 | username 55 | createdAt 56 | token 57 | } 58 | } 59 | ` 60 | 61 | export const CREATE_POST_MUTATION = gql` 62 | mutation createPost($body: String!) { 63 | createPost(body: $body) { 64 | id 65 | body 66 | createdAt 67 | username 68 | likes { 69 | id 70 | username 71 | createdAt 72 | } 73 | likeCount 74 | comments { 75 | id 76 | body 77 | username 78 | createdAt 79 | } 80 | commentCount 81 | } 82 | } 83 | ` 84 | 85 | export const LIKE_POST_MUTATION = gql` 86 | mutation likePost($postId: ID!) { 87 | likePost(postId: $postId) { 88 | id 89 | likes { 90 | id 91 | username 92 | } 93 | likeCount 94 | } 95 | } 96 | ` 97 | 98 | export const FETCH_POST_QUERY = gql` 99 | query ($postId: ID!) { 100 | getPost(postId: $postId) { 101 | id 102 | body 103 | createdAt 104 | username 105 | likeCount 106 | likes { 107 | username 108 | } 109 | commentCount 110 | comments { 111 | id 112 | username 113 | createdAt 114 | body 115 | } 116 | } 117 | } 118 | ` 119 | 120 | export const DELETE_POST_MUTATION = gql` 121 | mutation deletePost($postId: ID!) { 122 | deletePost(postId: $postId) 123 | } 124 | ` 125 | 126 | export const DELETE_COMMENT_MUTATION = gql` 127 | mutation deleteComment($postId: String!, $commentId: ID!) { 128 | deleteComment(postId: $postId, commentId: $commentId) { 129 | id 130 | comments { 131 | id 132 | username 133 | createdAt 134 | body 135 | } 136 | commentCount 137 | } 138 | } 139 | ` 140 | 141 | export const SUBMIT_COMMENT_MUTATION = gql` 142 | mutation ($postId: ID!, $body: String!) { 143 | createComment(postId: $postId, body: $body) { 144 | id 145 | comments { 146 | id 147 | body 148 | createdAt 149 | username 150 | } 151 | commentCount 152 | } 153 | } 154 | ` 155 | 156 | export const GET_USERS_QUERY = gql` 157 | query { 158 | getUsers { 159 | email 160 | createdAt 161 | id 162 | username 163 | friends { 164 | status 165 | username 166 | _id 167 | } 168 | } 169 | } 170 | ` 171 | 172 | export const ADD_FRIEND_MUTATION = gql` 173 | mutation ($userId: ID!) { 174 | addFriend(userId: $userId) { 175 | friends { 176 | status 177 | } 178 | } 179 | } 180 | ` 181 | 182 | export const REMOVE_FRIEND_MUTATION = gql` 183 | mutation ($userId: ID!, $friendId: ID!) { 184 | removeFriend(userId: $userId, friendId: $friendId) { 185 | email 186 | } 187 | } 188 | ` 189 | 190 | export const GET_FRIEND_REQUESTS_QUERY = gql` 191 | query { 192 | getFriendRequests { 193 | username 194 | createdAt 195 | _id 196 | } 197 | } 198 | ` 199 | 200 | export const ACCEPT_FRIEND_REQUEST_MUTATION = gql` 201 | mutation ($friendId: ID!) { 202 | acceptFriendRequest(friendId: $friendId) { 203 | friends { 204 | status 205 | } 206 | } 207 | } 208 | ` 209 | -------------------------------------------------------------------------------- /src/pages/posts/[postId].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useMutation, useQuery } from '@apollo/client' 3 | import { useParams } from 'react-router-dom' 4 | import { FETCH_POST_QUERY, SUBMIT_COMMENT_MUTATION } from 'graphql/queries' 5 | import { Button, Card, Form, Grid, Icon, Image, Label } from 'semantic-ui-react' 6 | import moment from 'moment' 7 | import LikeButton from 'components/like-button' 8 | import { AuthContext } from 'context/auth' 9 | import DeleteButton from 'components/delete-button' 10 | import { useNavigate } from 'react-router-dom' 11 | 12 | interface TComments { 13 | id: string 14 | createdAt: string 15 | body: string 16 | username: string 17 | } 18 | 19 | export default function PostsPostIdPage() { 20 | const commentInputRef = React.useRef(null) 21 | const { user } = React.useContext(AuthContext) 22 | const navigate = useNavigate() 23 | const params = useParams() 24 | const [comment, setComment] = React.useState('') 25 | 26 | const { data } = useQuery(FETCH_POST_QUERY, { 27 | variables: { postId: params.postId }, 28 | }) 29 | 30 | const [submitComment] = useMutation(SUBMIT_COMMENT_MUTATION, { 31 | update() { 32 | setComment('') 33 | 34 | commentInputRef.current.blur() 35 | }, 36 | variables: { 37 | postId: params.postId, 38 | body: comment, 39 | }, 40 | }) 41 | 42 | let postMarkup 43 | if (!data?.getPost) { 44 | postMarkup =

Loading post...

45 | } else { 46 | const { 47 | id, 48 | body, 49 | createdAt, 50 | username, 51 | comments, 52 | likeCount, 53 | commentCount, 54 | likes, 55 | } = data?.getPost 56 | 57 | const deleteButtonCallback = () => { 58 | navigate('/') 59 | } 60 | 61 | postMarkup = ( 62 | <> 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | {username} 76 | {moment(createdAt).fromNow()} 77 | {body} 78 | 79 |
80 | 81 | 82 | 90 | 93 | 94 | {user && user.username === username && ( 95 | 96 | )} 97 | 98 |
99 | {user && ( 100 | 101 | 102 |

Post a comment

103 |
104 |
105 | setComment(e.target.value)} 111 | ref={commentInputRef} 112 | /> 113 | 121 |
122 |
123 |
124 |
125 | )} 126 | {comments.map((comment: TComments) => { 127 | return ( 128 | <> 129 | 130 | 131 | {user && user.username === comment.username && ( 132 | 133 | )} 134 | {username} 135 | 136 | {moment(comment.createdAt).fromNow()} 137 | 138 | {comment.body} 139 | 140 | 141 | 142 | ) 143 | })} 144 |
145 |
146 |
147 | 148 | ) 149 | } 150 | 151 | return postMarkup 152 | } 153 | --------------------------------------------------------------------------------