├── .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 |
22 |
23 | LogIn
24 |
25 |
26 |
27 |
28 |
29 | SignUp
30 |
31 |
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 | {
39 | deleteUserMutation({ variables: { id: user.id } });
40 | }}>
41 | Delete
42 |
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 | {
46 | handleAcceptFriends(requestt.id);
47 | }}>
48 | Confirm
49 |
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 |
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 |
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 |
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 |
39 | {value === index && (
40 |
41 | {children}
42 |
43 | )}
44 |
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 |
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 |
32 | {value === index && (
33 |
34 | {children}
35 |
36 | )}
37 |
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 |
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 |
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 |
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 |
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 |
44 | {value === index && (
45 |
46 | {children}
47 |
48 | )}
49 |
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 |
241 |
242 |
243 | {
247 | send_message({
248 | variables: { text: message, user: me?.id, chat: chat.id }
249 | });
250 | setMessage("");
251 | }}>
252 |
253 |
254 |
255 |
256 |
257 | ))}
258 |
259 |
260 |
261 | >
262 | );
263 | }
264 |
265 | // Fetch necessary data for the blog post using params.id
266 | export async function getServerSideProps(ctx) {
267 | //redirect if there is no authentcated user
268 | if (!ctx.req.headers.cookie) {
269 | ctx.res.writeHead(302, {
270 | // or 301
271 | Location: "/"
272 | });
273 | ctx.res.end();
274 | }
275 |
276 | const apolloClient = initializeApollo();
277 |
278 | let oneUserQuery = await apolloClient.query({
279 | query: ALL_USER_CHATS_QUERY
280 | });
281 |
282 | let userChats = oneUserQuery.data.userChats;
283 |
284 | return {
285 | props: {
286 | initialApolloState: apolloClient.cache.extract(),
287 | userChats: userChats
288 | }
289 | };
290 | }
291 |
--------------------------------------------------------------------------------
/client/components/edit-profile/AboutMe.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useReducer } from "react";
2 | import { Divider, Chip } from "@material-ui/core";
3 | import { Formik, Form, Field, ErrorMessage as FormikError } from "formik";
4 | import { Grid, Button } from "@material-ui/core";
5 | import * as Yup from "yup";
6 | import { TextField } from "formik-material-ui";
7 | import { useMutation } from "@apollo/client";
8 | import { UPDATE_PROGILE_MUTATION } from "../../graphql/mutations";
9 | import ErrorMessage from "../ToastMessage";
10 | import Router from "next/router";
11 |
12 | // form validation using Yup
13 | const validate = () =>
14 | Yup.object({
15 | aboutMe: Yup.string().notRequired().min(2, "Must be more than one character")
16 | });
17 |
18 | const AboutMe = ({ me }) => {
19 | // get the languaes state
20 | const [hobbies, setHobbies] = useState(me.hobbies);
21 | const [music, setMusic] = useState(me.music);
22 | const [books, setBooks] = useState(me.books);
23 | const [movies, setMovies] = useState(me.movies);
24 | const [tvShows, setTvShows] = useState(me.tvShows);
25 |
26 | // handle change the fields
27 | const [allValues, setAllValues] = useReducer(
28 | (state, newState) => ({ ...state, ...newState }),
29 | {
30 | hobby: "",
31 | music: "",
32 | book: "",
33 | movie: "",
34 | tvShow: ""
35 | }
36 | );
37 |
38 | // handle each item change to be saved in its array
39 | const changeHandler = e => {
40 | setAllValues({ ...allValues, [e.target.name]: e.target.value });
41 | };
42 |
43 | // handle add new item
44 | const addNewItem = category => {
45 | switch (category) {
46 | case "hobbies":
47 | if (!allValues.hobby) return;
48 | setHobbies(oldArray => [...oldArray, allValues.hobby]);
49 | setAllValues({ hobby: "" });
50 | break;
51 | case "music":
52 | if (!allValues.music) return;
53 | setMusic(oldArray => [...oldArray, allValues.music]);
54 | setAllValues({ music: "" });
55 | break;
56 | case "books":
57 | if (!allValues.book) return;
58 | setBooks(oldArray => [...oldArray, allValues.book]);
59 | setAllValues({ book: "" });
60 | break;
61 | case "movies":
62 | if (!allValues.movie) return;
63 | setMovies(oldArray => [...oldArray, allValues.movie]);
64 | setAllValues({ movie: "" });
65 | break;
66 | case "tvShows":
67 | if (!allValues.tvShow) return;
68 | setTvShows(oldArray => [...oldArray, allValues.tvShow]);
69 | setAllValues({ tvShow: "" });
70 | break;
71 | default:
72 | break;
73 | }
74 | };
75 |
76 | // handle delete one item
77 | const handleDelete = (category: string, toDelte: string) => {
78 | switch (category) {
79 | case "hobbies":
80 | let newItems: [];
81 | newItems = hobbies.filter((item: string) => item != toDelte);
82 | setHobbies(newItems);
83 | break;
84 | case "music":
85 | newItems = music.filter((item: string) => item != toDelte);
86 | setMusic(newItems);
87 | break;
88 | case "books":
89 | newItems = books.filter((item: string) => item != toDelte);
90 | setBooks(newItems);
91 | break;
92 | case "movies":
93 | newItems = movies.filter((item: string) => item != toDelte);
94 | setMovies(newItems);
95 | break;
96 | case "tvShows":
97 | newItems = tvShows.filter((item: string) => item != toDelte);
98 | setTvShows(newItems);
99 | break;
100 | default:
101 | break;
102 | }
103 | };
104 |
105 | // handle the update profile mutation
106 | const [updateProfile, { data, loading }] = useMutation(UPDATE_PROGILE_MUTATION);
107 |
108 | // show error or success message
109 | const handleSuccessError = () => {
110 | if (data?.updateProfileInfo.ok) {
111 | Router.push(`/users/${me.userName}`);
112 | return (
113 |
114 | );
115 | }
116 | if (data?.updateProfileInfo.error) {
117 | return ;
118 | }
119 | };
120 |
121 | return (
122 | <>
123 | {handleSuccessError()}
124 | {
135 | updateProfile({
136 | variables: {
137 | aboutMe: values.aboutMe,
138 | hobbies: hobbies,
139 | music: music,
140 | books: books,
141 | movies: movies,
142 | tvShows: tvShows
143 | }
144 | });
145 | setSubmitting(false);
146 | }}>
147 |
344 |
345 | >
346 | );
347 | };
348 |
349 | export default AboutMe;
350 |
--------------------------------------------------------------------------------
/server/schema/resolvers/user.ts:
--------------------------------------------------------------------------------
1 | import User from "./../../models/User";
2 | import {
3 | signupvalidatation,
4 | updateProfileValidation
5 | } from "./../../middlewares/validation/userValidation";
6 | import { loginValidatation } from "./../../middlewares/validation/userValidation";
7 | import * as jwt from "jsonwebtoken";
8 | import { userAuth, adminAuth } from "../../middlewares/auth";
9 | import { GraphQLError } from "graphql";
10 |
11 | // handle image upload
12 | let resultUrl = "";
13 | const cloudinaryUpload = async ({ stream }) => {
14 | const cloudinary = require("cloudinary");
15 | cloudinary.config({
16 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
17 | api_key: process.env.CLOUDINARY_API_KEY,
18 | api_secret: process.env.CLOUDINARY_API_SECRET
19 | });
20 |
21 | try {
22 | await new Promise((resolve, reject) => {
23 | const streamLoad = cloudinary.v2.uploader.upload_stream(function (error, result) {
24 | if (result) {
25 | resultUrl = result.secure_url;
26 | resolve(resultUrl);
27 | } else {
28 | reject(error);
29 | }
30 | });
31 |
32 | stream.pipe(streamLoad);
33 | });
34 | } catch (err) {
35 | throw new Error(`Failed to upload profile picture ! Err:${err.message}`);
36 | }
37 | };
38 |
39 | const processUpload = async (upload: any) => {
40 | const { createReadStream } = await upload;
41 | const stream = createReadStream();
42 |
43 | await cloudinaryUpload({ stream });
44 | return resultUrl;
45 | };
46 |
47 | const userResolver = {
48 | Query: {
49 | // current user query
50 | me: async (_: any, __: any, { req }) => {
51 | try {
52 | await userAuth(req);
53 |
54 | let myId = req.user.userId;
55 |
56 | let me = User.findById(myId)
57 | .populate({
58 | path: "friendsPending",
59 | model: "NewUser"
60 | })
61 | .populate({
62 | path: "bookmarks",
63 | model: "NewUser"
64 | })
65 | .exec();
66 |
67 | return {
68 | ok: true,
69 | user: me
70 | };
71 | } catch (error) {
72 | return {
73 | ok: false,
74 | error: error.message
75 | };
76 | }
77 | },
78 |
79 | // all users query
80 | users: async (_: any, __: any, { req }) => {
81 | try {
82 | await userAuth(req);
83 |
84 | let allUsers = await User.find()
85 | .populate({
86 | path: "friendsPending",
87 | model: "NewUser"
88 | })
89 | .populate({
90 | path: "friends",
91 | model: "NewUser"
92 | })
93 | .exec();
94 |
95 | return {
96 | ok: true,
97 | users: allUsers
98 | };
99 | } catch (error) {
100 | return {
101 | ok: false,
102 | error: error.message
103 | };
104 | }
105 | },
106 |
107 | // one user query
108 | userInfo: async (_: any, { userName }, { req }) => {
109 | try {
110 | //await userAuth(req);
111 |
112 | let user = await User.findOne({ userName: userName })
113 | .populate({
114 | path: "friendsPending",
115 | model: "NewUser"
116 | })
117 | .populate({
118 | path: "friends",
119 | model: "NewUser"
120 | })
121 | .exec();
122 |
123 | return {
124 | user: user,
125 | ok: true
126 | };
127 | } catch (error) {
128 | return {
129 | ok: false,
130 | error: error.message
131 | };
132 | }
133 | },
134 |
135 | // fetch friends requests
136 | friendRequests: async (_: any, args: any, { req }) => {
137 | try {
138 | await userAuth(req);
139 |
140 | const myId = req.user.userId;
141 |
142 | const user = await User.findOne({ userName: args.userName })
143 | .populate({
144 | path: "friendsPending",
145 | model: "NewUser"
146 | })
147 | .populate({
148 | path: "friends",
149 | model: "NewUser"
150 | })
151 | .exec();
152 |
153 | return {
154 | friends: user.friendsPending,
155 | ok: true
156 | };
157 | } catch (error) {
158 | return {
159 | ok: false,
160 | error: error.message
161 | };
162 | }
163 | }
164 | },
165 |
166 | /* User: {
167 | contactInfo: async (parent: any) => {
168 | let me = await User.findById(parent.id).exec();
169 | console.log(me);
170 |
171 | return me.contactInfo;
172 | }
173 | }, */
174 |
175 | Mutation: {
176 | //signup mutation
177 | signUp: async (_: any, args: any, context: any) => {
178 | try {
179 | // 1- validate input data
180 | await signupvalidatation.validate(args);
181 |
182 | // 2- create new user and save it in the DB
183 | const newUser = new User({
184 | userName: args.userName,
185 | email: args.email,
186 | password: args.password,
187 | verifyPassword: args.verifyPassword,
188 | firstName: args.firstName,
189 | lastName: args.lastName,
190 | role: "user",
191 | gender: args.gender,
192 | country: args.country
193 | });
194 |
195 | const user = newUser.save();
196 |
197 | // 3- sign a new token with the required data
198 | const token = jwt.sign(
199 | { userId: newUser.id, role: newUser.role },
200 | process.env.JWT_SECRET,
201 | {
202 | expiresIn: "1y"
203 | }
204 | );
205 |
206 | // 4- set a cookies with the token value and it's httpOnly
207 | context.res.cookie("token", token, {
208 | expires: new Date(Date.now() + 900000),
209 | httpOnly: true,
210 | secure: true,
211 | domain: process.env.DOMAIN_URI, // your domain name eg "xoxo.com", "localhost"
212 | path: "/"
213 | });
214 |
215 | return { ok: true, user, successMessage: "Registered Successfully" };
216 | } catch (error) {
217 | return {
218 | ok: false,
219 | error: error.message
220 | };
221 | }
222 | },
223 | //******************************************
224 | //*********** login mutation ***************
225 | //******************************************
226 | login: async (_: any, args: any, context: any) => {
227 | try {
228 | //1- validate input data
229 | await loginValidatation.validate(args);
230 |
231 | // 2- find user
232 | const userName = args.userName;
233 | const user = await User.findOne({ userName: userName });
234 |
235 | if (!user) {
236 | return {
237 | ok: false,
238 | error: "no such user"
239 | };
240 | }
241 |
242 | // 3- sign a new token
243 | const token = jwt.sign(
244 | { userId: user.id, role: user.role },
245 | process.env.JWT_SECRET,
246 | {
247 | expiresIn: "1y"
248 | }
249 | );
250 |
251 | // 4- set a cookies with the token value and it's httpOnly
252 | context.res.cookie("token", token, {
253 | expires: new Date(Date.now() + 18000000),
254 | httpOnly: true,
255 | secure: true,
256 | domain: process.env.DOMAIN_URI, // your domain name eg "xoxo.com", "localhost"
257 | path: "/"
258 | });
259 |
260 | return { ok: true, user, successMessage: "Logged in Successfully" };
261 | } catch (error) {
262 | return {
263 | ok: false,
264 | error: error.message
265 | };
266 | }
267 | },
268 | //logout Mutation
269 | logout: async (_: any, __: any, { res }) => {
270 | res.clearCookie("token", {
271 | domain: process.env.DOMAIN_URI, // your domain name eg "xoxo.com", "localhost"
272 | path: "/"
273 | });
274 |
275 | return {
276 | ok: true
277 | };
278 | },
279 | // Delete user mutation
280 | deleteUser: async (_: any, args: any, { req }) => {
281 | try {
282 | // 1- authenticate user
283 | await adminAuth(req);
284 |
285 | // 2- find user
286 | const id = args.id;
287 | await User.findByIdAndDelete(id);
288 |
289 | return {
290 | ok: true,
291 | successMessage: "Deleted Successfully"
292 | };
293 | } catch (error) {
294 | return {
295 | ok: false,
296 | error: error.message
297 | };
298 | }
299 | },
300 | // Delete user mutation
301 | uploadProfilePictures: async (_: any, { file }: any, { req }) => {
302 | try {
303 | // 1- Authenticate user
304 | await userAuth(req);
305 |
306 | let myId = req.user.userId;
307 |
308 | // 2- Get the uploaded pictures url, and update the user's pictures in the DB
309 | await Promise.all(file.map(processUpload)).then(res => {
310 | const newPics: any = res;
311 |
312 | User.findByIdAndUpdate(
313 | { _id: myId },
314 | { $push: { pictures: { $each: newPics } } },
315 | { useFindAndModify: false, upsert: true }
316 | ).exec();
317 | });
318 |
319 | return {
320 | ok: true,
321 | successMessage: "Pictures uploaded to your account"
322 | };
323 | } catch (error) {
324 | return {
325 | ok: false,
326 | error: error.message
327 | };
328 | }
329 | },
330 | // delete profile picture
331 | deleteProfilePicture: async (_: any, { name }, { req }) => {
332 | try {
333 | // 1- authenticate user
334 | await userAuth(req);
335 |
336 | let myId = req.user.userId;
337 |
338 | // 2- find pic and delete
339 | User.findByIdAndUpdate(
340 | { _id: myId },
341 | { $pull: { pictures: name } },
342 | { useFindAndModify: false, upsert: true }
343 | ).exec();
344 |
345 | return {
346 | ok: true,
347 | successMessage: "Picture was deleted"
348 | };
349 | } catch (e) {
350 | return {
351 | ok: false,
352 | error: e.message
353 | };
354 | }
355 | },
356 |
357 | // choose profile picture
358 | chooseProfilePicture: async (_: any, { name }, { req }) => {
359 | try {
360 | // 1- authenticate user
361 | await userAuth(req);
362 |
363 | let myId = req.user.userId;
364 |
365 | // 2- find pic and delete
366 | User.findByIdAndUpdate(
367 | { _id: myId },
368 | { $set: { avatarUrl: name } },
369 | { useFindAndModify: false, upsert: true }
370 | ).exec();
371 |
372 | return {
373 | ok: true,
374 | successMessage: "Profile photo was updated"
375 | };
376 | } catch (e) {
377 | return {
378 | ok: false,
379 | error: e.message
380 | };
381 | }
382 | },
383 | // update User info
384 | updateProfileInfo: async (_: any, args: any, { req }) => {
385 | try {
386 | // 1- authenticate user
387 | await userAuth(req);
388 |
389 | // 2- validate inputs
390 | await updateProfileValidation.validate(args);
391 |
392 | let myId = req.user.userId;
393 |
394 | // 3- find the user and update the given fields
395 | User.findByIdAndUpdate(
396 | { _id: myId },
397 | { $set: args },
398 | { useFindAndModify: false, upsert: true }
399 | ).exec();
400 |
401 | return {
402 | ok: true,
403 | successMessage: "Profile updated Successfully"
404 | };
405 | } catch (error) {
406 | return {
407 | ok: false,
408 | error: error.message
409 | };
410 | }
411 | },
412 | // add friend
413 | addFriend: async (_: any, { id }, { req }) => {
414 | try {
415 | // 1- authenticate user
416 | await userAuth(req);
417 |
418 | let myId = req.user.userId;
419 |
420 | //2- check if the 2 accounts are already friends
421 | const user = await User.findOne({ _id: myId }).exec();
422 | if (user?.friends.includes(id)) {
423 | throw new GraphQLError("You're already friends");
424 | }
425 |
426 | // if they aren't friends
427 | // 3- find the friend you wanna add and update its pending friends array
428 | await User.findByIdAndUpdate(
429 | { _id: id },
430 | { $push: { friendsPending: myId } },
431 | { useFindAndModify: false }
432 | ).exec();
433 |
434 | return {
435 | ok: true,
436 | successMessage: "Friends Request sent"
437 | };
438 | } catch (error) {
439 | return {
440 | ok: false,
441 | error: error.message
442 | };
443 | }
444 | },
445 | // accept friend request
446 | acceptFriend: async (_: any, { id }, { req }) => {
447 | try {
448 | // 1- authenticate user
449 | await userAuth(req);
450 |
451 | let myId = req.user.userId;
452 |
453 | // 2- find the friend you wanna add and update its pending friends array
454 | await User.findOneAndUpdate(
455 | { _id: myId },
456 | { $push: { friends: id }, $pull: { friendsPending: id } }
457 | ).exec();
458 |
459 | // 3- update my friends list
460 | await User.findOneAndUpdate({ _id: id }, { $push: { friends: myId } }).exec();
461 |
462 | return {
463 | ok: true,
464 | successMessage: "You're now friends"
465 | };
466 | } catch (e) {
467 | return {
468 | ok: false,
469 | error: e.message
470 | };
471 | }
472 | },
473 | // delete friend
474 | deleteFriend: async (_: any, { id }, { req }) => {
475 | try {
476 | // 1- authenticate user
477 | await userAuth(req);
478 |
479 | let myId = req.user.userId;
480 |
481 | // 2- update my friends list
482 | await User.findOneAndUpdate({ _id: myId }, { $pull: { friends: id } }).exec();
483 |
484 | // 3- update the other friend friends list
485 | await User.findOneAndUpdate({ _id: id }, { $pull: { friends: myId } }).exec();
486 |
487 | return {
488 | ok: true,
489 | successMessage: "You're not friends anymore"
490 | };
491 | } catch (e) {
492 | return {
493 | ok: false,
494 | error: e.message
495 | };
496 | }
497 | },
498 | // add bookmark
499 | addBookmark: async (_: any, { id }, { req }) => {
500 | try {
501 | // 1- authenticate user
502 | await userAuth(req);
503 |
504 | let myId = req.user.userId;
505 |
506 | // 2- check if the user already in bookmarks
507 | const user = await User.findOne({ _id: myId }).exec();
508 | if (user?.bookmarks.includes(id)) {
509 | throw new GraphQLError("Already in your bookmarks");
510 | }
511 |
512 | // 3- find the friend you wanna add and update its pending friends array
513 | await User.findByIdAndUpdate(
514 | { _id: myId },
515 | { $push: { bookmarks: id } },
516 | { useFindAndModify: false }
517 | ).exec();
518 |
519 | return {
520 | ok: true,
521 | successMessage: "Added to bookmarks",
522 | error: null
523 | };
524 | } catch (error) {
525 | return {
526 | ok: false,
527 | error: error.message,
528 | successMessage: null
529 | };
530 | }
531 | },
532 | // delete bookmark
533 | deleteBookmark: async (_: any, { id }, { req }) => {
534 | try {
535 | // 1- authenticate user
536 | await userAuth(req);
537 |
538 | let myId = req.user.userId;
539 |
540 | // 3- find the friend you wanna add and update its pending friends array
541 | await User.findByIdAndUpdate(
542 | { _id: myId },
543 | { $pull: { bookmarks: id } },
544 | { useFindAndModify: false }
545 | ).exec();
546 |
547 | return {
548 | ok: true,
549 | successMessage: "Deleted from bookmarks",
550 | error: null
551 | };
552 | } catch (error) {
553 | return {
554 | ok: false,
555 | error: error.message,
556 | successMessage: null
557 | };
558 | }
559 | }
560 | }
561 | };
562 |
563 | export default userResolver;
564 |
--------------------------------------------------------------------------------
/client/pages/users/[userName].tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { initializeApollo } from "../../lib/apollo";
3 | import { ONE_USER_QUERY } from "../../graphql/queries";
4 | import Head from "next/head";
5 | import {
6 | Grid,
7 | Avatar,
8 | Chip,
9 | List,
10 | ListItem,
11 | ListItemText,
12 | ListItemIcon,
13 | Button
14 | } from "@material-ui/core";
15 | import PhotosSlider from "../../components/user-profile/PhotosSlider";
16 | import { useMutation } from "@apollo/client";
17 | import {
18 | ADD_BOOKMARK_MUTATION,
19 | ADD_FRIEND_MUTATION,
20 | CREATE_NEW_CHAT_MUTATION
21 | } from "../../graphql/mutations";
22 | import ErrorMessage from "../../components/ToastMessage";
23 | import Typed from "react-typed";
24 |
25 | /**
26 | * This page will render the user's profile
27 | * here we will render depeds on the data coming from props
28 | * if the data belongs to the logged in user will render my profile
29 | */
30 | function User(props) {
31 | // extract the logged in user
32 | let { me, loading } = props.data;
33 | let { error, ok, user: myProfile } = me;
34 |
35 | // this is destructing
36 | const {
37 | user: userQuery,
38 | user: { user }
39 | } = props;
40 |
41 | const [handleOpen, setHandleOpen] = useState({ open: false });
42 | const handleClick = () => {
43 | setHandleOpen({ open: true });
44 | };
45 |
46 | // handle add friend mutation
47 | const [add_friend, { data }] = useMutation(ADD_FRIEND_MUTATION);
48 |
49 | // handle add friend mutation
50 | const [add_bookmark, { data: bookmarkData }] = useMutation(ADD_BOOKMARK_MUTATION);
51 |
52 | // handle create new chat mutation
53 | const [create_new_chat, { data: newChatData }] = useMutation(CREATE_NEW_CHAT_MUTATION);
54 |
55 | return (
56 | <>
57 | {userQuery.error && }
58 |
59 | {error && }
60 |
61 | {data?.addFriend.ok && (
62 |
63 | )}
64 |
65 | {data?.addFriend.error && (
66 |
67 | )}
68 |
69 | {bookmarkData?.addBookmark.ok && (
70 |
71 | )}
72 |
73 | {bookmarkData?.addBookmark.error && (
74 |
75 | )}
76 |
77 | {user && (
78 | <>
79 |
80 | {myProfile?.userName == user?.userName ? (
81 | My Profile
82 | ) : (
83 | {user.userName}'s Profile
84 | )}
85 |
86 |
87 |
88 |
89 | {/*********** Welcome message ***************/}
90 | {myProfile?.userName == user?.userName && (
91 |
92 |
101 |
102 | )}
103 |
104 | {/************************ Profile details ********************/}
105 |
106 | {/* Profile images */}
107 |
108 | {user.pictures.length < 1 ? (
109 |
114 | ) : (
115 | <>
116 | {/* profile main pic */}
117 |
123 | {/* profile rest of the pics */}
124 |
129 | {user.pictures.map((pic, index) => {
130 | if (pic !== user.avatarUrl)
131 | return (
132 |
133 |
139 |
140 | );
141 | })}
142 |
143 | >
144 | )}
145 |
146 |
147 | {/* Photos slider component */}
148 |
153 |
154 | {/* profile details */}
155 |
156 |
157 | Country :
158 | {user.country}
159 |
160 |
161 |
162 | Status :
163 | {user.status}
164 |
165 |
166 | {user.speakLanguages.length > 0 && (
167 |
168 | Speaks :
169 | {user.speakLanguages.map(lang => (
170 | {lang}
171 | ))}
172 |
173 | )}
174 |
175 | {user.speakLanguages.length > 0 && (
176 |
177 | Learning :
178 | {user.learnLanguages.map(lang => (
179 | {lang}
180 | ))}
181 |
182 | )}
183 |
184 | {user.education && (
185 |
186 | Education :
187 | {user.education}
188 |
189 | )}
190 |
191 | {user.relationship && (
192 |
193 | Relationship status :
194 | {user.relationship}
195 |
196 | )}
197 |
198 |
199 |
200 | {/* Message - Bookmark - Add Friend - Comments - Block - Report */}
201 | {myProfile?.userName != user?.userName && (
202 |
208 | {/* send message */}
209 | {
212 | create_new_chat({
213 | variables: { users: [user.id, myProfile.id] }
214 | });
215 | }}>
216 | Message
217 |
218 |
219 |
220 | {/* Add friend */}
221 | {
224 | add_friend({ variables: { id: user.id } });
225 | }}>
226 | Add Friend
227 |
228 |
229 |
230 | {/* add bookmark */}
231 | {
234 | add_bookmark({ variables: { id: user.id } });
235 | }}>
236 | Bookmark
237 |
238 |
239 |
240 | )}
241 |
242 | {/************************ About me ********************/}
243 | {user.aboutMe && (
244 |
245 | About me
246 | {user.aboutMe}
247 |
248 | )}
249 |
250 | {/************************ hobbies ********************/}
251 | {user.hobbies.length > 0 && (
252 |
253 | Hobbies & Interests
254 |
259 | {user.hobbies.map(hobby => (
260 |
261 |
262 |
263 | ))}
264 |
265 |
266 | )}
267 |
268 | {/************************ music ********************/}
269 | {user.music.length > 0 && (
270 |
271 | Favorite Music
272 |
277 | {user.music.map(music => (
278 |
279 |
280 |
281 | ))}
282 |
283 |
284 | )}
285 |
286 | {/************************ movies ********************/}
287 | {user.movies.length > 0 && (
288 |
289 | Favorite Movies
290 |
295 | {user.movies.map(movie => (
296 |
297 |
298 |
299 | ))}
300 |
301 |
302 | )}
303 |
304 | {/************************ tv shows ********************/}
305 | {user.tvShows.length > 0 && (
306 |
307 | Favorite TV Shows
308 |
313 | {user.tvShows.map(tvShow => (
314 |
315 |
321 |
322 | ))}
323 |
324 |
325 | )}
326 |
327 | {/************************ Books ********************/}
328 | {user.books.length > 0 && (
329 |
330 | Favorite Books
331 |
336 | {user.books.map(book => (
337 |
338 |
339 |
340 | ))}
341 |
342 |
343 | )}
344 |
345 | {/************************ contact me ********************/}
346 | {(user.contactInfo.skype ||
347 | user.contactInfo.facebook ||
348 | user.contactInfo.instagram ||
349 | user.contactInfo.snapchat ||
350 | (user.contactInfo.website && user.contactInfo)) && (
351 |
352 | Contact me
353 |
354 | {user.contactInfo.skype && (
355 |
356 |
357 |
358 |
359 |
360 |
361 | )}
362 |
363 | {user.contactInfo.facebook && (
364 |
365 |
366 |
367 |
368 |
369 |
370 | )}
371 |
372 | {user.contactInfo.instagram && (
373 |
374 |
375 |
376 |
377 |
378 |
379 | )}
380 |
381 | {user.contactInfo.snapchat && (
382 |
383 |
384 |
385 |
386 |
387 |
388 | )}
389 |
390 | {user.contactInfo.website && (
391 |
392 |
393 |
394 |
395 |
396 |
397 | )}
398 |
399 |
400 | )}
401 |
402 |
403 |
404 | Right Side
405 |
406 |
407 | >
408 | )}
409 | >
410 | );
411 | }
412 |
413 | // Fetch necessary data for the blog post using params.id
414 | export async function getServerSideProps(ctx) {
415 | // redirect to home page if there is no user
416 | if (!ctx.req.headers.cookie) {
417 | ctx.res.writeHead(302, {
418 | // or 301
419 | Location: "/"
420 | });
421 | ctx.res.end();
422 | }
423 |
424 | const apolloClient = initializeApollo();
425 |
426 | let oneUserQuery = await apolloClient.query({
427 | query: ONE_USER_QUERY,
428 | variables: { userName: ctx.params.userName }
429 | });
430 |
431 | let user = oneUserQuery.data.userInfo;
432 |
433 | return {
434 | props: {
435 | initialApolloState: apolloClient.cache.extract(),
436 | user: JSON.parse(JSON.stringify(user))
437 | }
438 | };
439 | }
440 |
441 | export default User;
442 |
--------------------------------------------------------------------------------