├── .vercelignore ├── client ├── styles │ ├── profile │ │ ├── about-me.scss │ │ ├── contact.scss │ │ ├── general.scss │ │ ├── languages.scss │ │ ├── settings.scss │ │ └── index.scss │ ├── utilities │ │ └── variables.scss │ ├── shared │ │ ├── index.scss │ │ ├── footer.scss │ │ └── header.scss │ ├── Home-page │ │ ├── index.scss │ │ └── home-page.scss │ ├── user-profile │ │ ├── index.scss │ │ ├── add-friend-section.scss │ │ ├── profile-pics.scss │ │ ├── profile-info.scss │ │ └── profile-section.scss │ ├── bookmarks │ │ └── index.scss │ ├── theme.ts │ ├── friends │ │ └── index.scss │ ├── index.scss │ ├── chat │ │ └── index.scss │ └── photos │ │ └── index.scss ├── next.config.js ├── next-env.d.ts ├── lib │ ├── withRedirect.tsx │ └── apollo.ts ├── pages │ ├── 404.tsx │ ├── index.tsx │ ├── _document.tsx │ ├── login.tsx │ ├── _app.tsx │ ├── profile │ │ └── editProfile.tsx │ ├── homePage.tsx │ ├── bookmarks │ │ └── index.tsx │ ├── friends │ │ └── [userName].tsx │ ├── signup.tsx │ ├── photos │ │ └── index.tsx │ ├── chat │ │ └── index.tsx │ └── users │ │ └── [userName].tsx ├── .gitignore ├── graphql │ ├── subscription.ts │ ├── queries.ts │ └── mutations.ts ├── tsconfig.json ├── components │ ├── Layout.tsx │ ├── ToastMessage.tsx │ ├── user-profile │ │ └── PhotosSlider.tsx │ ├── UsersList.tsx │ ├── friends │ │ ├── FriendsRequests.tsx │ │ └── FriendsList.tsx │ ├── shared │ │ ├── Footer.tsx │ │ ├── HeaderProfile.tsx │ │ └── Header.tsx │ ├── edit-profile │ │ ├── Contact.tsx │ │ ├── Languages.tsx │ │ ├── General.tsx │ │ └── AboutMe.tsx │ └── UploadProfileImages.tsx ├── public │ └── vercel.svg ├── README.md └── package.json ├── .gitignore ├── server ├── tsconfig.json ├── schema │ ├── resolvers │ │ ├── index.ts │ │ ├── chat.ts │ │ ├── message.ts │ │ └── user.ts │ ├── typeDefs │ │ ├── index.ts │ │ ├── chat.ts │ │ ├── message.ts │ │ └── user.ts │ └── dummy.ts ├── models │ ├── Chat.ts │ ├── Message.ts │ ├── avatars.ts │ └── User.ts ├── db │ └── index.ts ├── middlewares │ ├── auth.ts │ └── validation │ │ └── userValidation.ts └── index.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.vercelignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/styles/profile/about-me.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/profile/contact.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/profile/general.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/profile/languages.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/profile/settings.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/styles/utilities/variables.scss: -------------------------------------------------------------------------------- 1 | $main-color: #556cd6 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vercel 4 | vercel.json 5 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "serverless" 3 | }; 4 | -------------------------------------------------------------------------------- /client/styles/shared/index.scss: -------------------------------------------------------------------------------- 1 | @import './header.scss'; 2 | @import './footer.scss'; -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": ["node_modules"] 4 | } 5 | -------------------------------------------------------------------------------- /client/lib/withRedirect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const withRedirect = () => { 4 | return
; 5 | }; 6 | 7 | export default withRedirect; 8 | -------------------------------------------------------------------------------- /client/styles/Home-page/index.scss: -------------------------------------------------------------------------------- 1 | @import './home-page.scss'; 2 | 3 | /* General components */ 4 | 5 | .home-page-body { 6 | padding-top: 24px; 7 | min-height: 500px; 8 | } -------------------------------------------------------------------------------- /server/schema/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import userResolver from "./user"; 2 | import messageResolver from "./message"; 3 | import chatResolver from "./chat"; 4 | 5 | export default [userResolver, messageResolver, chatResolver]; 6 | -------------------------------------------------------------------------------- /client/styles/user-profile/index.scss: -------------------------------------------------------------------------------- 1 | @import "./profile-pics.scss"; 2 | @import "./profile-info.scss"; 3 | @import "./add-friend-section.scss"; 4 | @import "./profile-section.scss"; 5 | 6 | /* profile Page */ 7 | .profile-page { 8 | margin-top: 20px; 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "sourceMap": true, 6 | "strict": false, 7 | "moduleResolution": "node", 8 | "jsx": "preserve" 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /server/models/Chat.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from "mongoose"; 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const ChatSchema = new Schema( 6 | { 7 | users: { type: [Schema.Types.ObjectId], ref: "NewUser" }, 8 | messages: { type: [Schema.Types.ObjectId], ref: "Message" } 9 | }, 10 | { timestamps: true } 11 | ); 12 | 13 | const Chat = mongoose.model("Chat", ChatSchema); 14 | export default Chat; 15 | -------------------------------------------------------------------------------- /server/schema/typeDefs/index.ts: -------------------------------------------------------------------------------- 1 | import userTypeDef from "./user"; 2 | import chatTypeDef from "./chat"; 3 | import messageTypeDef from "./message"; 4 | 5 | const base = ` 6 | type Query { 7 | _: String 8 | } 9 | 10 | type Mutation { 11 | _: String 12 | } 13 | 14 | type Subscription { 15 | _: String 16 | } 17 | `; 18 | 19 | export default [base, userTypeDef, chatTypeDef, messageTypeDef]; 20 | -------------------------------------------------------------------------------- /client/styles/bookmarks/index.scss: -------------------------------------------------------------------------------- 1 | .bookmarks-page { 2 | 3 | .left-side { 4 | .empty-message { 5 | 6 | display: block; 7 | margin: 0 auto; 8 | margin-top: 25px; 9 | font-size: 20px; 10 | 11 | 12 | a { 13 | color: blueviolet; 14 | border-bottom: 1px dashed #6d6fd3; 15 | } 16 | } 17 | 18 | } 19 | } -------------------------------------------------------------------------------- /server/models/Message.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from "mongoose"; 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const MessageSchema = new Schema( 6 | { 7 | user: { type: Schema.Types.ObjectId, ref: "NewUser" }, 8 | chat: { type: Schema.Types.ObjectId, ref: "Chat" }, 9 | text: { type: String, required: true } 10 | }, 11 | { timestamps: true } 12 | ); 13 | 14 | const Message = mongoose.model("Message", MessageSchema); 15 | export default Message; 16 | -------------------------------------------------------------------------------- /client/styles/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | import red from "@material-ui/core/colors/red"; 3 | 4 | // Create a theme instance. 5 | const theme = createMuiTheme({ 6 | palette: { 7 | primary: { 8 | main: "#556cd6" 9 | }, 10 | secondary: { 11 | main: "#19857b" 12 | }, 13 | error: { 14 | main: red.A400 15 | }, 16 | background: { 17 | default: "#fff" 18 | } 19 | } 20 | }); 21 | 22 | export default theme; 23 | -------------------------------------------------------------------------------- /client/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { initializeApollo } from "../lib/apollo"; 3 | import UsersList from "../components/UsersList"; 4 | import { ALL_USERS_QUERY } from "../graphql/queries"; 5 | 6 | function PageNotFound(props) { 7 | return ( 8 | <> 9 | 10 | {`404 :(`} 11 | 12 | 13 |
14 |

{`404 :( Not found`}

15 |
16 | 17 | ); 18 | } 19 | 20 | export default PageNotFound; 21 | -------------------------------------------------------------------------------- /client/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | .vercel 33 | -------------------------------------------------------------------------------- /client/styles/user-profile/add-friend-section.scss: -------------------------------------------------------------------------------- 1 | @import "./../utilities/variables.scss"; 2 | 3 | /* profile page add friend section */ 4 | .profile-page { 5 | 6 | .add-friend-section { 7 | margin: 35px 0; 8 | 9 | .MuiGrid-item { 10 | cursor: pointer; 11 | margin: 0 10px; 12 | border: 1px solid $main-color; 13 | padding: 10px; 14 | border-radius: 5px; 15 | 16 | .fa { 17 | margin-left: 5px; 18 | } 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /client/styles/friends/index.scss: -------------------------------------------------------------------------------- 1 | .friends-page { 2 | min-height: 500px; 3 | margin: 25px 0; 4 | 5 | .left-side { 6 | .one-friend { 7 | margin-top: 15px; 8 | 9 | .MuiAvatar-circle { 10 | width: 65px; 11 | height: 65px; 12 | } 13 | 14 | a { 15 | border-bottom: 1px dashed #6d6fd3; 16 | transition: ease-in-out all .2s; 17 | 18 | &:hover { 19 | color: 20 | #6d6fd3; 21 | } 22 | } 23 | } 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /client/styles/profile/index.scss: -------------------------------------------------------------------------------- 1 | @import './general.scss'; 2 | @import './contact.scss'; 3 | @import './about-me.scss'; 4 | @import './languages.scss'; 5 | @import './settings.scss'; 6 | 7 | /* genaral style */ 8 | .profile-settings-page { 9 | min-height: 550px; 10 | margin-bottom: 35px; 11 | 12 | .left-side>div:first-of-type { 13 | box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | } 17 | 18 | .react-swipeable-view-container { 19 | 20 | div[data-swipeable] { 21 | background-color: #fff; 22 | min-height: 400px; 23 | margin-top: 3px; 24 | } 25 | 26 | 27 | } -------------------------------------------------------------------------------- /client/styles/shared/footer.scss: -------------------------------------------------------------------------------- 1 | /* Main footer */ 2 | .main-footer { 3 | margin-bottom: 15px; 4 | 5 | 6 | .MuiAppBar-root { 7 | padding: 15px; 8 | } 9 | 10 | .lists-container { 11 | color: #000; 12 | 13 | .list-title { 14 | border-bottom: 2px solid #000; 15 | font-weight: 600; 16 | } 17 | 18 | ul { 19 | list-style: none; 20 | margin-left: -40px; 21 | 22 | li { 23 | color: #222 24 | } 25 | } 26 | } 27 | 28 | .privacy-policy { 29 | margin: 10px 0; 30 | } 31 | } -------------------------------------------------------------------------------- /client/graphql/subscription.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | export const SEND_MESSAGE_SUB = gql` 4 | subscription { 5 | userChats { 6 | ok 7 | error 8 | chat { 9 | id 10 | users { 11 | id 12 | userName 13 | firstName 14 | lastName 15 | avatarUrl 16 | } 17 | messages { 18 | id 19 | text 20 | createdAt 21 | user { 22 | id 23 | userName 24 | firstName 25 | lastName 26 | avatarUrl 27 | } 28 | } 29 | } 30 | } 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /server/schema/typeDefs/chat.ts: -------------------------------------------------------------------------------- 1 | // defines chat typeDef 2 | 3 | const chatTypeDef = ` 4 | 5 | type Chat{ 6 | id: ID! 7 | users: [User!] 8 | messages: [Message!] 9 | } 10 | 11 | type UserChatsQueryPayload{ 12 | ok: Boolean! 13 | error: String 14 | chats: [Chat!] 15 | } 16 | 17 | type NewChatPayload{ 18 | ok: Boolean! 19 | chat: Chat 20 | error: String 21 | successMessage: String 22 | } 23 | 24 | extend type Query{ 25 | userChats: UserChatsQueryPayload 26 | } 27 | 28 | extend type Mutation { 29 | createNewChat(users:[ID!]):NewChatPayload! 30 | } 31 | 32 | `; 33 | 34 | export default chatTypeDef; 35 | -------------------------------------------------------------------------------- /client/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa&display=swap'); 2 | 3 | @import "./Home-page/index.scss"; 4 | @import "./shared/index.scss"; 5 | @import "./user-profile/index.scss"; 6 | @import "./chat/index.scss"; 7 | @import "./friends/index.scss"; 8 | @import "./photos/index.scss"; 9 | @import "./bookmarks/index.scss"; 10 | @import "./profile/index.scss"; 11 | 12 | 13 | * { 14 | font-family: 'Comfortaa', cursive; 15 | } 16 | 17 | a { 18 | text-decoration: none; 19 | color: #000; 20 | } 21 | 22 | // the whole layout container 23 | .container { 24 | background-color: #f5f5f5; 25 | padding: 0 5%; 26 | padding-top: 60px; 27 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "exclude": ["node_modules"], 18 | "include": [ 19 | "next-env.d.ts", 20 | "**/*.ts", 21 | "**/*.tsx", 22 | "lib/*", 23 | "next.config.js", 24 | "pages/**/*" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /server/schema/dummy.ts: -------------------------------------------------------------------------------- 1 | /* This dummy data i use to test my application before connectig to the db */ 2 | export default { 3 | users: [ 4 | { id: "1", name: "momo", email: "momo@yahoo" }, 5 | { id: "2", name: "lolo", email: "lolo@yahoo" }, 6 | { id: "3", name: "koko", email: "koko@yahoo" } 7 | ], 8 | messages: [ 9 | { id: "1", body: "message 1", user: "1" }, 10 | { id: "2", body: "message 2", user: "2" }, 11 | { id: "3", body: "message 3", user: "3" }, 12 | { id: "4", body: "message 4", user: "1" }, 13 | { id: "5", body: "message 5", user: "2" }, 14 | { id: "6", body: "message 6", user: "3" }, 15 | { id: "7", body: "message 7", user: "1" } 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /server/db/index.ts: -------------------------------------------------------------------------------- 1 | //import * as mongoose from "mongoose"; 2 | const mongoose = require("mongoose"); 3 | 4 | const db = mongoose.connection; 5 | 6 | export const connectDb = () => { 7 | // Database configuration 8 | mongoose.connect(process.env.DB_URI, { 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | useCreateIndex: true 12 | }); 13 | 14 | db.on("error", console.error.bind(console, "error: unable to connect to database")); 15 | db.once("open", function () { 16 | console.log("Conncted to database successfully..."); 17 | }); 18 | }; 19 | 20 | export const disconnectDb = () => { 21 | if (!db) { 22 | return; 23 | } 24 | 25 | mongoose.disconnect(); 26 | }; 27 | -------------------------------------------------------------------------------- /server/schema/typeDefs/message.ts: -------------------------------------------------------------------------------- 1 | // defines message typeDef 2 | 3 | const messageTypeDef = ` 4 | type Message { 5 | id: ID 6 | text: String! 7 | user: User! 8 | chat: Chat! 9 | createdAt: String! 10 | } 11 | 12 | type MessagePayload{ 13 | ok: Boolean! 14 | error: String 15 | successMessage: String 16 | } 17 | 18 | type SendMessageSubPayload{ 19 | ok: Boolean! 20 | error: String 21 | chat: Chat! 22 | } 23 | 24 | extend type Mutation { 25 | sendMessage(text: String!, user: ID!, chat: ID): MessagePayload! 26 | } 27 | 28 | extend type Subscription { 29 | userChats: SendMessageSubPayload 30 | } 31 | 32 | `; 33 | 34 | export default messageTypeDef; 35 | -------------------------------------------------------------------------------- /client/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./shared/Header"; 3 | import Footer from "./shared/Footer"; 4 | import Head from "next/head"; 5 | 6 | // out pages layout 7 | const Layout = props => { 8 | if (props.children.type.name == "Home") { 9 | return
{props.children}
; 10 | } 11 | 12 | return ( 13 |
14 | 15 | 19 | 20 | 21 |
22 | 23 | {props.children} 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | export default Layout; 31 | -------------------------------------------------------------------------------- /client/styles/user-profile/profile-pics.scss: -------------------------------------------------------------------------------- 1 | /* Profile Page -> profile pics*/ 2 | .profile-page { 3 | 4 | /* profile details */ 5 | .profile-details { 6 | margin: 20px 0; 7 | } 8 | 9 | .welcome-message { 10 | font-size: 18px; 11 | color: #687cd3; 12 | } 13 | 14 | /* the avatar styles before the user updats a pic */ 15 | .profile-main-avatar { 16 | width: 13rem; 17 | height: 13rem; 18 | cursor: pointer; 19 | } 20 | 21 | .profile-main-pic { 22 | width: 90% !important; 23 | height: 12rem; 24 | margin-bottom: 5px; 25 | cursor: pointer; 26 | } 27 | 28 | /* rest of the pics */ 29 | .profile-pics { 30 | width: 90%; 31 | cursor: pointer; 32 | 33 | .MuiAvatar-root { 34 | width: 80%; 35 | height: 90%; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /client/components/ToastMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Snackbar from "@material-ui/core/Snackbar"; 3 | import MuiAlert, { AlertProps } from "@material-ui/lab/Alert"; 4 | 5 | function Alert(props: AlertProps) { 6 | return ; 7 | } 8 | 9 | const ErrorMessage = props => { 10 | const [open, setOpen] = React.useState(true); 11 | 12 | const handleClose = (event?: React.SyntheticEvent, reason?: string) => { 13 | if (reason === "clickaway") { 14 | return; 15 | } 16 | 17 | setOpen(false); 18 | }; 19 | 20 | return ( 21 |
22 | 23 | 24 | {props.message} 25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default ErrorMessage; 32 | -------------------------------------------------------------------------------- /client/styles/user-profile/profile-info.scss: -------------------------------------------------------------------------------- 1 | /* profile page info */ 2 | .profile-page { 3 | .profile-info { 4 | 5 | /* each profile info */ 6 | .profile-info-item { 7 | line-height: 2rem; 8 | 9 | /* the title */ 10 | .title { 11 | font-weight: 600; 12 | font-size: 17px; 13 | color: #666; 14 | } 15 | 16 | /* details */ 17 | .info { 18 | color: #999; 19 | } 20 | 21 | /* languages details */ 22 | .lang-info { 23 | margin-left: 5px; 24 | 25 | &:last-child { 26 | &::after { 27 | content: unset; 28 | display: block; 29 | 30 | } 31 | } 32 | 33 | &::after { 34 | content: "/"; 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/styles/user-profile/profile-section.scss: -------------------------------------------------------------------------------- 1 | /* each profile section */ 2 | .profile-page { 3 | .profile-section { 4 | margin-bottom: 50px; 5 | 6 | .profile-section-title { 7 | border-bottom: 3px solid #556cd6; 8 | width: 30%; 9 | } 10 | 11 | /* generate random colors for chips */ 12 | @for $i from 1 through 100 { 13 | .chip-wrapper:nth-child(#{$i}) { 14 | .MuiChip-outlined { 15 | border-color: rgb(random(255), random(255), random(255)); 16 | color: #556cd6; 17 | margin-left: 10px; 18 | margin-bottom: 10px; 19 | border-width: 2px; 20 | font-weight: 600; 21 | } 22 | } 23 | } 24 | 25 | .contact-list { 26 | i { 27 | font-size: 25px; 28 | color: #556cd6; 29 | } 30 | 31 | p { 32 | font-weight: 600; 33 | font-size: 1rem; 34 | } 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mohamed Elashmawy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/components/user-profile/PhotosSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AutoRotatingCarousel, Slide } from "material-auto-rotating-carousel"; 3 | import Button from "@material-ui/core/Button"; 4 | import useMediaQuery from "@material-ui/core/useMediaQuery"; 5 | import { red, blue, green } from "@material-ui/core/colors"; 6 | 7 | const PhotosSlider = ({ handleOpen, setHandleOpen, photos }) => { 8 | return ( 9 |
10 | setHandleOpen({ open: false })} 14 | onStart={() => setHandleOpen({ open: false })} 15 | autoplay={false} 16 | // mobile={isMobile} 17 | style={{ position: "absolute" }}> 18 | {photos && 19 | photos.map(photo => ( 20 | } 22 | mediaBackgroundStyle={{ backgroundColor: red[400] }} 23 | style={{ backgroundColor: red[600] }} 24 | /> 25 | ))} 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default PhotosSlider; 32 | -------------------------------------------------------------------------------- /server/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from "jsonwebtoken"; 2 | import { GraphQLError } from "graphql"; 3 | import User from "./../models/User"; 4 | 5 | // authenticate user 6 | export const userAuth = async (req: any) => { 7 | const authHeader = (await req.get("token")) || (await req.get("cookie")); 8 | let token: string; 9 | 10 | if (authHeader) { 11 | token = authHeader.split("=")[1]; 12 | } 13 | 14 | // check if the authentication exists 15 | if (!authHeader || authHeader === null || !token || token === null) { 16 | req.isAuth = false; 17 | req.user = null; 18 | throw new GraphQLError("Not authenticated, please login"); 19 | } 20 | 21 | try { 22 | //verify token 23 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 24 | 25 | // add user from token payload which contains the user id we attached to the token 26 | req.user = decoded; 27 | } catch (e) { 28 | throw new GraphQLError(e); 29 | } 30 | }; 31 | 32 | // Admins auth 33 | export const adminAuth = async (req: any) => { 34 | await userAuth(req); 35 | 36 | if (req.user.role !== "admin") { 37 | throw new GraphQLError("Not authenticated, Admins only"); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /client/styles/shared/header.scss: -------------------------------------------------------------------------------- 1 | /* Main header */ 2 | .main-header { 3 | padding-top: 20px; 4 | 5 | /* the navbar before scrolling down */ 6 | .MuiPaper-elevation0 { 7 | margin-top: 20px; 8 | } 9 | 10 | /* the fixed navbar itself */ 11 | .MuiAppBar-positionFixed { 12 | transition: all ease-in-out .3s; 13 | max-width: 90% !important; 14 | right: unset; 15 | left: unset; 16 | 17 | /* header link */ 18 | a { 19 | margin-left: 15px; 20 | font-weight: 500; 21 | 22 | &:hover { 23 | color: #fff; 24 | border-bottom: 1px solid #fff; 25 | transition: all ease-in-out .3s 26 | } 27 | } 28 | 29 | /* signed in account */ 30 | .me-header { 31 | margin-left: auto; 32 | 33 | &:hover { 34 | cursor: pointer; 35 | } 36 | 37 | } 38 | } 39 | 40 | /* Home button */ 41 | .MuiButtonBase-root { 42 | &:hover { 43 | background-color: unset !important; 44 | } 45 | 46 | /* home button a */ 47 | a { 48 | margin-left: 0; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { initializeApollo } from "../lib/apollo"; 3 | import UsersList from "../components/UsersList"; 4 | import { ALL_USERS_QUERY } from "../graphql/queries"; 5 | import { Button } from "@material-ui/core"; 6 | import Link from "next/link"; 7 | import { useRouter } from "next/router"; 8 | 9 | function Home(props) { 10 | let { 11 | data: { me } 12 | } = props; 13 | 14 | return ( 15 | <> 16 | 17 | Social App 18 | 19 | 20 |

Welcome

21 | 26 | 27 | 32 | 33 | ); 34 | } 35 | 36 | export async function getServerSideProps({ req, res }) { 37 | // get the cookies from the headers in the request object 38 | const token = req.headers.cookie ? req.headers.cookie : null; 39 | 40 | if (token) { 41 | res.statusCode = 302; 42 | res.setHeader("Location", `/homePage`); // Replace with your url link 43 | } 44 | 45 | return { props: {} }; 46 | } 47 | 48 | export default Home; 49 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLServer, PubSub } from "graphql-yoga"; 2 | import { connectDb } from "./db/index"; 3 | import typeDefs from "./schema/typeDefs"; 4 | import resolvers from "./schema/resolvers"; 5 | import path = require("path"); 6 | require("dotenv").config(); 7 | const express = require("express"); 8 | 9 | const pubsub = new PubSub(); 10 | 11 | // Creating our graphQL server with the schema defined 12 | const server = new GraphQLServer({ 13 | typeDefs, 14 | resolvers, 15 | context: ({ request, response }) => { 16 | return { 17 | req: request, 18 | res: response, 19 | token: request?.headers?.token, //request?.headers ? request.headers.token : null, 20 | pubsub 21 | }; 22 | } 23 | }); 24 | 25 | // database connection 26 | connectDb(); 27 | 28 | // Server connection options 29 | const options = { 30 | port: process.env.PORT || 5000, 31 | endpoint: "/graphql", 32 | subscriptions: "/subscriptions", 33 | playground: "/graphql", 34 | cors: { 35 | credentials: true, 36 | origin: 37 | process.env.NODE_ENV == "production" 38 | ? "https://huhuhu.vercel.app" 39 | : "http://localhost:3000" // your frontend url. 40 | } 41 | }; 42 | 43 | // starting the server on port 5000 44 | server.start(options, () => { 45 | console.log(`Server is running on http://localhost:${options.port}`); 46 | }); 47 | -------------------------------------------------------------------------------- /client/styles/chat/index.scss: -------------------------------------------------------------------------------- 1 | @import "../utilities/variables.scss"; 2 | 3 | .all-chats { 4 | margin-top: 20px; 5 | 6 | .empty-message { 7 | display: block; 8 | margin: 0 auto; 9 | margin-top: 25px; 10 | font-size: 20px; 11 | 12 | a { 13 | color: blueviolet; 14 | border-bottom: 1px dashed #6d6fd3; 15 | } 16 | } 17 | 18 | .chat-list { 19 | overflow-y: scroll; 20 | scrollbar-width: none; 21 | height: 400px; 22 | width: 100%; 23 | margin-bottom: 15px; 24 | } 25 | 26 | .MuiBox-root { 27 | padding: 0 24px; 28 | } 29 | 30 | .my-message, 31 | .other-message { 32 | background-color: #d6556c; 33 | padding: 15px; 34 | border-radius: 15px; 35 | width: auto; 36 | flex: none; 37 | max-width: 50%; 38 | } 39 | 40 | .other-message { 41 | background-color: #55add6; 42 | } 43 | 44 | .send-form { 45 | padding-left: 75px; 46 | 47 | textarea { 48 | width: 100%; 49 | border-radius: 25px; 50 | border: 1px solid $main-color; 51 | padding-left: 20px; 52 | height: 55px; 53 | line-height: 50px; 54 | } 55 | 56 | button { 57 | border-radius: 50%; 58 | min-width: 50px !important; 59 | min-height: 50px !important; 60 | 61 | @media (min-width: 768px) { 62 | margin-left: 3rem; 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social-app", 3 | "version": "1.0.0", 4 | "description": "A socail application built with MERN + GraphQL", 5 | "main": "index.ts", 6 | "dependencies": { 7 | "bcrypt": "^5.0.0", 8 | "cloudinary": "^1.22.0", 9 | "cookie": "^0.4.1", 10 | "cors": "^2.8.5", 11 | "dotenv": "^8.2.0", 12 | "esm": "^3.2.25", 13 | "express": "^4.17.1", 14 | "graphql-yoga": "^1.18.3", 15 | "isomorphic-unfetch": "^3.0.0", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongodb": "^3.5.9", 18 | "mongoose": "^5.9.25", 19 | "yup": "^0.29.2" 20 | }, 21 | "devDependencies": { 22 | "@types/bcrypt": "^3.0.0", 23 | "@types/esm": "^3.2.0", 24 | "@types/express": "^4.17.7", 25 | "@types/jsonwebtoken": "^8.5.0", 26 | "@types/mongoose": "^5.7.32", 27 | "@types/node": "^14.6.2", 28 | "@types/react": "^16.9.43", 29 | "@types/typescript": "^2.0.0", 30 | "@types/yup": "^0.29.6", 31 | "concurrently": "^5.2.0", 32 | "nodemon": "^2.0.4", 33 | "ts-node": "^8.10.2", 34 | "typescript": "^4.0.2" 35 | }, 36 | "scripts": { 37 | "postinstall": "cd client && npm install", 38 | "start": "ts-node -r esm server/index.ts", 39 | "dev:backend": "nodemon -r esm server/index.ts", 40 | "dev:frontend": "npm run dev --prefix client", 41 | "dev": "concurrently --kill-others npm:dev:*" 42 | }, 43 | "author": "Mohamed Elashmawy", 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /client/styles/photos/index.scss: -------------------------------------------------------------------------------- 1 | .user-photos-page { 2 | padding: 24px; 3 | 4 | /* all photos section */ 5 | .all-photos { 6 | 7 | /* one item */ 8 | .one-item { 9 | margin: 20px 0; 10 | border-bottom: 1px dashed #6d6fd3; 11 | padding-bottom: 20px; 12 | 13 | /* the photo */ 14 | .MuiAvatar-root { 15 | height: 90px; 16 | width: 90px; 17 | } 18 | } 19 | } 20 | 21 | /* upload photos section */ 22 | .upload-photos-form { 23 | border-left: 1px dashed #6d6fd3; 24 | align-content: flex-start; 25 | 26 | /* the header */ 27 | h3 { 28 | text-align: center; 29 | } 30 | 31 | /* the form iteself */ 32 | form { 33 | width: 100%; 34 | text-align: center; 35 | margin-bottom: 20px; 36 | } 37 | 38 | /* every photo with delete sign */ 39 | .each-item { 40 | margin-top: 20px; 41 | 42 | .wrapper { 43 | position: relative; 44 | } 45 | 46 | .MuiAvatar-rounded { 47 | width: 70px; 48 | height: 70px; 49 | } 50 | 51 | .delete-item { 52 | position: absolute; 53 | top: -10px; 54 | left: -5px; 55 | cursor: pointer; 56 | font-size: 15px; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /client/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | import { ServerStyleSheets } from "@material-ui/core/styles"; 4 | import theme from "../styles/theme"; 5 | 6 | export default class MyDocument extends Document { 7 | render() { 8 | return ( 9 | 10 | 11 | {/* PWA primary color */} 12 | 13 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | // `getInitialProps` belongs to `_document` (instead of `_app`), 28 | // it's compatible with server-side generation (SSG). 29 | MyDocument.getInitialProps = async ctx => { 30 | // Render app and page and get the context of the page with collected side effects. 31 | const sheets = new ServerStyleSheets(); 32 | const originalRenderPage = ctx.renderPage; 33 | 34 | ctx.renderPage = () => 35 | originalRenderPage({ 36 | enhanceApp: App => props => sheets.collect() 37 | }); 38 | 39 | const initialProps = await Document.getInitialProps(ctx); 40 | 41 | return { 42 | ...initialProps, 43 | // Styles fragment is rendered after the app and page rendering finish. 44 | styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()] 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /client/components/UsersList.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/client"; 2 | import Link from "next/link"; 3 | import ErrorMessage from "./ToastMessage"; 4 | import Button from "@material-ui/core/Button"; 5 | import { MUTATION_DELETE_USER } from "../graphql/mutations"; 6 | 7 | export default function UsersList(props) { 8 | const { users, error, ok } = props.users; 9 | 10 | let [deleteUserMutation, { data: mutation_data, loading: l }] = useMutation( 11 | MUTATION_DELETE_USER 12 | ); 13 | 14 | return ( 15 | <> 16 | {error &&
{error}
} 17 | {mutation_data?.deleteUser?.ok && ( 18 | 19 | )} 20 | {mutation_data?.deleteUser?.error && ( 21 | 22 | )} 23 |
    24 | {users && 25 | users.map(user => ( 26 |
  • 27 | 28 | {user.userName} 29 | 30 |
    {user.userName}
    31 |
    {user.email}
    32 |
    {user.firstName}
    33 |
    {user.lastName}
    34 |
    {user.createdAt}
    35 | 43 |
  • 44 | ))} 45 |
46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/styles/Home-page/home-page.scss: -------------------------------------------------------------------------------- 1 | .home-page-body { 2 | 3 | .user-greeting { 4 | margin-bottom: 30px; 5 | padding-bottom: 30px; 6 | border-bottom: 1px solid black; 7 | 8 | /* user acatar */ 9 | .home-page-user-avatar { 10 | 11 | height: 70px !important; 12 | width: 70px !important; 13 | margin-left: 50%; 14 | 15 | } 16 | 17 | /* auto typing section */ 18 | .home-page-typed { 19 | .home-page-username { 20 | color: rgb(59, 18, 24); 21 | font-size: 20px; 22 | font-weight: 600; 23 | margin-right: 5px; 24 | } 25 | 26 | span { 27 | &:nth-child(2) { 28 | color: #000; 29 | } 30 | } 31 | } 32 | } 33 | 34 | /* random users list */ 35 | .some-random-users { 36 | background-color: #fff; 37 | padding: 10px; 38 | border-radius: 10px; 39 | margin-top: 50px; 40 | box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2); 41 | 42 | /* */ 43 | .heading { 44 | margin: 0 auto; 45 | border-bottom: 1px dashed #6d6fd3; 46 | font-size: 20px; 47 | margin-bottom: 30px; 48 | } 49 | 50 | /* one user card */ 51 | .one-user-card { 52 | margin: 0 auto; 53 | 54 | .MuiAvatar-circle { 55 | width: 55px; 56 | height: 55px; 57 | margin: 0 auto; 58 | margin-bottom: 20px 59 | } 60 | 61 | .one-user-url { 62 | display: block; 63 | text-align: center; 64 | margin-bottom: 2px; 65 | 66 | &:hover { 67 | color: #6d6fd3; 68 | border-bottom: 1px dashed #6d6fd3; 69 | transition: all ease .5s; 70 | } 71 | 72 | } 73 | 74 | .one-user-country { 75 | text-align: center; 76 | font-size: 12px; 77 | color: #acacac; 78 | } 79 | } 80 | } 81 | 82 | 83 | } -------------------------------------------------------------------------------- /server/schema/resolvers/chat.ts: -------------------------------------------------------------------------------- 1 | import { userAuth } from "../../middlewares/auth"; 2 | import Chat from "../../models/Chat"; 3 | import Message from "../../models/Message"; 4 | 5 | const chatResolver = { 6 | Query: { 7 | userChats: async (_, __, { req }) => { 8 | try { 9 | // 1- authenticate user 10 | await userAuth(req); 11 | 12 | const userId = req.user.userId; 13 | 14 | // 2- get all chats that contain the user id 15 | const chats = await Chat.find({ 16 | users: { 17 | $in: userId 18 | } 19 | }) 20 | .populate({ 21 | path: "users", 22 | model: "NewUser" 23 | }) 24 | .populate({ 25 | path: "messages", 26 | model: "Message", 27 | populate: { 28 | path: "user", 29 | model: "NewUser" 30 | } 31 | }) 32 | .exec(); 33 | 34 | return { 35 | ok: true, 36 | chats: chats 37 | }; 38 | } catch (error) { 39 | return { 40 | ok: false, 41 | error: error.message 42 | }; 43 | } 44 | } 45 | }, 46 | 47 | /************** Mutations ****************/ 48 | Mutation: { 49 | //create new chat 50 | createNewChat: async (_, { users }, { req }) => { 51 | try { 52 | // 1- authenticate user 53 | userAuth(req); 54 | 55 | //2- once the user clicks chat, it starts new chat netween the 2 users 56 | const newChat = await new Chat({ users: users }).save(); 57 | 58 | return { 59 | ok: true, 60 | chat: newChat, 61 | successMessage: "Started new chat" 62 | }; 63 | } catch (error) { 64 | return { 65 | ok: false, 66 | error: error.message 67 | }; 68 | } 69 | } 70 | } 71 | }; 72 | 73 | export default chatResolver; 74 | -------------------------------------------------------------------------------- /client/components/friends/FriendsRequests.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Grid, Avatar, Button } from "@material-ui/core"; 3 | import Link from "next/link"; 4 | import { useMutation } from "@apollo/client"; 5 | import { ACCEPT_FRIEND_MUTATION } from "../../graphql/mutations"; 6 | import ErrorMessage from "../ToastMessage"; 7 | 8 | const FriendsRequests = ({ requests }) => { 9 | // handle accept friend mutation 10 | const [accept_friend, { data, loading }] = useMutation(ACCEPT_FRIEND_MUTATION); 11 | 12 | const [allRequests, setAllRequests] = useState(requests); 13 | 14 | // handle accept friend state 15 | const handleAcceptFriends = friendId => { 16 | accept_friend({ variables: { id: friendId } }); 17 | 18 | const requestsAfterAccept = requests.filter(friend => friend.id !== friendId); 19 | 20 | setAllRequests(requestsAfterAccept); 21 | }; 22 | 23 | return ( 24 | 25 | {data?.acceptFriend.ok && ( 26 | 27 | )} 28 | 29 | {requests?.length < 1 &&
No requests yet
} 30 | 31 | {requests?.length > 0 && 32 | requests.map(requestt => ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | {requestt.firstName + " " + requestt.lastName} 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | ))} 53 |
54 | ); 55 | }; 56 | 57 | export default FriendsRequests; 58 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.0.2", 12 | "@farbenmeer/react-spring-slider": "^1.2.1", 13 | "@fortawesome/fontawesome-free": "^5.14.0", 14 | "@material-ui/core": "^4.11.0", 15 | "@material-ui/icons": "^4.9.1", 16 | "@material-ui/lab": "^4.0.0-alpha.56", 17 | "apollo-link-context": "^1.0.20", 18 | "apollo-link-retry": "^2.2.16", 19 | "apollo-link-ws": "^1.0.20", 20 | "apollo-upload-client": "^14.1.1", 21 | "font-awesome": "^4.7.0", 22 | "fontawesome": "^5.6.3", 23 | "formik": "^2.1.5", 24 | "formik-material-ui": "^3.0.0-alpha.0", 25 | "graphql": "^15.3.0", 26 | "isomorphic-unfetch": "^3.0.0", 27 | "js-cookie": "^2.2.1", 28 | "material-auto-rotating-carousel": "^3.0.2", 29 | "moment": "^2.27.0", 30 | "next": "^9.4.4", 31 | "node-sass": "^4.14.1", 32 | "nprogress": "^0.2.0", 33 | "react": "16.13.1", 34 | "react-awesome-slider": "^4.1.0", 35 | "react-dom": "^16.13.1", 36 | "react-swipeable-views": "^0.13.9", 37 | "react-typed": "^1.2.0", 38 | "sass": "^1.26.10", 39 | "subscriptions-transport-ws": "^0.9.18", 40 | "ws": "^7.3.1", 41 | "yup": "^0.29.2" 42 | }, 43 | "devDependencies": { 44 | "@types/apollo-upload-client": "^8.1.3", 45 | "@types/js-cookie": "^2.2.6", 46 | "@types/node": "^14.0.26", 47 | "@types/react": "^16.9.43", 48 | "@types/ws": "^7.2.6", 49 | "@types/yup": "^0.29.3", 50 | "@types/nprogress": "^0.2.0", 51 | "redux-devtools-extension": "^2.13.8", 52 | "typescript": "^3.9.7" 53 | }, 54 | "proxy": "http://localhost:5000", 55 | "description": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).", 56 | "main": "index.js", 57 | "directories": { 58 | "lib": "lib" 59 | }, 60 | "author": "", 61 | "license": "ISC" 62 | } 63 | -------------------------------------------------------------------------------- /server/models/avatars.ts: -------------------------------------------------------------------------------- 1 | export const maleAvatars = [ 2 | "https://res.cloudinary.com/hamohuh/image/upload/v1597812931/147144_pr6mx8.svg", 3 | "https://res.cloudinary.com/hamohuh/image/upload/v1597852090/avatars/user-sign-icon-person-symbol-human-avatar-successful-man-84531334_mpa40y.jpg", 4 | "https://res.cloudinary.com/hamohuh/image/upload/v1597852090/avatars/man-2_icon-icons.com_55041_hdhaob.png", 5 | "https://res.cloudinary.com/hamohuh/image/upload/v1597852089/avatars/doctor-web-icon-head-physician-avatar-doctor-web-icon-head-physician-medical-avatar-flat-style-illustration-103706729_efcpl9.jpg", 6 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851896/avatars/user-sign-icon-person-symbol-human-avatar-rich-man-84519083_a6aejk.jpg", 7 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851895/avatars/user-sign-icon-person-symbol-human-avatar-rich-man-84519323_lnk7xg.jpg" 8 | ]; 9 | 10 | export const femaleAvatars = [ 11 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851895/avatars/profile-icon-female-head-in-chat-bubble-isolated-vector-23798505_n2ztrx.jpg", 12 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851895/avatars/user-sign-icon-person-symbol-human-avatar-successful-woman-84519100_xau31i.jpg", 13 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851895/avatars/user-sign-icon-person-symbol-human-avatar-successful-woman-84527747_zg2hp8.jpg", 14 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851892/avatars/school-girl-avatar-isolated-faceless-female-vector-21453401_mvahm0.jpg", 15 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851892/avatars/profile-icon-female-head-chat-bubble-isolated-caucasian-woman-avatar-cartoon-character-portrait-flat-vector-illustration-108359494_xfbtge.jpg", 16 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851891/avatars/index_lojuaw.png", 17 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851891/avatars/emotional-women-portrait-hand-drawn-flat-design-concept-illustration-girl-happy-female-face-shoulders-avatars_137966-235_yja1lj.jpg", 18 | "https://res.cloudinary.com/hamohuh/image/upload/v1597851891/avatars/images_jb4q6b.png" 19 | ]; 20 | -------------------------------------------------------------------------------- /server/schema/resolvers/message.ts: -------------------------------------------------------------------------------- 1 | import { userAuth } from "../../middlewares/auth"; 2 | import Chat from "../../models/Chat"; 3 | import Message from "../../models/Message"; 4 | 5 | const CHAT_CHANNEL = "CHAT_CHANNEL"; 6 | 7 | const messageResolver = { 8 | /********** Mutations ************/ 9 | Mutation: { 10 | // send message 11 | sendMessage: async (_, { text, user, chat }, { req, pubsub }) => { 12 | try { 13 | // 1- authenticate user 14 | await userAuth(req); 15 | 16 | const userId = req.user.userId; 17 | 18 | // 2- create new message 19 | const newMessage = new Message({ text: text, user: user, chat: chat }); 20 | await newMessage.save(); 21 | 22 | // 3- update the messages array with the new message id 23 | await Chat.findByIdAndUpdate( 24 | chat, 25 | { $push: { messages: newMessage._id } }, 26 | { useFindAndModify: false } 27 | ); 28 | 29 | // 4- get the current active chat the user send the message in 30 | const currentChat = await Chat.findById(chat) 31 | .populate({ 32 | path: "users", 33 | model: "NewUser" 34 | }) 35 | .populate({ 36 | path: "messages", 37 | model: "Message", 38 | populate: { 39 | path: "user", 40 | model: "NewUser" 41 | } 42 | }) 43 | .exec(); 44 | 45 | // 5- publish the current chat send message subscription 46 | pubsub.publish(CHAT_CHANNEL, { userChats: { ok: true, chat: currentChat } }); 47 | 48 | return { 49 | ok: true, 50 | successMessage: "Message sent" 51 | }; 52 | } catch (error) { 53 | return { 54 | ok: false, 55 | error: error.message 56 | }; 57 | } 58 | } 59 | }, 60 | 61 | /************** Subscription ***********/ 62 | Subscription: { 63 | userChats: { 64 | subscribe: (_, __, { pubsub }) => { 65 | return pubsub.asyncIterator(CHAT_CHANNEL); 66 | } 67 | } 68 | } 69 | }; 70 | 71 | export default messageResolver; 72 | -------------------------------------------------------------------------------- /client/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField } from "formik-material-ui"; 3 | import Layout from "../components/Layout"; 4 | import { Formik, Form, Field } from "formik"; 5 | import { Button } from "@material-ui/core"; 6 | import * as Yup from "yup"; 7 | import { useMutation } from "@apollo/client"; 8 | import ErrorMessage from "../components/ToastMessage"; 9 | import { initializeApollo } from "../lib/apollo"; 10 | import { useRouter } from "next/router"; 11 | import { LOGIN_MUTATION } from "../graphql/mutations"; 12 | 13 | // form validation useing Yup 14 | const validate = () => 15 | Yup.object({ 16 | userName: Yup.string() 17 | .min(2, "Must be more than one character") 18 | .required("Username is required"), 19 | password: Yup.string() 20 | .min(8, "Must be more than 8 characters") 21 | .required("This field is required") 22 | }); 23 | 24 | export default function login() { 25 | const [login, { data, loading }] = useMutation(LOGIN_MUTATION); 26 | 27 | const handleRegisterSuccess = () => { 28 | if (data?.login?.ok) { 29 | useRouter().replace("/homePage"); 30 | 31 | return ; 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 | {loading &&
logging in.....
} 38 | {data?.login?.error && } 39 | 40 | {handleRegisterSuccess()} 41 | 42 | { 49 | login({ 50 | variables: { 51 | userName: values.userName, 52 | password: values.password 53 | } 54 | }); 55 | setSubmitting(false); 56 | }}> 57 |
58 | 59 |
60 | 67 |
68 | 69 | 72 | 73 |
74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /client/components/friends/FriendsList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Grid, Avatar, Button } from "@material-ui/core"; 3 | import Link from "next/link"; 4 | import { DELETE_FRIEND_MUTATION } from "../../graphql/mutations"; 5 | import { useMutation } from "@apollo/client"; 6 | import ErrorMessage from "../ToastMessage"; 7 | import DeleteIcon from "@material-ui/icons/Delete"; 8 | import IconButton from "@material-ui/core/IconButton"; 9 | import Tooltip from "@material-ui/core/Tooltip"; 10 | 11 | const FriendsList = ({ friends, showDeleteFriend }) => { 12 | // handle delete friend mutation 13 | const [delete_friend, { data }] = useMutation(DELETE_FRIEND_MUTATION); 14 | 15 | const [allFriends, setAllFiends] = useState(friends); 16 | 17 | // handle delete friend state 18 | const handleDeleteFriends = friendId => { 19 | delete_friend({ variables: { id: friendId } }); 20 | 21 | const friendsAfterDelete = allFriends.filter(friend => friend.id !== friendId); 22 | 23 | setAllFiends(friendsAfterDelete); 24 | }; 25 | 26 | return ( 27 | 28 | {data?.deleteFriend.ok && ( 29 | 30 | )} 31 | 32 | {data?.deleteFriend.error && ( 33 | 34 | )} 35 | 36 | {allFriends?.length < 1 &&
Your friends list is empty, make some friends
} 37 | 38 | {friends?.length > 0 && 39 | allFriends.map(friend => ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | {friend.firstName + " " + friend.lastName} 47 | 48 | 49 | {showDeleteFriend && ( 50 | 51 | 52 | 53 | { 55 | handleDeleteFriends(friend.id); 56 | }} 57 | /> 58 | 59 | 60 | 61 | )} 62 | 63 | ))} 64 |
65 | ); 66 | }; 67 | 68 | export default FriendsList; 69 | -------------------------------------------------------------------------------- /client/components/shared/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { AppBar, Grid, Toolbar } from "@material-ui/core"; 2 | import React from "react"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 |
About
11 |
    12 |
  • About us
  • 13 |
  • Blog
  • 14 |
  • Donate
  • 15 |
  • Feedback
  • 16 |
  • Jobs
  • 17 |
18 |
19 | 20 |
Help
21 |
    22 |
  • F A Q
  • 23 |
  • Help
  • 24 |
  • Forgot Password
  • 25 |
  • Contact Us
  • 26 |
27 |
28 | 29 |
Language Practice
30 |
    31 |
  • Learn Spanish
  • 32 |
  • Learn Chinese
  • 33 |
  • Learn French
  • 34 |
  • Learn German
  • 35 |
  • Learn Japanese
  • 36 |
  • Learn Russian
  • 37 |
  • Learn other languages
  • 38 |
39 |
40 | 41 | 42 |
Make New Friends
43 |
    44 |
  • Who's Online Now?
  • 45 |
  • Live Global Updates
  • 46 |
  • Search & Meet People
  • 47 |
  • CLearn German
  • 48 |
  • Forums & Topics
  • 49 |
  • Language Exchange
  • 50 |
  • Invite Friends
  • 51 |
52 |
53 | 54 |
Your Profile
55 |
    56 |
  • Account
  • 57 |
  • Home
  • 58 |
  • Edit Profile
  • 59 |
  • Your Messages
  • 60 |
  • Upload Photos
  • 61 |
  • Your Friends
  • 62 |
  • Your Settings
  • 63 |
64 |
65 |
66 | 67 | 68 | © 2020 Social-app. Terms of Service | Privacy Policy 69 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | export default Footer; 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

🌍  Social App 🌎

2 | 3 | > This application isn't finished yet. 4 | 5 | > Built with MERN stack (MongoDB, Express, React and Node) + (GrapgQL). 6 | 7 | ###

⚡️⚡️⚡️   [Live Demo](https://huhuhu.vercel.app/) ⚡️⚡️⚡️

8 | 9 | > PS. The realtime chat doesn't work on the live demo, cause vercel doesn't support WS, 10 | > but it works fine on localhost, and will deploy ot to another host once it's done. 11 | 12 | ## 📜   Table of contents 13 | 14 | - [Main Features](#--main-features) 15 | - [Technologies](#--technologies) 16 | - [Key Concepts](#--key-concepts) 17 | - [Setup](#--setup) 18 | - [ENV_VARS](#--env-variables) 19 | 20 | ## 🚩   Main Features 21 | 22 | > This App is made to create a socail network where people can connect, 23 | > meet each other depends on interests in common, 24 | > and will support real time chat 25 | 26 | ## 💹   Technologies 27 | 28 | > Project is created with: 29 | 30 | #### Backend 31 | 32 | - Express 33 | - GraphQL Yoga 34 | - Mongoose 35 | - .... 36 | 37 | #### Frontend 38 | 39 | - React JS 40 | - Next JS 41 | - .... 42 | 43 | ## 💡   Key Concepts 44 | 45 | - Built with GraphQL 46 | - Apollo 47 | - MVC (Model-View-Controller) 48 | - CRUD operations 49 | - Authentication system 50 | - Encrypting passwords 51 | - Images handling using multer 52 | 53 | ## 💻   Setup 54 | 55 | To run this project, install it locally using npm: 56 | 57 | ``` 58 | $ cd social-app 59 | $ npm install 60 | $ npm run dev:backend (for graphql + Node server side development) 61 | $ npm run dev:frontend (for React client side development) 62 | $ npm run dev (for both client and server side) 63 | ``` 64 | 65 | ## #️⃣   66 | 67 | ## 💻   Env-Variables 68 | 69 | > Please consider adding these envirnment variables to have the app working 70 | 71 | DB_URI=mongodb+srv://:@cluster0-7ckqn.azure.mongodb.net/local_library. 72 | JWT_SECRET = your JWT secret. 73 | CLOUDINARY_CLOUD_NAME = this one is for images upload on cloudinary. 74 | CLOUDINARY_API_KEY = also for cloudinary. 75 | CLOUDINARY_API_SECRET = also for cloudinary. 76 | DOMAIN_URI = (if you're on localhost keep it "localhost", if you will deploy it put the URL). 77 | 78 | # Author 79 | 80 | 👤   **Mohamed Elashmawy** 81 | 82 | - Twitter: [@hamohuh](https://twitter.com/hamohuh) 83 | - Github: [@moelashmawy](https://github.com/moelashmawy) 84 | - Linkedin: [@moelashmawy](https://www.linkedin.com/in/moelashmawy/) 85 | - Email: [ashmawy894@gmail.com](mailto:ashmawy894@gmail.com) 86 | 87 | ## 📝   License 88 | 89 | - This project is [MIT](./LICENSE) licensed. 90 | -------------------------------------------------------------------------------- /client/components/shared/HeaderProfile.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; 3 | import Popover from "@material-ui/core/Popover"; 4 | import Typography from "@material-ui/core/Typography"; 5 | import { Avatar, MenuItem, MenuList } from "@material-ui/core"; 6 | import { useMutation } from "@apollo/client"; 7 | import { LOGOUT_MUTATION } from "../../graphql/mutations"; 8 | import { useRouter } from "next/router"; 9 | import Link from "next/link"; 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | typography: { 14 | padding: theme.spacing(2) 15 | } 16 | }) 17 | ); 18 | 19 | /* This component renders the current logged in acount on the navbar(header) */ 20 | export default function HeaderProfile({ me }) { 21 | const classes = useStyles(); 22 | const [anchorEl, setAnchorEl] = React.useState(null); 23 | 24 | const handleClick = (event: React.MouseEvent) => { 25 | setAnchorEl(event.currentTarget); 26 | }; 27 | 28 | const handleClose = () => { 29 | setAnchorEl(null); 30 | }; 31 | 32 | const open = Boolean(anchorEl); 33 | const id = open ? "simple-popover" : undefined; 34 | 35 | //handle logout 36 | const [logout, { data, loading, error }] = useMutation(LOGOUT_MUTATION); 37 | 38 | // redirect to home page after logout 39 | if (data?.logout.ok) { 40 | useRouter().reload(); 41 | } 42 | 43 | return ( 44 |
45 | 46 | 47 | 48 | 49 | 63 | 64 | 65 | handleClose()}> 66 | 67 | My Profile 68 | 69 | 70 | handleClose()}> 71 | 72 | Bookmarks 73 | 74 | 75 | handleClose()}> 76 | 77 | Settings 78 | 79 | 80 | logout()}>Logout 81 | 82 | 83 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from "@apollo/client"; 2 | import { useApollo, initializeApollo } from "../lib/apollo"; 3 | import Layout from "../components/Layout"; 4 | import { ME_QUERY } from "../graphql/queries"; 5 | import "./../styles/index.scss"; 6 | import "font-awesome/css/font-awesome.min.css"; 7 | import "react-awesome-slider/dist/custom-animations/cube-animation.css"; 8 | import { useEffect } from "react"; 9 | import { ThemeProvider } from "@material-ui/core"; 10 | import CssBaseline from "@material-ui/core/CssBaseline"; 11 | import theme from "../styles/theme"; 12 | import Router from "next/router"; 13 | import NProgress from "nprogress"; //nprogress module 14 | import "nprogress/nprogress.css"; //styles of nprogress//Binding events. 15 | 16 | // handle show progress bar 17 | NProgress.configure({ showSpinner: false }); 18 | Router.events.on("routeChangeStart", () => NProgress.start()); 19 | Router.events.on("routeChangeComplete", () => NProgress.done()); 20 | Router.events.on("routeChangeError", () => NProgress.done()); 21 | 22 | // custom _app component 23 | function MyApp(props: any) { 24 | const { Component, pageProps } = props; 25 | 26 | const apolloClient = useApollo(pageProps?.initialApolloState); 27 | 28 | // will send me as a prop to each page in the Component props 29 | let me = props.meQuery; 30 | 31 | // part of nextjs - material Ui configuration 32 | useEffect(() => { 33 | // Remove the server-side injected CSS. 34 | const jssStyles = document.querySelector("#jss-server-side"); 35 | if (jssStyles) { 36 | jssStyles.parentElement!.removeChild(jssStyles); 37 | } 38 | }, []); 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | // we will get the current logged in user, send it to all Cpts 53 | // after the user logs in, i save the token in a cookie 54 | // the cookie is sent with every request on the server 55 | // i have access to the cookie in `getInitialProps` cause it rund on the server 56 | // we get the logged in user and send it as a prop to all the components 57 | // all the components as a props so we deal with authentication 58 | MyApp.getInitialProps = async ({ Component, ctx }: any) => { 59 | let pageProps = {}; 60 | const apolloClient = initializeApollo(); 61 | 62 | // get the cookies from the headers in the request object 63 | const token = ctx?.req?.headers.cookie ? ctx.req.headers.cookie : null; 64 | 65 | let meQuery = await apolloClient.query({ 66 | query: ME_QUERY, 67 | // apollo can send headers with every request in `setContext` func provided by apollo team 68 | // so here we have the token attached to every request, to know if the user has auth 69 | context: { headers: { token: token } } 70 | }); 71 | 72 | if (Component.getInitialProps) { 73 | pageProps = await Component.getInitialProps(ctx); 74 | } 75 | 76 | return { 77 | pageProps, 78 | meQuery 79 | }; 80 | }; 81 | 82 | export default MyApp; 83 | -------------------------------------------------------------------------------- /client/components/shared/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import AppBar from "@material-ui/core/AppBar"; 4 | import Toolbar from "@material-ui/core/Toolbar"; 5 | import IconButton from "@material-ui/core/IconButton"; 6 | import { Home } from "@material-ui/icons"; 7 | import HeaderProfile from "./HeaderProfile"; 8 | import { 9 | Box, 10 | Container, 11 | CssBaseline, 12 | Typography, 13 | useScrollTrigger 14 | } from "@material-ui/core"; 15 | 16 | function ElevationScroll(props) { 17 | const { children, window } = props; 18 | // Note that you normally won't need to set the window ref as useScrollTrigger 19 | // will default to window. 20 | // This is only being set here because the demo is in an iframe. 21 | const trigger = useScrollTrigger({ 22 | disableHysteresis: true, 23 | threshold: 0, 24 | target: window ? window() : undefined 25 | }); 26 | 27 | return React.cloneElement(children, { 28 | elevation: trigger ? 4 : 0 29 | }); 30 | } 31 | 32 | function Header(props) { 33 | // check whether the user is logged in or not 34 | const { me } = props; 35 | 36 | return ( 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {!me.ok && ( 50 | 51 | LogIn 52 | 53 | )} 54 | 55 | {!me.ok && ( 56 | 57 | SignUp 58 | 59 | )} 60 | 61 | {me.ok && ( 62 | 63 | My Profile 64 | 65 | )} 66 | 67 | {me.ok && ( 68 | 69 | Chat 70 | 71 | )} 72 | 73 | {me.ok && ( 74 | 75 | Friends 76 | 77 | )} 78 | 79 | {me.ok && ( 80 | 81 | Photos 82 | 83 | )} 84 | 85 | {me.ok && ( 86 | 87 | Bookmarks 88 | 89 | )} 90 | 91 | {me.ok && ( 92 | 93 | Settings 94 | 95 | )} 96 | 97 | {me.ok && ( 98 |
99 | 100 |
101 | )} 102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | 109 | export default Header; 110 | -------------------------------------------------------------------------------- /client/lib/apollo.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { ApolloClient, InMemoryCache, split } from "@apollo/client"; 3 | import { setContext } from "@apollo/client/link/context"; 4 | import Cookies from "js-cookie"; 5 | import fetch from "isomorphic-unfetch"; 6 | import { createUploadLink } from "apollo-upload-client"; 7 | import { WebSocketLink } from "@apollo/client/link/ws"; 8 | import { getMainDefinition } from "apollo-utilities"; 9 | 10 | let apolloClient: ApolloClient; 11 | let token: string; 12 | const isBrowser = typeof window !== "undefined"; 13 | 14 | // http link 15 | const httpLink = createUploadLink({ 16 | uri: 17 | process.env.NODE_ENV == "production" 18 | ? `https://huhuhu.vercel.app/graphql` 19 | : "http://localhost:5000/graphql", // Server URL (must be absolute) 20 | credentials: "include", // Additional fetch() options like `credentials` or `headers` 21 | fetch 22 | }); 23 | 24 | // websocket link 25 | const wsLink = isBrowser 26 | ? new WebSocketLink({ 27 | uri: 28 | process.env.NODE_ENV == "production" 29 | ? `wss://huhuhu.vercel.app/subscriptions` 30 | : `ws://localhost:5000/subscriptions`, 31 | options: { reconnect: true } 32 | }) 33 | : null; 34 | 35 | // splitLink decides which link to use 36 | const splitLink = isBrowser 37 | ? split( 38 | ({ query }) => { 39 | const definition = getMainDefinition(query); 40 | return ( 41 | definition.kind === "OperationDefinition" && 42 | definition.operation === "subscription" 43 | ); 44 | }, 45 | wsLink, 46 | httpLink as any 47 | ) 48 | : httpLink; 49 | 50 | // will user `setContext` to send the token with every request 51 | const authLink = setContext((_, { headers }) => { 52 | // get the authentication token from protected route context 53 | if (typeof window !== "undefined") { 54 | token = Cookies.get("token"); 55 | } 56 | if (headers) { 57 | token = headers.token; 58 | } 59 | 60 | // return the headers to the context so httpLink can read them 61 | return { 62 | headers: { 63 | ...headers, 64 | token: token ? token : "" 65 | } 66 | }; 67 | }); 68 | 69 | // create an apollo client 70 | function createApolloClient() { 71 | return new ApolloClient({ 72 | uri: "/graphql", 73 | ssrMode: !isBrowser, 74 | link: authLink.concat(splitLink as any), 75 | cache: new InMemoryCache() 76 | }); 77 | } 78 | 79 | export function initializeApollo(initialState = null) { 80 | const _apolloClient = apolloClient ?? createApolloClient(); 81 | 82 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state 83 | // get hydrated here 84 | if (initialState) { 85 | _apolloClient.cache.restore(initialState); 86 | } 87 | // For SSG and SSR always create a new Apollo Client 88 | if (typeof window === "undefined") return _apolloClient; 89 | // Create the Apollo Client once in the client 90 | if (!apolloClient) apolloClient = _apolloClient; 91 | 92 | return _apolloClient; 93 | } 94 | 95 | export function useApollo(initialState) { 96 | const store = useMemo(() => initializeApollo(initialState), [initialState]); 97 | 98 | return store; 99 | } 100 | -------------------------------------------------------------------------------- /server/schema/typeDefs/user.ts: -------------------------------------------------------------------------------- 1 | //This defines the user typeDef 2 | const userTypeDef = ` 3 | scalar Upload 4 | 5 | type ContactPlatform { 6 | skype: String 7 | instagram: String 8 | snapchat: String 9 | facebook: String 10 | website: String 11 | } 12 | 13 | input ContactPlatformInput { 14 | skype: String 15 | instagram: String 16 | snapchat: String 17 | facebook: String 18 | website: String 19 | } 20 | 21 | type User { 22 | id: ID 23 | userName: String! 24 | email: String! 25 | firstName: String! 26 | lastName: String! 27 | gender: String 28 | country: String 29 | city: String 30 | pictures: [String!] 31 | avatarUrl: String 32 | role: String! 33 | birthday: String 34 | friends: [User!] 35 | friendsPending: [User!] 36 | bookmarks: [User!] 37 | chats: [Chat!] 38 | messages: [Message] 39 | status: String 40 | speakLanguages: [String!] 41 | learnLanguages: [String!] 42 | education: String 43 | job: String 44 | relationship: String 45 | contactInfo: ContactPlatform 46 | aboutMe: String 47 | hobbies: [String!] 48 | music: [String!] 49 | books: [String!] 50 | movies: [String!] 51 | tvShows: [String!] 52 | createdAt: String! 53 | } 54 | 55 | type OneUserQuery{ 56 | ok: Boolean! 57 | user: User 58 | error: String 59 | } 60 | 61 | type AllUsersQuery{ 62 | ok: Boolean! 63 | users: [User!] 64 | error: String 65 | } 66 | 67 | type FriendRequestsQuery{ 68 | ok: Boolean! 69 | friends: [User!] 70 | error: String 71 | } 72 | 73 | type UserPayload{ 74 | ok: Boolean! 75 | successMessage: String 76 | error: String 77 | user: User 78 | } 79 | 80 | type ActionPayload{ 81 | ok: Boolean! 82 | error: String 83 | successMessage: String 84 | } 85 | 86 | extend type Query { 87 | me: OneUserQuery! 88 | users: AllUsersQuery! 89 | userInfo(userName: String!): OneUserQuery! 90 | friendRequests(userName: String!): FriendRequestsQuery! 91 | } 92 | 93 | extend type Mutation { 94 | signUp( 95 | userName: String!, 96 | email: String!, 97 | password: String!, 98 | verifyPassword: String!, 99 | firstName: String!, 100 | lastName: String!, 101 | gender: String!, 102 | country: String! 103 | ): UserPayload! 104 | 105 | login(userName: String!, password: String!): UserPayload! 106 | 107 | logout: ActionPayload! 108 | 109 | deleteUser(id:ID!): ActionPayload! 110 | 111 | uploadProfilePictures(file: [Upload!]): ActionPayload! 112 | 113 | deleteProfilePicture(name: String): ActionPayload! 114 | 115 | chooseProfilePicture(name: String): ActionPayload! 116 | 117 | updateProfileInfo( 118 | firstName: String, 119 | lastName: String, 120 | gender: String, 121 | country: String, 122 | city: String, 123 | birthday: String, 124 | speakLanguages: [String!], 125 | learnLanguages: [String!], 126 | education: String, 127 | job: String, 128 | relationship: String, 129 | contactInfo: ContactPlatformInput, 130 | aboutMe: String, 131 | hobbies: [String!], 132 | music: [String!], 133 | books: [String!], 134 | movies: [String!], 135 | tvShows: [String!], 136 | ): ActionPayload! 137 | 138 | 139 | addFriend(id: ID!): ActionPayload! 140 | 141 | acceptFriend(id: ID!): ActionPayload! 142 | 143 | deleteFriend(id: ID!): ActionPayload! 144 | 145 | addBookmark(id: ID!): ActionPayload! 146 | 147 | deleteBookmark(id: ID!): ActionPayload! 148 | 149 | } 150 | `; 151 | 152 | export default userTypeDef; 153 | -------------------------------------------------------------------------------- /server/models/User.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from "mongoose"; 2 | import * as bcrypt from "bcrypt"; 3 | import { maleAvatars, femaleAvatars } from "./avatars"; 4 | 5 | const Schema = mongoose.Schema; 6 | 7 | export interface IUser extends mongoose.Document { 8 | userName: string; 9 | email: string; 10 | password: string; 11 | firstName: string; 12 | lastName: string; 13 | pictures: Array; 14 | avatarUrl: string; 15 | role: string; 16 | birthday: string; 17 | friendsPending: Array; 18 | friends: Array; 19 | bookmarks: Array; 20 | messages: Array; 21 | chats: Array; 22 | gender: string; 23 | country: string; 24 | city: string; 25 | status: string; 26 | speakLanguages: Array; 27 | learnLanguages: Array; 28 | education: string; 29 | job: string; 30 | relationship: string; 31 | contactInfo: Array; 32 | aboutMe: string; 33 | hobbies: Array; 34 | music: Array; 35 | books: Array; 36 | movies: Array; 37 | tvShows: Array; 38 | } 39 | 40 | // our user schema 41 | const UserSchema = new Schema( 42 | { 43 | userName: { type: String, required: true, unique: true }, 44 | email: { type: String, required: true, unique: true }, 45 | password: { type: String, required: true }, 46 | firstName: { type: String, required: true }, 47 | lastName: { type: String, required: true }, 48 | pictures: { type: Array }, 49 | avatarUrl: { type: String }, 50 | role: { type: String, required: true }, 51 | birthday: { type: String }, 52 | friendsPending: { type: [Schema.Types.ObjectId], ref: "NewUser" }, 53 | friends: { type: [Schema.Types.ObjectId], ref: "NewUser" }, 54 | bookmarks: { type: [Schema.Types.ObjectId], ref: "NewUser" }, 55 | messages: { type: [Schema.Types.ObjectId], ref: "Message" }, 56 | chats: { type: [Schema.Types.ObjectId], ref: "Chat" }, 57 | gender: { type: String, required: true }, 58 | country: { type: String }, 59 | city: { type: String }, 60 | status: { type: String }, 61 | speakLanguages: { type: Array, default: [] }, 62 | learnLanguages: { type: Array, default: [] }, 63 | education: { type: String }, 64 | job: { type: String }, 65 | relationship: { type: String }, 66 | contactInfo: { 67 | skype: String, 68 | facebook: String, 69 | snapchat: String, 70 | instagram: String, 71 | website: String 72 | }, 73 | aboutMe: { type: String }, 74 | hobbies: { type: Array, default: [] }, 75 | music: { type: Array, default: [] }, 76 | books: { type: Array, default: [] }, 77 | movies: { type: Array, default: [] }, 78 | tvShows: { type: Array, default: [] } 79 | }, 80 | { timestamps: true } 81 | ); 82 | 83 | // encrypt the password before it's saved in the database 84 | UserSchema.pre("save", function (next: mongoose.HookNextFunction): void { 85 | const user: any = this; 86 | 87 | if (!user.password) { 88 | next; 89 | } else { 90 | if (user.gender == "male") { 91 | const random = Math.floor(Math.random() * maleAvatars.length); 92 | const randomMaleAvatar = maleAvatars[random]; 93 | user.avatarUrl = randomMaleAvatar; 94 | } else { 95 | const random = Math.floor(Math.random() * femaleAvatars.length); 96 | const randomFemaleAvatar = femaleAvatars[random]; 97 | user.avatarUrl = randomFemaleAvatar; 98 | } 99 | 100 | bcrypt.genSalt(10, function (err: any, salt: string): void { 101 | if (err) { 102 | throw new Error(err); 103 | } else { 104 | bcrypt.hash(user.password, salt, function (err: any, hashed: string) { 105 | if (err) { 106 | return next(err); 107 | } 108 | user.password = hashed; 109 | next(); 110 | }); 111 | } 112 | }); 113 | } 114 | }); 115 | 116 | const NewUser = mongoose.model("NewUser", UserSchema); 117 | export default NewUser; 118 | -------------------------------------------------------------------------------- /client/components/edit-profile/Contact.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Formik, Form, Field } from "formik"; 3 | import { Grid, Button } from "@material-ui/core"; 4 | import * as Yup from "yup"; 5 | import { TextField } from "formik-material-ui"; 6 | import { useMutation } from "@apollo/client"; 7 | import { UPDATE_PROGILE_MUTATION } from "../../graphql/mutations"; 8 | import ErrorMessage from "../ToastMessage"; 9 | import Router from "next/router"; 10 | 11 | // form validation using Yup 12 | const validate = () => 13 | Yup.object({ 14 | skype: Yup.string().notRequired().min(2, "Must be more than one character"), 15 | instagram: Yup.string().notRequired().min(2, "Must be more than one character"), 16 | snapchat: Yup.string().notRequired().min(2, "Must be more than one character"), 17 | facebook: Yup.string().notRequired().min(2, "Must be more than one character"), 18 | website: Yup.string().notRequired().min(2, "Must be more than one character") 19 | }); 20 | 21 | const Contact = ({ me }) => { 22 | let { contactInfo } = me; 23 | 24 | // handle the update profile mutation 25 | const [updateProfile, { data, loading }] = useMutation(UPDATE_PROGILE_MUTATION); 26 | 27 | // show error or success message 28 | const handleSuccessError = () => { 29 | if (data?.updateProfileInfo.ok) { 30 | Router.push(`/users/${me.userName}`); 31 | return ( 32 | 33 | ); 34 | } 35 | if (data?.updateProfileInfo.error) { 36 | return ; 37 | } 38 | }; 39 | 40 | return ( 41 | <> 42 | {handleSuccessError()} 43 | { 53 | let socialMedia = { 54 | skype: values.skype, 55 | instagram: values.instagram, 56 | snapchat: values.snapchat, 57 | facebook: values.facebook, 58 | website: values.website 59 | }; 60 | 61 | updateProfile({ 62 | variables: { 63 | contactInfo: socialMedia 64 | } 65 | }); 66 | setSubmitting(false); 67 | }}> 68 |
69 | {/* Skype */} 70 | 71 | 72 | Skype 73 | 74 | 75 | 76 | 77 | 78 | 79 | {/* Instagram */} 80 | 81 | 82 | Instagram 83 | 84 | 85 | 86 | 87 | 88 | 89 | {/* Snapchat */} 90 | 91 | 92 | Snapchat 93 | 94 | 95 | 96 | 97 | 98 | 99 | {/* Facebook */} 100 | 101 | 102 | Facebook 103 | 104 | 105 | 106 | 107 | 108 | 109 | {/* Website */} 110 | 111 | 112 | Website 113 | 114 | 115 | 116 | 117 | 118 | 119 | {/* Submit buttom */} 120 | 123 |
124 |
125 | 126 | ); 127 | }; 128 | 129 | export default Contact; 130 | -------------------------------------------------------------------------------- /client/graphql/queries.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | // fetch the current logged in user query 4 | export const ME_QUERY = gql` 5 | query myAccountInfo { 6 | me { 7 | ok 8 | error 9 | user { 10 | id 11 | userName 12 | email 13 | firstName 14 | lastName 15 | gender 16 | country 17 | city 18 | pictures 19 | avatarUrl 20 | role 21 | birthday 22 | friendsPending { 23 | id 24 | userName 25 | firstName 26 | lastName 27 | avatarUrl 28 | } 29 | bookmarks { 30 | id 31 | userName 32 | firstName 33 | lastName 34 | avatarUrl 35 | } 36 | chats { 37 | id 38 | } 39 | messages { 40 | id 41 | } 42 | contactInfo { 43 | skype 44 | instagram 45 | snapchat 46 | facebook 47 | website 48 | } 49 | status 50 | speakLanguages 51 | learnLanguages 52 | education 53 | job 54 | relationship 55 | aboutMe 56 | hobbies 57 | music 58 | books 59 | movies 60 | tvShows 61 | createdAt 62 | } 63 | } 64 | } 65 | `; 66 | 67 | // fetch one user query 68 | export const ONE_USER_QUERY = gql` 69 | query userInfo($userName: String!) { 70 | userInfo(userName: $userName) { 71 | ok 72 | error 73 | user { 74 | id 75 | userName 76 | email 77 | firstName 78 | lastName 79 | gender 80 | country 81 | city 82 | pictures 83 | avatarUrl 84 | role 85 | birthday 86 | friends { 87 | id 88 | userName 89 | firstName 90 | lastName 91 | avatarUrl 92 | } 93 | friendsPending { 94 | id 95 | userName 96 | firstName 97 | lastName 98 | avatarUrl 99 | } 100 | chats { 101 | id 102 | } 103 | messages { 104 | id 105 | } 106 | contactInfo { 107 | skype 108 | instagram 109 | snapchat 110 | facebook 111 | website 112 | } 113 | status 114 | speakLanguages 115 | learnLanguages 116 | education 117 | job 118 | relationship 119 | aboutMe 120 | hobbies 121 | music 122 | books 123 | movies 124 | tvShows 125 | createdAt 126 | } 127 | } 128 | } 129 | `; 130 | 131 | // fetch all users query 132 | export const ALL_USERS_QUERY = gql` 133 | query getAllUsers { 134 | users { 135 | ok 136 | error 137 | users { 138 | id 139 | userName 140 | email 141 | firstName 142 | lastName 143 | gender 144 | country 145 | city 146 | pictures 147 | avatarUrl 148 | role 149 | birthday 150 | friendsPending { 151 | id 152 | userName 153 | firstName 154 | lastName 155 | avatarUrl 156 | } 157 | 158 | chats { 159 | id 160 | } 161 | messages { 162 | id 163 | } 164 | contactInfo { 165 | skype 166 | instagram 167 | snapchat 168 | facebook 169 | website 170 | } 171 | status 172 | speakLanguages 173 | learnLanguages 174 | education 175 | job 176 | relationship 177 | aboutMe 178 | hobbies 179 | music 180 | books 181 | movies 182 | tvShows 183 | createdAt 184 | } 185 | } 186 | } 187 | `; 188 | 189 | // fetch all user chats query 190 | export const ALL_USER_CHATS_QUERY = gql` 191 | query userChats { 192 | userChats { 193 | ok 194 | error 195 | chats { 196 | id 197 | users { 198 | id 199 | userName 200 | firstName 201 | lastName 202 | avatarUrl 203 | } 204 | messages { 205 | id 206 | text 207 | createdAt 208 | user { 209 | id 210 | userName 211 | firstName 212 | lastName 213 | avatarUrl 214 | } 215 | } 216 | } 217 | } 218 | } 219 | `; 220 | -------------------------------------------------------------------------------- /client/pages/profile/editProfile.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AppBar from "@material-ui/core/AppBar"; 3 | import Tab from "@material-ui/core/Tab"; 4 | import TabContext from "@material-ui/lab/TabContext"; 5 | import TabList from "@material-ui/lab/TabList"; 6 | import { 7 | makeStyles, 8 | Theme, 9 | Grid, 10 | Box, 11 | Typography, 12 | useTheme, 13 | Tabs 14 | } from "@material-ui/core"; 15 | import General from "../../components/edit-profile/General"; 16 | import Head from "next/head"; 17 | import Contact from "../../components/edit-profile/Contact"; 18 | import AboutMe from "../../components/edit-profile/AboutMe"; 19 | import Languages from "../../components/edit-profile/Languages"; 20 | import SwipeableViews from "react-swipeable-views"; 21 | 22 | interface TabPanelProps { 23 | children?: React.ReactNode; 24 | dir?: string; 25 | index: any; 26 | value: any; 27 | } 28 | 29 | function TabPanel(props: TabPanelProps) { 30 | const { children, value, index, ...other } = props; 31 | 32 | return ( 33 | 45 | ); 46 | } 47 | 48 | function a11yProps(index: any) { 49 | return { 50 | id: `full-width-tab-${index}`, 51 | "aria-controls": `full-width-tabpanel-${index}` 52 | }; 53 | } 54 | 55 | const useStyles = makeStyles((theme: Theme) => ({ 56 | root: { 57 | backgroundColor: theme.palette.background.paper, 58 | width: 500 59 | } 60 | })); 61 | 62 | const editProfile = props => { 63 | const { user } = props.data.me; 64 | 65 | const classes = useStyles(); 66 | const theme = useTheme(); 67 | 68 | const [value, setValue] = React.useState(0); 69 | 70 | const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { 71 | setValue(newValue); 72 | }; 73 | 74 | const handleChangeIndex = (index: number) => { 75 | setValue(index); 76 | }; 77 | 78 | return ( 79 | <> 80 | 81 | My Profile - Update 82 | 83 | 84 | 85 | {/* swipeable all settings list */} 86 | 87 |

My Account

88 | 89 | 90 | {/* all settings tabs */} 91 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | {/* every tab content */} 107 | 111 | {/* each tab details */} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | Settings 130 | 131 | 132 |
133 | 134 | 135 | Right Side 136 | 137 |
138 | 139 | ); 140 | }; 141 | 142 | // redirect to home page if there is not user 143 | export const getServerSideProps = async ({ req, res }) => { 144 | if (!req.headers.cookie) { 145 | res.writeHead(302, { 146 | // or 301 147 | Location: "/" 148 | }); 149 | res.end(); 150 | } 151 | return { props: {} }; 152 | }; 153 | 154 | export default editProfile; 155 | -------------------------------------------------------------------------------- /client/pages/homePage.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { initializeApollo } from "../lib/apollo"; 3 | import UsersList from "../components/UsersList"; 4 | import { ALL_USERS_QUERY } from "../graphql/queries"; 5 | import { Grid, createStyles, makeStyles, Theme, Avatar } from "@material-ui/core"; 6 | import Link from "next/link"; 7 | import Typed from "react-typed"; 8 | 9 | const useStyles = makeStyles((theme: Theme) => 10 | createStyles({ 11 | root: { 12 | flexGrow: 1 13 | }, 14 | paper: { 15 | padding: theme.spacing(2), 16 | textAlign: "center", 17 | color: theme.palette.text.secondary 18 | } 19 | }) 20 | ); 21 | 22 | function homePage(props) { 23 | const classes = useStyles(); 24 | 25 | let { users } = props.users; 26 | 27 | // extract the logged in user 28 | let { me, loading } = props.data; 29 | let { error, ok, user } = me; 30 | 31 | //calculate user age depends on his birthday 32 | const userAge = birthday => { 33 | let birthDate = new Date(birthday); 34 | let nowDate = new Date(); 35 | 36 | let years = nowDate.getFullYear() - birthDate.getFullYear(); 37 | return {years}; 38 | }; 39 | 40 | return ( 41 | <> 42 | 43 | Social App 44 | 45 | 46 |
47 | 48 | {/* left section */} 49 | 50 | {/* user greetings */} 51 | 52 | {/* user infi */} 53 | 54 | 60 | 61 | 62 | {/* home page typed */} 63 | 64 | {/* username */} 65 | 71 | 82 | 83 | 84 | 85 | {/* Quick search */} 86 | 87 |

Make new friends

88 | 89 | {/* some random users */} 90 | 91 | {users && 92 | users.map(user => ( 93 | /* one user */ 94 | 95 | 96 | 97 | <> 98 | 99 | {user.firstName}, {userAge(user.birthday)} 100 | 101 | 102 | 103 | 104 |
{user.country}
105 |
106 | ))} 107 |
108 |
109 |
110 | 111 | {/********* Home page right side *************/} 112 | 113 | Right Side 114 | 115 |
116 |
117 | 118 | ); 119 | } 120 | 121 | export async function getServerSideProps({ req, res }) { 122 | if (!req.headers.cookie) { 123 | res.writeHead(302, { 124 | // or 301 125 | Location: "/" 126 | }); 127 | res.end(); 128 | } 129 | 130 | const apolloClient = initializeApollo(); 131 | 132 | const allUsers = await apolloClient.query({ 133 | query: ALL_USERS_QUERY 134 | }); 135 | 136 | const users = allUsers.data.users; 137 | 138 | return { 139 | props: { 140 | initialApolloState: apolloClient.cache.extract(), 141 | users: JSON.parse(JSON.stringify(users)) 142 | } 143 | }; 144 | } 145 | 146 | export default homePage; 147 | -------------------------------------------------------------------------------- /client/pages/bookmarks/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { createStyles, Theme, makeStyles } from "@material-ui/core/styles"; 3 | import List from "@material-ui/core/List"; 4 | import ListItem from "@material-ui/core/ListItem"; 5 | import ListItemText from "@material-ui/core/ListItemText"; 6 | import ListItemAvatar from "@material-ui/core/ListItemAvatar"; 7 | import Avatar from "@material-ui/core/Avatar"; 8 | import Link from "next/link"; 9 | import { Grid, IconButton, Tooltip } from "@material-ui/core"; 10 | import DeleteIcon from "@material-ui/icons/Delete"; 11 | import { useMutation } from "@apollo/client"; 12 | import { DELETE_BOOKMARK_MUTATION } from "../../graphql/mutations"; 13 | import ErrorMessage from "../../components/ToastMessage"; 14 | import { ME_QUERY } from "../../graphql/queries"; 15 | import { initializeApollo } from "../../lib/apollo"; 16 | import Head from "next/head"; 17 | 18 | const useStyles = makeStyles((theme: Theme) => 19 | createStyles({ 20 | root: { 21 | width: "100%", 22 | maxWidth: 360 23 | } 24 | }) 25 | ); 26 | 27 | export default function index(props) { 28 | let bookmarks = props.me.bookmarks; 29 | 30 | const [userBookmarks, setUserBookmarks] = useState(bookmarks); 31 | 32 | const classes = useStyles(); 33 | 34 | // handle add friend mutation 35 | const [delete_bookmark, { data }] = useMutation(DELETE_BOOKMARK_MUTATION); 36 | 37 | return ( 38 | <> 39 | 40 | Bookmarks 41 | 42 | 43 | {data?.deleteBookmark.ok && ( 44 | 45 | )} 46 | 47 | {data?.deleteBookmark.error && ( 48 | 49 | )} 50 | 51 | 52 | {/* left section */} 53 | 54 | {userBookmarks.length == 0 && ( 55 |
56 | Your bookmarks is empty,{" "} 57 | { 58 | 59 | get to know people. 60 | 61 | }{" "} 62 |
63 | )} 64 | 65 | {userBookmarks.length > 0 && 66 | userBookmarks.map(user => ( 67 | 73 | {/* photo */} 74 | 75 | 76 | 77 | 78 | {/* username */} 79 | 80 | 81 | {user.firstName + " " + user.lastName} 82 | 83 | 84 | 85 | {/* delete button */} 86 | 87 | 88 | 89 | { 91 | delete_bookmark({ variables: { id: user.id } }); 92 | setUserBookmarks( 93 | userBookmarks.filter(bookmark => bookmark.id !== user.id) 94 | ); 95 | }} 96 | /> 97 | 98 | 99 | 100 | 101 | ))} 102 |
103 | 104 | {/* right section */} 105 | 106 | right side 107 | 108 |
109 | 110 | ); 111 | } 112 | 113 | // Fetch necessary data for the blog post using params.id 114 | export async function getServerSideProps(ctx) { 115 | //redirect if there is no authentcated user 116 | if (!ctx.req.headers.cookie) { 117 | ctx.res.writeHead(302, { 118 | // or 301 119 | Location: "/" 120 | }); 121 | ctx.res.end(); 122 | } 123 | 124 | const apolloClient = initializeApollo(); 125 | 126 | // get the cookies from the headers in the request object 127 | const token = ctx.req.headers.cookie ? ctx.req.headers.cookie : null; 128 | 129 | let oneUserQuery = await apolloClient.query({ 130 | query: ME_QUERY 131 | }); 132 | 133 | let me = oneUserQuery.data.me.user; 134 | 135 | return { 136 | props: { 137 | initialApolloState: apolloClient.cache.extract(), 138 | me: me 139 | } 140 | }; 141 | } 142 | -------------------------------------------------------------------------------- /client/pages/friends/[userName].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { initializeApollo } from "../../lib/apollo"; 3 | import { ONE_USER_QUERY } from "../../graphql/queries"; 4 | import Head from "next/head"; 5 | import AppBar from "@material-ui/core/AppBar"; 6 | import Tab from "@material-ui/core/Tab"; 7 | import TabContext from "@material-ui/lab/TabContext"; 8 | import TabList from "@material-ui/lab/TabList"; 9 | import { Box, Grid, makeStyles, Tabs, Typography, useTheme } from "@material-ui/core"; 10 | import FriendsRequests from "../../components/friends/FriendsRequests"; 11 | import FriendsList from "../../components/friends/FriendsList"; 12 | import { Theme } from "@material-ui/core/styles"; 13 | import SwipeableViews from "react-swipeable-views"; 14 | 15 | interface TabPanelProps { 16 | children?: React.ReactNode; 17 | dir?: string; 18 | index: any; 19 | value: any; 20 | } 21 | 22 | function TabPanel(props: TabPanelProps) { 23 | const { children, value, index, ...other } = props; 24 | 25 | return ( 26 | 38 | ); 39 | } 40 | 41 | function a11yProps(index: any) { 42 | return { 43 | id: `full-width-tab-${index}`, 44 | "aria-controls": `full-width-tabpanel-${index}` 45 | }; 46 | } 47 | 48 | const useStyles = makeStyles((theme: Theme) => ({ 49 | root: { 50 | backgroundColor: theme.palette.background.paper, 51 | width: 500 52 | } 53 | })); 54 | 55 | const Friends = props => { 56 | let me = props.data.me.user; 57 | 58 | const classes = useStyles(); 59 | const theme = useTheme(); 60 | 61 | const [value, setValue] = React.useState(0); 62 | 63 | const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { 64 | setValue(newValue); 65 | }; 66 | 67 | /* handle change material ui tabs index */ 68 | const handleChangeIndex = (index: number) => { 69 | setValue(index); 70 | }; 71 | 72 | const showDeleteFriend = me.id === props.user.user.id; 73 | 74 | return ( 75 | <> 76 | 77 | Friends 78 | 79 | 80 | {/* swipeable friends and requests list */} 81 | 82 | 83 | {/* friends and requests tabs */} 84 | 91 | 92 | {me.id === props.user.user.id && } 93 | 94 | 95 | 96 | {/* every tab content */} 97 | 101 | 102 | 106 | 107 | 108 | {me.id === props.user.user.id && ( 109 | 110 | 111 | 112 | )} 113 | 114 | 115 | 116 | 117 | Right Side 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | // Fetch necessary data for the blog post using params.id 125 | export async function getServerSideProps(ctx) { 126 | //redirect if there is no authentcated user 127 | if (!ctx.req.headers.cookie) { 128 | ctx.res.writeHead(302, { 129 | // or 301 130 | Location: "/" 131 | }); 132 | ctx.res.end(); 133 | } 134 | 135 | const apolloClient = initializeApollo(); 136 | 137 | // get the cookies from the headers in the request object 138 | const token = ctx.req.headers.cookie ? ctx.req.headers.cookie : null; 139 | 140 | let oneUserQuery = await apolloClient.query({ 141 | query: ONE_USER_QUERY, 142 | variables: { userName: ctx.params.userName } 143 | }); 144 | 145 | let user = oneUserQuery.data.userInfo; 146 | 147 | return { 148 | props: { 149 | initialApolloState: apolloClient.cache.extract(), 150 | user: JSON.parse(JSON.stringify(user)) 151 | } 152 | }; 153 | } 154 | 155 | export default Friends; 156 | -------------------------------------------------------------------------------- /server/middlewares/validation/userValidation.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | import User from "../../models/User"; 3 | import * as bcrypt from "bcrypt"; 4 | 5 | // validate user inputs using Yup 6 | export const signupvalidatation = Yup.object().shape({ 7 | userName: Yup.string() 8 | .trim() 9 | .min(2, "Must be more than one character") 10 | .required("Username is required") 11 | .test("uniqueUsername", "This username already exists", async userName => { 12 | const user = await User.findOne({ userName: userName }); 13 | return !user; 14 | }), 15 | email: Yup.string() 16 | .email("Please enter a vaild email") 17 | .required("email is required") 18 | .test("uniqueEmail", "This email already exists", async email => { 19 | const user = await User.findOne({ email: email }); 20 | return !user; 21 | }), 22 | password: Yup.string() 23 | .trim() 24 | .required("password is required") 25 | .min(6, "Password is too short") 26 | .matches( 27 | /[a-zA-Z0-9@!#%]/, 28 | "Password can only contain Latin letters, numbers and/or [@, !, #, %]." 29 | ), 30 | verifyPassword: Yup.string() 31 | .oneOf([Yup.ref("password"), null], "Passwords must match") 32 | .min(8, "Must be more than 8 characters") 33 | .required("This field is required"), 34 | firstName: Yup.string() 35 | .min(2, "Must be more than one character") 36 | .required("first Name is required"), 37 | lastName: Yup.string() 38 | .min(2, "Must be more than one character") 39 | .required("lastName is required"), 40 | gender: Yup.string() 41 | .min(2, "Must be more than one character") 42 | .required("Gender is required"), 43 | country: Yup.string() 44 | .min(2, "Must be more than one character") 45 | .required("Country is required") 46 | }); 47 | 48 | // validate user inputs using Yup 49 | export const loginValidatation = Yup.object().shape({ 50 | userName: Yup.string() 51 | .trim() 52 | .min(2, "Must be more than one character") 53 | .required("Username is required") 54 | .test("checkUsername", "Invalid username", async userName => { 55 | const user = await User.findOne({ userName: userName }); 56 | return !!user; 57 | }), 58 | password: Yup.string() 59 | .trim() 60 | .required("password is required") 61 | .min(6, "Password is too short") 62 | .when("userName", (userName: string, schema: any) => 63 | schema.test({ 64 | test: async (password: string) => { 65 | const user = await User.findOne({ userName: userName }); 66 | const valid = await bcrypt.compare(password, user!.password); 67 | return valid; 68 | }, 69 | message: "Invalid password" 70 | }) 71 | ) 72 | }); 73 | 74 | // validate update user info using Yup 75 | export const updateProfileValidation = Yup.object().shape({ 76 | firstName: Yup.string() 77 | .notRequired() 78 | .trim() 79 | .min(2, "First name must be more than one character"), 80 | lastName: Yup.string() 81 | .notRequired() 82 | .trim() 83 | .min(2, "Last name must be more than one character"), 84 | gender: Yup.string() 85 | .notRequired() 86 | .trim() 87 | .min(2, "Gender must be more than one character"), 88 | city: Yup.string().notRequired().trim().min(2, "City must be more than one character"), 89 | birthday: Yup.date().notRequired(), 90 | education: Yup.string() 91 | .notRequired() 92 | .trim() 93 | .min(2, "Education must be more than one character"), 94 | job: Yup.string().notRequired().trim().min(2, "Job must be more than one character"), 95 | relationship: Yup.string() 96 | .notRequired() 97 | .trim() 98 | .min(2, "Relationship must be more than one character"), 99 | aboutMe: Yup.string() 100 | .notRequired() 101 | .trim() 102 | .min(2, "About me must be more than one character"), 103 | speakLanguages: 104 | Yup.array() 105 | .of(Yup.string()) 106 | .compact(value => value != typeof String) 107 | .ensure() || Yup.array(), 108 | learnLanguages: 109 | Yup.array() 110 | .of(Yup.string()) 111 | .compact(value => value != typeof String) 112 | .ensure() || Yup.array(), 113 | hobbies: 114 | Yup.array() 115 | .of(Yup.string()) 116 | .compact(value => value != typeof String) 117 | .ensure() || Yup.array(), 118 | music: 119 | Yup.array() 120 | .of(Yup.string()) 121 | .compact(value => value != typeof String) 122 | .ensure() || Yup.array(), 123 | books: 124 | Yup.array() 125 | .of(Yup.string()) 126 | .compact(value => value != typeof String) 127 | .ensure() || Yup.array(), 128 | movies: 129 | Yup.array() 130 | .of(Yup.string()) 131 | .compact(value => value != typeof String) 132 | .ensure() || Yup.array(), 133 | tvShows: 134 | Yup.array() 135 | .of(Yup.string()) 136 | .compact(value => value != typeof String) 137 | .ensure() || Yup.array() 138 | /* contactInfo: 139 | Yup.array() 140 | .of( 141 | Yup.object().shape({ 142 | skype: String 143 | }) 144 | ) 145 | .compact(value => value != typeof String) 146 | .ensure() || Yup.array() */ 147 | }); 148 | -------------------------------------------------------------------------------- /client/components/UploadProfileImages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Formik, Form } from "formik"; 3 | import { useMutation } from "@apollo/client"; 4 | import ErrorMessage from "./ToastMessage"; 5 | import { UPLOAD_PICTURES } from "../graphql/mutations"; 6 | import PhotoCamera from "@material-ui/icons/PhotoCamera"; 7 | import { 8 | Avatar, 9 | Button, 10 | createStyles, 11 | Grid, 12 | IconButton, 13 | makeStyles, 14 | Theme, 15 | Tooltip 16 | } from "@material-ui/core"; 17 | import { useRouter } from "next/router"; 18 | 19 | const useStyles = makeStyles((theme: Theme) => 20 | createStyles({ 21 | root: { 22 | "& > *": { 23 | margin: theme.spacing(1) 24 | } 25 | }, 26 | input: { 27 | display: "none" 28 | } 29 | }) 30 | ); 31 | 32 | export default function UploadProfileImages(props) { 33 | const classes = useStyles(); 34 | 35 | // the fileList images returned from the form 36 | const [imgs, setImgs] = useState([]); 37 | 38 | // the current images preview before upload 39 | const [preview, setPreview] = useState([]); 40 | 41 | // handle delete mutation 42 | const [upload, { data, loading, error }] = useMutation(UPLOAD_PICTURES); 43 | 44 | //handle on select preview 45 | const onDrop = e => { 46 | let filesToPreview: any[] = []; 47 | 48 | // to be able to preview images before uploading them 49 | // we will convert the files to object url 50 | for (let i = 0; i < e.target.files.length; i++) { 51 | filesToPreview.push(URL.createObjectURL(e.target.files[i])); 52 | } 53 | 54 | setPreview(preview.concat(filesToPreview)); 55 | 56 | // convert the fileList to an array so it accepts array functions 57 | let fileArray = Array.from(imgs); 58 | let filesRecievedFromFrom = Array.from(e.target.files); 59 | 60 | setImgs(fileArray.concat(filesRecievedFromFrom)); 61 | }; 62 | 63 | //handle delete one from preview before upload 64 | const hanldeDeletePicFromPreview = index => { 65 | const previewsAfterDelete = preview.filter((p, ind) => ind !== index); 66 | 67 | setPreview(previewsAfterDelete); 68 | 69 | // convert the fileList to an array so it accepts array functions 70 | let fileArray = Array.from(imgs); 71 | 72 | const imgsAfterDelete = fileArray.filter((p, ind) => ind !== index); 73 | 74 | setImgs(imgsAfterDelete); 75 | }; 76 | 77 | if (data?.uploadProfilePictures.ok) { 78 | useRouter().push(`/users/[userName]`, `/users/${props.user.userName}`, { 79 | shallow: true 80 | }); 81 | } 82 | 83 | return ( 84 | <> 85 | {data?.uploadProfilePictures.ok && ( 86 | 90 | )} 91 | 92 | {data?.uploadProfilePictures.error && ( 93 | 94 | )} 95 | 96 | { 106 | setSubmitting(false); 107 | }}> 108 |
109 | { 118 | onDrop(e); 119 | }} 120 | /> 121 | 122 | 127 | 128 | 129 | {loading && ( 130 | 133 | )} 134 | {!loading && ( 135 | 147 | )} 148 | 149 |
150 | 151 | {preview.length > 0 && 152 | preview.map((img, index) => ( 153 | 154 |
155 | 156 | hanldeDeletePicFromPreview(index)}> 159 | 160 | 161 |
162 |
163 | ))} 164 | 165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /client/pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField, Select } from "formik-material-ui"; 3 | import { Formik, Form, Field } from "formik"; 4 | import { Button, InputLabel, MenuItem, FormControl } from "@material-ui/core"; 5 | import * as Yup from "yup"; 6 | import { useMutation } from "@apollo/client"; 7 | import ErrorMessage from "../components/ToastMessage"; 8 | import Router from "next/router"; 9 | import { REGISTER_MUTATION } from "../graphql/mutations"; 10 | 11 | // form validation useing Yup 12 | const validate = () => 13 | Yup.object({ 14 | userName: Yup.string() 15 | .min(2, "Must be more than one character") 16 | .required("Username is required"), 17 | password: Yup.string() 18 | .min(8, "Must be more than 8 characters") 19 | .required("This field is required"), 20 | verifyPassword: Yup.string() 21 | .min(8, "Must be more than 8 characters") 22 | .required("This field is required"), 23 | firstName: Yup.string() 24 | .min(2, "Must be more than one character") 25 | .required("This field is required"), 26 | lastName: Yup.string() 27 | .min(2, "Must be more than one character") 28 | .required("This field is required"), 29 | email: Yup.string() 30 | .email("Please enter a vaild email") 31 | .required("This field is required"), 32 | gender: Yup.string().required("This field is required"), 33 | country: Yup.string().required("This field is required") 34 | }); 35 | 36 | export default function signup() { 37 | const [register, { data, loading, error }] = useMutation(REGISTER_MUTATION, { 38 | onError(error) { 39 | //console.log(error); 40 | } 41 | }); 42 | 43 | const handleRegisterSuccess = () => { 44 | try { 45 | if (data?.signUp?.ok) { 46 | Router.push("/"); 47 | return ; 48 | } 49 | } catch (e) { 50 | throw e; 51 | } 52 | }; 53 | 54 | return ( 55 | <> 56 | {data?.signUp?.error && } 57 | 58 | {handleRegisterSuccess()} 59 | 60 | { 73 | console.log(values); 74 | 75 | register({ 76 | variables: { 77 | userName: values.userName, 78 | email: values.email, 79 | password: values.password, 80 | verifyPassword: values.verifyPassword, 81 | firstName: values.firstName, 82 | lastName: values.lastName, 83 | gender: values.gender, 84 | country: values.country 85 | } 86 | }); 87 | setSubmitting(false); 88 | }}> 89 |
90 | 91 |
92 | 93 |
94 | 101 |
102 | 109 |
110 | 111 |
112 | 113 |
114 | 115 | Gender 116 | 121 | Male 122 | Female 123 | 124 | 125 |
126 | 127 |
128 | 129 | Country 130 | 135 | Egypt 136 | United States 137 | Switzerland 138 | 139 | 140 |
141 | 144 | 145 |
146 | 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /client/graphql/mutations.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | 3 | // new user registeration mutatiol 4 | export const REGISTER_MUTATION = gql` 5 | mutation signUp( 6 | $userName: String! 7 | $email: String! 8 | $password: String! 9 | $verifyPassword: String! 10 | $firstName: String! 11 | $lastName: String! 12 | $gender: String! 13 | $country: String! 14 | ) { 15 | signUp( 16 | userName: $userName 17 | email: $email 18 | password: $password 19 | verifyPassword: $verifyPassword 20 | firstName: $firstName 21 | lastName: $lastName 22 | gender: $gender 23 | country: $country 24 | ) { 25 | ok 26 | successMessage 27 | error 28 | } 29 | } 30 | `; 31 | 32 | // login mutation 33 | export const LOGIN_MUTATION = gql` 34 | mutation login($userName: String!, $password: String!) { 35 | login(userName: $userName, password: $password) { 36 | ok 37 | successMessage 38 | error 39 | } 40 | } 41 | `; 42 | 43 | // delete user mutation 44 | export const MUTATION_DELETE_USER = gql` 45 | mutation deleteUser($id: ID!) { 46 | deleteUser(id: $id) { 47 | ok 48 | successMessage 49 | error 50 | } 51 | } 52 | `; 53 | 54 | // logout mutation 55 | export const LOGOUT_MUTATION = gql` 56 | mutation logout { 57 | logout { 58 | ok 59 | } 60 | } 61 | `; 62 | 63 | // upload profile pics mutation 64 | export const UPLOAD_PICTURES = gql` 65 | mutation uploadProfilePictures($file: [Upload!]) { 66 | uploadProfilePictures(file: $file) { 67 | ok 68 | error 69 | successMessage 70 | } 71 | } 72 | `; 73 | 74 | // upload profile pics mutation 75 | export const DELETE_PICTURE = gql` 76 | mutation deleteProfilePicture($name: String) { 77 | deleteProfilePicture(name: $name) { 78 | ok 79 | error 80 | successMessage 81 | } 82 | } 83 | `; 84 | 85 | // update profile photo mutation 86 | export const CHOOSE_PROFILE_PICTURE = gql` 87 | mutation chooseProfilePicture($name: String) { 88 | chooseProfilePicture(name: $name) { 89 | ok 90 | error 91 | successMessage 92 | } 93 | } 94 | `; 95 | 96 | // update profile mutation 97 | export const UPDATE_PROGILE_MUTATION = gql` 98 | mutation updateProfileInfo( 99 | $firstName: String 100 | $lastName: String 101 | $gender: String 102 | $country: String 103 | $city: String 104 | $birthday: String 105 | $speakLanguages: [String!] 106 | $learnLanguages: [String!] 107 | $education: String 108 | $job: String 109 | $relationship: String 110 | $contactInfo: ContactPlatformInput 111 | $aboutMe: String 112 | $hobbies: [String!] 113 | $music: [String!] 114 | $books: [String!] 115 | $movies: [String!] 116 | $tvShows: [String!] 117 | ) { 118 | updateProfileInfo( 119 | firstName: $firstName 120 | lastName: $lastName 121 | gender: $gender 122 | country: $country 123 | city: $city 124 | birthday: $birthday 125 | speakLanguages: $speakLanguages 126 | learnLanguages: $learnLanguages 127 | education: $education 128 | job: $job 129 | relationship: $relationship 130 | contactInfo: $contactInfo 131 | aboutMe: $aboutMe 132 | hobbies: $hobbies 133 | music: $music 134 | books: $books 135 | movies: $movies 136 | tvShows: $tvShows 137 | ) { 138 | ok 139 | successMessage 140 | error 141 | } 142 | } 143 | `; 144 | 145 | // Add friend (send friend request) 146 | export const ADD_FRIEND_MUTATION = gql` 147 | mutation addFriend($id: ID!) { 148 | addFriend(id: $id) { 149 | ok 150 | error 151 | successMessage 152 | } 153 | } 154 | `; 155 | 156 | // Add friend (send friend request) 157 | export const ACCEPT_FRIEND_MUTATION = gql` 158 | mutation acceptFriend($id: ID!) { 159 | acceptFriend(id: $id) { 160 | ok 161 | error 162 | successMessage 163 | } 164 | } 165 | `; 166 | 167 | // delete friend 168 | export const DELETE_FRIEND_MUTATION = gql` 169 | mutation deleteFriend($id: ID!) { 170 | deleteFriend(id: $id) { 171 | ok 172 | error 173 | successMessage 174 | } 175 | } 176 | `; 177 | 178 | // Add profile to bookmarks 179 | export const ADD_BOOKMARK_MUTATION = gql` 180 | mutation addBookmark($id: ID!) { 181 | addBookmark(id: $id) { 182 | ok 183 | error 184 | successMessage 185 | } 186 | } 187 | `; 188 | 189 | // Delete profile to bookmarks 190 | export const DELETE_BOOKMARK_MUTATION = gql` 191 | mutation deleteBookmark($id: ID!) { 192 | deleteBookmark(id: $id) { 193 | ok 194 | error 195 | successMessage 196 | } 197 | } 198 | `; 199 | 200 | // Delete profile to bookmarks 201 | export const CREATE_NEW_CHAT_MUTATION = gql` 202 | mutation createNewChat($users: [ID!]) { 203 | createNewChat(users: $users) { 204 | ok 205 | error 206 | successMessage 207 | chat { 208 | id 209 | } 210 | } 211 | } 212 | `; 213 | 214 | // Delete profile to bookmarks 215 | export const SEND_MESSAGE_MUTATION = gql` 216 | mutation sendMessage($user: ID!, $chat: ID, $text: String!) { 217 | sendMessage(user: $user, chat: $chat, text: $text) { 218 | ok 219 | error 220 | successMessage 221 | } 222 | } 223 | `; 224 | -------------------------------------------------------------------------------- /client/components/edit-profile/Languages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Divider, Chip } from "@material-ui/core"; 3 | import { Formik, Form, Field } from "formik"; 4 | import { Grid, Button } from "@material-ui/core"; 5 | import { TextField } from "formik-material-ui"; 6 | import { useMutation } from "@apollo/client"; 7 | import { UPDATE_PROGILE_MUTATION } from "../../graphql/mutations"; 8 | import ErrorMessage from "../ToastMessage"; 9 | import Router from "next/router"; 10 | 11 | const Languages = ({ me }) => { 12 | // get the languaes state 13 | const [speakLanguages, setSpeakLanguages] = useState(me.speakLanguages); 14 | const [learnLanguages, setLearnLanguages] = useState(me.learnLanguages); 15 | 16 | const [speakLanguage, setSpeakLanguage] = useState(""); 17 | const [learnLanguage, setLearnLanguage] = useState(""); 18 | 19 | // handle add learn language 20 | const addSpeakLang = () => { 21 | if (!speakLanguage) return; 22 | setSpeakLanguages(oldArray => [...oldArray, speakLanguage]); 23 | setSpeakLanguage(""); 24 | }; 25 | 26 | // handle add learn language 27 | const addLearnLang = () => { 28 | if (!learnLanguage) return; 29 | setLearnLanguages(oldArray => [...oldArray, learnLanguage]); 30 | setLearnLanguage(""); 31 | }; 32 | 33 | // handle delete one lang 34 | const handleDelete = (langName: string, langGroup: []) => { 35 | const NewLangs = langGroup.filter(lang => lang != langName); 36 | if (langGroup == learnLanguages) { 37 | setLearnLanguages(NewLangs); 38 | } else setSpeakLanguages(NewLangs); 39 | }; 40 | 41 | // handle the update profile mutation 42 | const [updateProfile, { data, loading }] = useMutation(UPDATE_PROGILE_MUTATION); 43 | 44 | // show error or success message 45 | const handleSuccessError = () => { 46 | if (data?.updateProfileInfo.ok) { 47 | Router.push(`/users/${me.userName}`); 48 | return ( 49 | 50 | ); 51 | } 52 | if (data?.updateProfileInfo.error) { 53 | return ; 54 | } 55 | }; 56 | 57 | return ( 58 | <> 59 | {handleSuccessError()} 60 | { 67 | updateProfile({ 68 | variables: { 69 | speakLanguages: speakLanguages, 70 | learnLanguages: learnLanguages 71 | } 72 | }); 73 | setSubmitting(false); 74 | }}> 75 | {({ errors, touched, validateField }) => ( 76 |
77 | {/* Languages I speak */} 78 | 79 | 80 | Languages I speak 81 | 82 | 83 | setSpeakLanguage(e.target.value)} 86 | component={TextField} 87 | name='speakLang' 88 | label='Languages i speak' 89 | /> 90 | {errors.speakLang && touched.speakLang &&
{errors.speakLang}
} 91 |
92 |
93 | 94 | 100 | 101 |
102 | {speakLanguages.length > 0 && 103 | speakLanguages.map(lang => ( 104 | handleDelete(lang, speakLanguages)} 107 | color='primary' 108 | variant='outlined' 109 | /> 110 | ))} 111 |
112 | 113 | 114 | 115 | {/* Languages I speak */} 116 | 117 | 118 | Languages I learn 119 | 120 | 121 | setLearnLanguage(e.target.value)} 124 | component={TextField} 125 | name='learnLang' 126 | label='Languages i learn' 127 | /> 128 | 129 | 130 | 131 | 137 | 138 |
139 | {learnLanguages.length > 0 && 140 | learnLanguages.map(lang => ( 141 | handleDelete(lang, learnLanguages)} 144 | color='primary' 145 | variant='outlined' 146 | /> 147 | ))} 148 |
149 | 150 |
151 | 154 | 155 | )} 156 |
157 | 158 | ); 159 | }; 160 | 161 | export default Languages; 162 | -------------------------------------------------------------------------------- /client/pages/photos/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { initializeApollo } from "../../lib/apollo"; 3 | import { useMutation } from "@apollo/client"; 4 | import ErrorMessage from "../../components/ToastMessage"; 5 | import { ME_QUERY, ONE_USER_QUERY } from "../../graphql/queries"; 6 | import Head from "next/head"; 7 | import { Avatar, Grid, IconButton, Tooltip } from "@material-ui/core"; 8 | import { DELETE_PICTURE, CHOOSE_PROFILE_PICTURE } from "../../graphql/mutations"; 9 | import UploadProfileImages from "../../components/UploadProfileImages"; 10 | import DeleteIcon from "@material-ui/icons/Delete"; 11 | import PhotoSizeSelectActualIcon from "@material-ui/icons/PhotoSizeSelectActual"; 12 | import HeaderProfile from "../../components/shared/HeaderProfile"; 13 | 14 | function UserPhotos(props) { 15 | // extract the logged in user 16 | let { me, loading } = props.data; 17 | let { error, ok, user: myProfile } = me; 18 | 19 | // data returned from query in getServerSideProps 20 | const { user } = props.me; 21 | 22 | // handle delete pic mutation 23 | const [ 24 | deletePicture, 25 | { data, loading: deleteLoading, error: deleteError } 26 | ] = useMutation(DELETE_PICTURE); 27 | 28 | // handle select profile pic mutation 29 | const [ 30 | chooseProfilePhoto, 31 | { data: choosePhotoData, loading: chooseLoading, error: chooseError } 32 | ] = useMutation(CHOOSE_PROFILE_PICTURE); 33 | 34 | const [myPhotos, setMyPhotos] = useState(user.pictures); 35 | 36 | // handle delete photo 37 | const deletePhoto = item => { 38 | const newPhotos = myPhotos.filter(photo => photo !== item); 39 | 40 | deletePicture({ variables: { name: item } }); 41 | 42 | setMyPhotos(newPhotos); 43 | }; 44 | 45 | return ( 46 | <> 47 | {deleteError && } 48 | {error && } 49 | 50 | {/* delete pic success message */} 51 | {data?.deleteProfilePicture.ok && ( 52 | 53 | )} 54 | 55 | {/* delete pic error if there is any */} 56 | {data?.deleteProfilePicture.error && ( 57 | 58 | )} 59 | 60 | {/* choose pic success message */} 61 | {choosePhotoData?.chooseProfilePicture.ok && ( 62 | 66 | )} 67 | 68 | {/* choose pic error if there is any */} 69 | {choosePhotoData?.chooseProfilePicture.error && ( 70 | 71 | )} 72 | 73 | {user && ( 74 | <> 75 | 76 | {myProfile?.userName == user?.userName ? ( 77 | My Photos 78 | ) : ( 79 | {user.userName}'s Photos 80 | )} 81 | 82 | 83 | 84 | {myPhotos.length > 0 && ( 85 | 86 | {myPhotos.map((pic, index) => ( 87 | /* one item */ 88 | 89 | {/* avatar */} 90 | 91 | 92 | 93 | 94 | {/* delete and choose default photo buttons */} 95 | {myProfile.userName == user.userName && ( 96 | <> 97 | {/* Set as profile pic button */} 98 | 99 | { 102 | chooseProfilePhoto({ variables: { name: pic } }); 103 | }}> 104 | 105 | 106 | 107 | 108 | 109 | 110 | {/* delete photo button */} 111 | 112 | { 115 | deletePhoto(pic); 116 | }}> 117 | 118 | 119 | 120 | 121 | 122 | 123 | )} 124 | 125 | ))} 126 | 127 | )} 128 | 129 | {/* upload files section */} 130 | 131 | 132 |

Upload more Photos

133 |
134 | 135 |
136 |
137 | 138 | )} 139 | 140 | ); 141 | } 142 | 143 | // Fetch necessary data for the blog post using params.id 144 | export async function getServerSideProps({ req, res, params }) { 145 | if (!req.headers.cookie) { 146 | res.writeHead(302, { 147 | // or 301 148 | Location: "/" 149 | }); 150 | res.end(); 151 | } 152 | 153 | const apolloClient = initializeApollo(); 154 | 155 | let oneUserQuery = await apolloClient.query({ 156 | query: ME_QUERY 157 | }); 158 | 159 | let user = oneUserQuery.data.me; 160 | 161 | return { 162 | props: { 163 | initialApolloState: apolloClient.cache.extract(), 164 | me: JSON.parse(JSON.stringify(user)) 165 | } 166 | }; 167 | } 168 | 169 | export default UserPhotos; 170 | -------------------------------------------------------------------------------- /client/components/edit-profile/General.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField, Select } from "formik-material-ui"; 3 | import { Formik, Form, Field } from "formik"; 4 | import { 5 | Button, 6 | Grid, 7 | MenuItem, 8 | InputLabel, 9 | FormControl, 10 | Divider 11 | } from "@material-ui/core"; 12 | import * as Yup from "yup"; 13 | import { useMutation } from "@apollo/client"; 14 | import ErrorMessage from "../../components/ToastMessage"; 15 | import Router, { useRouter } from "next/router"; 16 | import { UPDATE_PROGILE_MUTATION } from "../../graphql/mutations"; 17 | 18 | // form validation using Yup 19 | const validate = () => 20 | Yup.object({ 21 | firstName: Yup.string().min(2, "Must be more than one character"), 22 | lastName: Yup.string().min(2, "Must be more than one character"), 23 | birthday: Yup.string().min(2, "Must be more than one character"), 24 | gender: Yup.string().min(2, "Must be more than one character"), 25 | country: Yup.string().min(2, "Must be more than one character"), 26 | city: Yup.string().min(2, "Must be more than one character"), 27 | education: Yup.string().min(2, "Must be more than one character"), 28 | job: Yup.string().min(2, "Must be more than one character"), 29 | relationship: Yup.string().min(2, "Must be more than one character") 30 | }); 31 | 32 | export default function General({ me }) { 33 | // handle the update profile mutation 34 | const [updateProfile, { data, loading }] = useMutation(UPDATE_PROGILE_MUTATION); 35 | 36 | // show error or success message 37 | const handleSuccessError = () => { 38 | if (data?.updateProfileInfo.ok) { 39 | useRouter().push(`/users/${me.userName}`); 40 | return ( 41 | 42 | ); 43 | } 44 | if (data?.updateProfileInfo.error) { 45 | return ; 46 | } 47 | }; 48 | 49 | return ( 50 | <> 51 | {handleSuccessError()} 52 | 53 | { 67 | updateProfile({ 68 | variables: { 69 | firstName: values.firstName, 70 | lastName: values.lastName, 71 | birthday: values.birthday, 72 | gender: values.gender, 73 | country: values.country, 74 | city: values.city, 75 | education: values.education, 76 | job: values.job, 77 | relationship: values.relationship 78 | } 79 | }); 80 | setSubmitting(false); 81 | }}> 82 |
83 | {/* first name */} 84 | 85 | 86 | First Name 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {/* last name */} 96 | 97 | 98 | Last Name 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | {/* Birthday */} 108 | 109 | 110 | Birthday 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {/* Gender */} 120 | 121 | 122 | Gender 123 | 124 | 125 | 126 | Select 127 | 128 | Male 129 | Female 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | {/* Country */} 138 | 139 | 140 | Country 141 | 142 | 143 | 144 | Egypt 145 | USA 146 | Germany 147 | 148 | 149 | 150 | 151 | {/* City */} 152 | 153 | 154 | City 155 | 156 | 157 | 158 | come City 159 | come City1 160 | come City2 161 | 162 | 163 | 164 | 165 | 166 | 167 | {/* Education */} 168 | 169 | 170 | Education 171 | 172 | 173 | 177 | Not Specified 178 | High School 179 | Some College 180 | Bachelor's Degree 181 | Graduate Degree 182 | 183 | 184 | 185 | 186 | 187 | 188 | {/* Occupation or Job */} 189 | 190 | 191 | Occupation or Job 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | {/* Relationship status */} 201 | 202 | 203 | Relationship status 204 | 205 | 206 | 207 | Not Specified 208 | Single 209 | In a relationship 210 | Married 211 | It's Complicated 212 | 213 | 214 | 215 | 216 | 217 | 218 | {/* Submit buttom */} 219 | 222 | 223 |
224 | 225 | ); 226 | } 227 | -------------------------------------------------------------------------------- /client/pages/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { makeStyles, Theme } from "@material-ui/core/styles"; 3 | import Tabs from "@material-ui/core/Tabs"; 4 | import Tab from "@material-ui/core/Tab"; 5 | import Typography from "@material-ui/core/Typography"; 6 | import Box from "@material-ui/core/Box"; 7 | import { useMutation, useQuery, useSubscription } from "@apollo/client"; 8 | import { ALL_USER_CHATS_QUERY } from "../../graphql/queries"; 9 | import { 10 | Avatar, 11 | Button, 12 | Grid, 13 | Icon, 14 | List, 15 | ListItem, 16 | ListItemAvatar, 17 | ListItemText, 18 | Tooltip 19 | } from "@material-ui/core"; 20 | import { SEND_MESSAGE_MUTATION } from "../../graphql/mutations"; 21 | import Link from "next/link"; 22 | import { initializeApollo } from "../../lib/apollo"; 23 | import { SEND_MESSAGE_SUB } from "../../graphql/subscription"; 24 | import moment from "moment"; 25 | import Head from "next/head"; 26 | 27 | interface TabPanelProps { 28 | children?: React.ReactNode; 29 | index: any; 30 | value: any; 31 | } 32 | 33 | /* tab panel component used in material ui */ 34 | function TabPanel(props: TabPanelProps) { 35 | const { children, value, index, ...other } = props; 36 | 37 | return ( 38 | 50 | ); 51 | } 52 | 53 | /* used within material ui tab panel */ 54 | function a11yProps(index: any) { 55 | return { 56 | id: `vertical-tab-${index}`, 57 | "aria-controls": `vertical-tabpanel-${index}` 58 | }; 59 | } 60 | 61 | /* used within material ui tab panel */ 62 | const useStyles = makeStyles((theme: Theme) => ({ 63 | root: { 64 | flexGrow: 1, 65 | backgroundColor: theme.palette.background.paper, 66 | display: "flex", 67 | "min-height": 550 68 | }, 69 | tabs: { 70 | borderRight: `1px solid ${theme.palette.divider}` 71 | } 72 | })); 73 | 74 | /* all tab panesl chat (the whole chat page component) */ 75 | export default function index(props: any) { 76 | // the current logged in user 77 | let me = props.data.me.user; 78 | 79 | // get all the cuser chats from `getServerSideProps()` to render 80 | //if the user opens chat page 81 | let userChats = props.userChats.chats; 82 | 83 | // handle the current chat messages 84 | const [message, setMessage] = useState(""); 85 | 86 | const classes = useStyles(); 87 | const [value, setValue] = useState(0); 88 | 89 | // handle material ui tab panel change 90 | const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { 91 | setValue(newValue); 92 | }; 93 | 94 | // handle send message mutation 95 | // once it's hit, it calls the new message subscription on the backend 96 | const [ 97 | send_message, 98 | { data: sendMessageData, loading: sendMessageLoading } 99 | ] = useMutation(SEND_MESSAGE_MUTATION); 100 | 101 | // handle send message subscription 102 | // it returns the current active chat 103 | const { data, loading, error } = useSubscription(SEND_MESSAGE_SUB); 104 | // the chat returned from the subscription 105 | let chatSubscribe = data?.userChats.chat; 106 | 107 | /* once the user hits send button the subscription return the current chat 108 | but the chat list keeps at the same position, but we need to scroll down 109 | at the very last message, so we need a functionality to scroll down to the 110 | bottom of the chat list */ 111 | const AlwaysScrollToBottom: any = () => { 112 | useEffect(() => { 113 | var list: any = document.getElementById("chat-list"); 114 | if (list) list.scrollTop = list.scrollHeight; 115 | }); 116 | }; 117 | AlwaysScrollToBottom(); 118 | 119 | /* once the user hits send button, the subscription returns the current active chat 120 | so only need to re render only the new chat with the new message, not all the chats 121 | */ 122 | // it takes chat parameter to decide which chat to render 123 | let msgs = chat => { 124 | if (chat?.id === chatSubscribe?.id && chatSubscribe) 125 | return chatSubscribe.messages.map((message, index) => ( 126 | /* single chat message */ 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | {message.user.userName === me.userName && ( 138 | 139 | )} 140 | 141 | {message.user.userName !== me.userName && ( 142 | 143 | )} 144 | 145 | )); 146 | else { 147 | return chat.messages.map((message, index) => ( 148 | /* single chat message */ 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | {message.user.userName === me.userName && ( 161 | 162 | )} 163 | 164 | {message.user.userName !== me.userName && ( 165 | 166 | )} 167 | 168 | )); 169 | } 170 | }; 171 | 172 | // retuens the whole tab panel with all the chats 173 | return ( 174 | <> 175 | 176 | Chat 177 | 178 | 179 |
180 | {/* all chats container */} 181 | 182 | {/* if there are no converstions show message */} 183 | {userChats.length == 0 && ( 184 |
185 | There are no conversations,{" "} 186 | { 187 | 188 | make new friends 189 | 190 | }{" "} 191 |
192 | )} 193 | 194 | {/* all user chat tabs */} 195 | {userChats.length > 0 && ( 196 | 197 | 204 | {userChats && 205 | userChats.map((chat, index) => ( 206 | 212 | ))} 213 | 214 | 215 | )} 216 | 217 | {/* the chosen tab panel from the above tab */} 218 | 219 | {userChats && 220 | userChats.map((chat, index) => ( 221 | 222 | 223 | {msgs(chat)} 224 | 225 | 226 | 227 | 228 |