├── frontend
├── src
│ ├── App.css
│ ├── Components
│ │ ├── index.tsx
│ │ └── Layout
│ │ │ ├── Footer
│ │ │ ├── index.tsx
│ │ │ └── Footer.tsx
│ │ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ └── Navbar
│ │ │ │ ├── index.tsx
│ │ │ │ └── Navbar.tsx
│ │ │ ├── index.tsx
│ │ │ └── Layout.tsx
│ ├── tailwind.css
│ ├── Pages
│ │ ├── LoginPage
│ │ │ ├── Components
│ │ │ │ ├── index.tsx
│ │ │ │ └── LoginForm.tsx
│ │ │ ├── index.tsx
│ │ │ └── LoginPage.tsx
│ │ ├── RegisterPage
│ │ │ ├── Components
│ │ │ │ ├── index.tsx
│ │ │ │ └── RegisterForm.tsx
│ │ │ ├── index.tsx
│ │ │ └── RegisterPage.tsx
│ │ ├── FeedPage
│ │ │ ├── index.tsx
│ │ │ ├── Components
│ │ │ │ ├── index.tsx
│ │ │ │ ├── Posts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── PostCreation.tsx
│ │ │ │ │ ├── Posts.tsx
│ │ │ │ │ └── Post.tsx
│ │ │ │ ├── Replies
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── ReplyCreation.tsx
│ │ │ │ │ ├── Replies.tsx
│ │ │ │ │ └── Reply.tsx
│ │ │ │ └── Comments
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── CommentCreation.tsx
│ │ │ │ │ ├── Comments.tsx
│ │ │ │ │ └── Comment.tsx
│ │ │ └── FeedPage.tsx
│ │ └── index.tsx
│ ├── config.tsx
│ ├── setupTests.ts
│ ├── App.test.tsx
│ ├── relay
│ │ ├── environment.tsx
│ │ └── fetchGraphQL.tsx
│ ├── Services
│ │ └── Subscriptions
│ │ │ ├── index.tsx
│ │ │ ├── ReplyLikeSubscription.tsx
│ │ │ ├── CommentLikeSubscription.tsx
│ │ │ ├── PostLikeSubscription.tsx
│ │ │ ├── Subscriptions.tsx
│ │ │ ├── NewPostsSubscription.tsx
│ │ │ ├── NewCommentsSubscription.tsx
│ │ │ └── NewRepliesSubscription.tsx
│ ├── index.tsx
│ ├── routes.tsx
│ ├── react-app-env.d.ts
│ ├── App.tsx
│ └── serviceWorker.ts
├── .dockerignore
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── socialnetwork-post_example.gif
│ ├── socialnetwork-register_example.gif
│ ├── manifest.json
│ └── index.html
├── types
│ ├── babel-plugin-relay.d.ts
│ └── react-router-dom.d.ts
├── .gitignore
├── README.md
├── tsconfig.json
├── tailwind.config.js
├── Dockerfile
├── package.json
└── schema
│ └── schema.graphql
├── backend
├── .gitignore
├── src
│ ├── modules
│ │ ├── users
│ │ │ ├── mutations
│ │ │ │ ├── index.ts
│ │ │ │ ├── Login.ts
│ │ │ │ └── CreateUser.ts
│ │ │ ├── UserLoader.ts
│ │ │ ├── UserType.ts
│ │ │ └── UserModel.ts
│ │ ├── posts
│ │ │ ├── mutations
│ │ │ │ ├── index.ts
│ │ │ │ ├── PostCreation.ts
│ │ │ │ └── LikePost.ts
│ │ │ ├── subscriptions
│ │ │ │ ├── index.ts
│ │ │ │ ├── PostCreation.ts
│ │ │ │ └── PostLike.ts
│ │ │ ├── PostLoader.ts
│ │ │ ├── PostModel.ts
│ │ │ └── PostType.ts
│ │ ├── reply
│ │ │ ├── mutations
│ │ │ │ ├── index.ts
│ │ │ │ ├── CreateReply.ts
│ │ │ │ └── LikeReply.ts
│ │ │ ├── subscriptions
│ │ │ │ ├── index.ts
│ │ │ │ ├── ReplyLikeSubscription.ts
│ │ │ │ └── ReplyCreationSubscription.ts
│ │ │ ├── ReplyLoader.ts
│ │ │ ├── ReplyModel.ts
│ │ │ └── ReplyType.ts
│ │ └── comments
│ │ │ ├── mutations
│ │ │ ├── index.ts
│ │ │ ├── CreateComment.ts
│ │ │ └── LikeComment.ts
│ │ │ ├── subscriptions
│ │ │ ├── index.ts
│ │ │ ├── CommentLikeSubscription.ts
│ │ │ └── CreateCommentSubscription.ts
│ │ │ ├── CommentLoader.ts
│ │ │ ├── CommentModel.ts
│ │ │ └── CommentType.ts
│ ├── schema
│ │ ├── Schema.ts
│ │ ├── SubscriptionType.ts
│ │ ├── MutationType.ts
│ │ └── QueryType.ts
│ ├── auth.ts
│ ├── graphql
│ │ ├── NodeDefinitions.ts
│ │ └── registeredTypes.ts
│ ├── server.ts
│ └── app.ts
├── tslint.json
├── .babelrc
├── README.md
├── tsconfig.json
└── package.json
└── README.md
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | .env
--------------------------------------------------------------------------------
/frontend/src/Components/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Layout'
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | /build
4 | dockerBuild.log
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Footer';
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Header/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Navbar';
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Header/Navbar/index.tsx:
--------------------------------------------------------------------------------
1 | export { default } from './Navbar';
--------------------------------------------------------------------------------
/frontend/src/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/frontend/src/Pages/LoginPage/Components/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as LoginForm} from './LoginForm';
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/Pages/RegisterPage/Components/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as RegisterForm} from './RegisterForm';
--------------------------------------------------------------------------------
/frontend/types/babel-plugin-relay.d.ts:
--------------------------------------------------------------------------------
1 | export = index;
2 | declare function index(context: any): any;
3 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Streeterxs/socialnetwork/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Streeterxs/socialnetwork/HEAD/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Streeterxs/socialnetwork/HEAD/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as FeedPage} from './FeedPage';
2 | export * from './Components';
--------------------------------------------------------------------------------
/frontend/src/Pages/LoginPage/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Components';
2 | export {default as LoginPage} from './LoginPage';
--------------------------------------------------------------------------------
/frontend/src/Pages/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './LoginPage';
2 | export * from './RegisterPage';
3 | export * from './FeedPage';
--------------------------------------------------------------------------------
/frontend/src/Pages/RegisterPage/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Components';
2 | export { default as RegisterPage} from './RegisterPage';
--------------------------------------------------------------------------------
/frontend/src/config.tsx:
--------------------------------------------------------------------------------
1 | const config = {
2 | GRAPHQL_URL: process.env.REACT_APP_GRAPHQL_URL
3 | };
4 |
5 | export default config;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Comments';
2 | export * from './Posts';
3 | export * from './Replies';
--------------------------------------------------------------------------------
/frontend/public/socialnetwork-post_example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Streeterxs/socialnetwork/HEAD/frontend/public/socialnetwork-post_example.gif
--------------------------------------------------------------------------------
/frontend/public/socialnetwork-register_example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Streeterxs/socialnetwork/HEAD/frontend/public/socialnetwork-register_example.gif
--------------------------------------------------------------------------------
/backend/src/modules/users/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import CreateUser from './CreateUser';
2 | import Login from './Login';
3 |
4 | export default {
5 | CreateUser,
6 | Login
7 | }
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Footer } from './Footer';
2 | export { default as Header} from './Header';
3 | export { default as Layout } from './Layout';
--------------------------------------------------------------------------------
/backend/src/modules/posts/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import PostCreation from './PostCreation';
2 | import LikePost from './LikePost';
3 |
4 | export default {
5 | PostCreation,
6 | LikePost
7 | };
--------------------------------------------------------------------------------
/backend/src/modules/reply/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import CreateReply from './CreateReply';
2 | import LikeReply from './LikeReply';
3 |
4 | export default {
5 | CreateReply,
6 | LikeReply
7 | }
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Posts/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as Posts} from './Posts';
2 | export {default as Post} from './Post';
3 | export {default as PostCreation} from './PostCreation';
--------------------------------------------------------------------------------
/backend/src/modules/comments/mutations/index.ts:
--------------------------------------------------------------------------------
1 | import CreateComment from './CreateComment';
2 | import LikeComment from './LikeComment'
3 |
4 | export default {
5 | CreateComment,
6 | LikeComment
7 | };
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Replies/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as Replies} from './Replies';
2 | export {default as Reply} from './Reply';
3 | export {default as ReplyCreation} from './ReplyCreation';
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Comments/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as Comments} from './Comments';
2 | export {default as Comment} from './Comment';
3 | export {default as CommentCreation} from './CommentCreation';
--------------------------------------------------------------------------------
/backend/src/modules/posts/subscriptions/index.ts:
--------------------------------------------------------------------------------
1 | import PostCreationSubscription from './PostCreation';
2 | import PostLikeSubscription from './PostLike';
3 |
4 | export default {
5 | PostCreationSubscription,
6 | PostLikeSubscription
7 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## About
2 |
3 | This is a social network made using a Graphql backend, MongoDB database and a Relay Web Client.
4 |
5 | ## How to contribute
6 |
7 | To contribute just create any issue or PR if you like. I will read all. Any doubts contact me if you want.
8 |
--------------------------------------------------------------------------------
/backend/src/modules/reply/subscriptions/index.ts:
--------------------------------------------------------------------------------
1 | import ReplyCreationSubscription from './ReplyCreationSubscription';
2 | import ReplyLikeSubscription from './ReplyLikeSubscription';
3 |
4 | export default {
5 | ReplyCreationSubscription,
6 | ReplyLikeSubscription
7 | }
--------------------------------------------------------------------------------
/frontend/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/backend/src/modules/comments/subscriptions/index.ts:
--------------------------------------------------------------------------------
1 | import {default as CreateCommentSubscription} from './CreateCommentSubscription';
2 | import {default as CommentLikeSubscription} from './CommentLikeSubscription';
3 |
4 | export default {
5 | CreateCommentSubscription,
6 | CommentLikeSubscription
7 | }
--------------------------------------------------------------------------------
/backend/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "jsRules": {},
7 | "rules": {
8 | "trailing-comma": [ false ],
9 | "no-var-requires": false,
10 | "no-console": false
11 | },
12 | "rulesDirectory": []
13 | }
--------------------------------------------------------------------------------
/backend/src/modules/reply/ReplyLoader.ts:
--------------------------------------------------------------------------------
1 | import Reply, {IReply} from './ReplyModel';
2 | import Dataloader from 'dataloader';
3 |
4 | const replyDataLoader = new Dataloader((keys: string[]) => Reply.find({_id: {$in: keys}}));
5 |
6 | export const replyLoader = async (id: string) => {
7 | return await replyDataLoader.load(id);
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = () => (
4 |
7 | );
8 |
9 | export default Footer;
--------------------------------------------------------------------------------
/frontend/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/backend/.babelrc:
--------------------------------------------------------------------------------
1 | //.babelrc
2 |
3 | {
4 | "presets": [
5 | "@babel/preset-env",
6 | "@babel/preset-typescript"
7 | ],
8 | "plugins": [
9 | // https://github.com/parcel-bundler/parcel/issues/871#issuecomment-370135105
10 | // https://github.com/babel/babel-loader/issues/560#issuecomment-370180866
11 | "@babel/plugin-transform-runtime"
12 | ]
13 | }
--------------------------------------------------------------------------------
/backend/src/schema/Schema.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema } from "graphql";
2 |
3 | import QueryType from "./QueryType";
4 | import MutationType from "./MutationType";
5 | import SubscriptionType from "./SubscriptionType";
6 |
7 |
8 | export const Schema = new GraphQLSchema({
9 | query: QueryType,
10 | mutation: MutationType,
11 | subscription: SubscriptionType
12 | });
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | ## Social Network Backend
2 |
3 | This is a backend GraphQL Api to serve solutions for an social network project.
4 |
5 | ### To run
6 |
7 | Firstle install dependencies using npm or yarn package manager `npm install` or `yarn install`.
8 |
9 | You must install a MongoDB environment and execute `mongod` to run it.
10 |
11 | Run backend using `npm start` on `yarn start` depending on your local environment.
12 |
13 |
--------------------------------------------------------------------------------
/backend/src/auth.ts:
--------------------------------------------------------------------------------
1 | import User, { IUser } from "./modules/users/UserModel";
2 |
3 |
4 | const getUser = async (token: string): Promise => {
5 | try {
6 | const user = await User.findByToken(token);
7 | if (user) {
8 | user.verifyAuthToken();
9 | return user;
10 | }
11 | } catch(err) {
12 | return { user: null };
13 | }
14 | }
15 |
16 | export default getUser;
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES6",
4 | "esModuleInterop": true,
5 | "target": "es6",
6 | "noImplicitAny": true,
7 | "moduleResolution": "node",
8 | "sourceMap": true,
9 | "outDir": "dist",
10 | "baseUrl": ".",
11 | "paths": {
12 | "*": [
13 | "node_modules/*"
14 | ]
15 | }
16 | },
17 | "include": [
18 | "src/**/*"
19 | ]
20 | }
--------------------------------------------------------------------------------
/frontend/src/relay/environment.tsx:
--------------------------------------------------------------------------------
1 | import { Environment, Network, RecordSource, Store, SubscribeFunction } from 'relay-runtime';
2 | import { fetchGraphQL, setupSubscription } from './fetchGraphQL';
3 |
4 |
5 | const network = Network.create(
6 | fetchGraphQL,
7 | setupSubscription as SubscribeFunction
8 | );
9 | // Export a singleton instance of Relay Environment configured with our network function:
10 | export default new Environment({
11 | network,
12 | store: new Store(new RecordSource()),
13 | });
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # Graphql Generated
27 | __generated__
28 | index.css
29 |
30 | # Docker Logs
31 | dockerBuild.log
--------------------------------------------------------------------------------
/backend/src/schema/SubscriptionType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType } from "graphql";
2 |
3 | import PostSubscriptions from "../modules/posts/subscriptions/";
4 | import CommentSubscriptions from '../modules/comments/subscriptions'
5 | import ReplySubscriptions from "../modules/reply/subscriptions";
6 |
7 |
8 | const SubscriptionType = new GraphQLObjectType({
9 | name: 'SubscriptionType',
10 | fields: () => ({
11 | ...PostSubscriptions,
12 | ...CommentSubscriptions,
13 | ...ReplySubscriptions
14 | })
15 | });
16 |
17 | export default SubscriptionType
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | ## Social Network Frontend
2 |
3 | This is a Relay client to consume the backend graphql api.
4 |
5 | ## To Run
6 |
7 | Execute `yarn install` or `npm install` depending on your local package manager. To install all dependencies
8 |
9 | Execute `yarn start` or `npm start` depending on your local package manager. To run the frontend web client.
10 |
11 | ## Examples
12 |
13 | ### User creation and login
14 |
15 | 
16 |
17 | ### Post creation and reply
18 |
19 | 
20 |
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/index.tsx:
--------------------------------------------------------------------------------
1 | export {default as NewPostsSubscriptionModule} from './NewPostsSubscription';
2 | export {default as PostLikeSubscriptionModule} from './PostLikeSubscription';
3 |
4 | export {default as NewCommentsSubscriptionModule} from './NewCommentsSubscription';
5 | export {default as CommentLikeSubscriptionModule} from './CommentLikeSubscription';
6 |
7 | export {default as ReplyLikeSubscriptionModule} from './ReplyLikeSubscription';
8 | export {default as NewRepliesSubscriptionModule} from './NewRepliesSubscription';
9 |
10 | export {default} from './Subscriptions';
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import {Header, Footer} from './';
4 |
5 | const Layout = ({children, userIsLogged, handleLogoutLogin}: any) => {
6 |
7 | return (
8 |
9 |
10 |
11 | {children}
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default Layout
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.unstable_createRoot(
8 | document.getElementById('root') as HTMLElement
9 | ).render(
10 |
11 |
12 |
13 | )
14 |
15 | // If you want your app to work offline and load faster, you can change
16 | // unregister() to register() below. Note this comes with some pitfalls.
17 | // Learn more about service workers: https://bit.ly/CRA-PWA
18 | serviceWorker.unregister();
19 |
--------------------------------------------------------------------------------
/backend/src/schema/MutationType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType } from "graphql";
2 | import UserMutations from "../modules/users/mutations";
3 | import PostMutation from "../modules/posts/mutations";
4 | import CommentMutations from '../modules/comments/mutations';
5 | import ReplyMutations from '../modules/reply/mutations';
6 |
7 | const MutationType = new GraphQLObjectType({
8 | name: 'MutationType',
9 | description: 'Mutation Type',
10 | fields: () => ({
11 | ...UserMutations,
12 | ...PostMutation,
13 | ...CommentMutations,
14 | ...ReplyMutations
15 | })
16 | });
17 |
18 | export default MutationType;
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/backend/src/graphql/NodeDefinitions.ts:
--------------------------------------------------------------------------------
1 | import { nodeDefinitions, fromGlobalId } from "graphql-relay";
2 | import registeredTypes from "./registeredTypes";
3 |
4 | export const {nodeInterface, nodeField, nodesField} = nodeDefinitions(
5 | (globalId) => {
6 | const {type, id} = fromGlobalId(globalId);
7 | const registeredType = registeredTypes.find(x => {
8 | return type === x.name
9 | });
10 | return registeredType.loader(id);
11 | },
12 | (obj) => {
13 | const registeredType = registeredTypes.find(x => obj instanceof x.dbType);
14 | if (registeredType) return registeredType.qlType
15 | return null;
16 | }
17 | );
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | theme: {
3 | extend: {},
4 | boxShadow: {
5 | default: '0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)',
6 | md: '0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06)',
7 | lg: '0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05)',
8 | xl: '0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04)',
9 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
10 | outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
11 | focus: '0 0 0 3px rgba(66, 153, 225, 0.5)',
12 | custom: '0 5px 20px rgba(0, 0, 0, 0.2)'
13 | }
14 | },
15 | variants: {},
16 | plugins: [],
17 | }
18 |
--------------------------------------------------------------------------------
/backend/src/modules/reply/ReplyModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | export interface IReply extends mongoose.Document {
4 | author: string;
5 | content: string;
6 | createdAt: Date;
7 | updatedAt: Date;
8 | likes: string[];
9 | }
10 |
11 | const replySchema = new mongoose.Schema({
12 | author: {
13 | type: mongoose.Schema.Types.ObjectId,
14 | ref: 'User',
15 | required: true
16 | },
17 | content: {
18 | type: String,
19 | required: true
20 | },
21 | likes: [{
22 | type: mongoose.Schema.Types.ObjectId,
23 | ref: 'User'
24 | }]
25 | }, {
26 | timestamps: true
27 | });
28 |
29 | const Reply = mongoose.model('Reply_SocialNetwork', replySchema);
30 |
31 | export default Reply
--------------------------------------------------------------------------------
/frontend/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Route, Switch} from 'react-router-dom';
3 | import { LoginPage, RegisterPage, FeedPage } from './Pages'
4 |
5 | const routes = ({userIsLogged, setUserIsLogged}: {
6 | userIsLogged: boolean,
7 | setUserIsLogged: (userIsLogged: boolean) => void
8 | }) => (
9 |
10 | }/>
11 | }/>
12 |
13 | {/*
14 |
15 | */}
16 |
17 | )
18 |
19 | export default routes;
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | declare module 'babel-plugin-relay/macro' {
5 | export { graphql as default} from 'react-relay'
6 | }
7 | declare module 'react-relay/lib/relay-experimental' {
8 | export {
9 | EntryPointContainer,
10 | LazyLoadEntryPointContainer_DEPRECATED,
11 | MatchContainer,
12 | ProfilerContext,
13 | RelayEnvironmentProvider,
14 | PreloadableQueryRegistry,
15 | fetchQuery,
16 | preloadQuery,
17 | prepareEntryPoint,
18 | useBlockingPaginationFragment,
19 | useFragment,
20 | useLazyLoadQuery,
21 | useMutation,
22 | usePaginationFragment,
23 | usePreloadedQuery,
24 | useRefetchableFragment,
25 | useRelayEnvironment,
26 | useSubscribeToInvalidationState
27 | } from 'react-relay/lib/relay-experimental';
28 | }
--------------------------------------------------------------------------------
/backend/src/modules/comments/CommentLoader.ts:
--------------------------------------------------------------------------------
1 | import Comment, {IComment} from './CommentModel';
2 | import Dataloader from 'dataloader';
3 |
4 |
5 | const commentDataLoader = new Dataloader((keys: string[]) => Comment.find({_id: {$in: keys}}));
6 | const commentByReplyDataLoader = new Dataloader((keys: string[]) => Comment.find({replies: {$in: keys}}));
7 |
8 | export const commentLoader = async (id: string) => {
9 | const commentFounded = await commentDataLoader.load(id);
10 | console.log('comment founded by dataloader: ', commentFounded);
11 | return commentFounded;
12 | }
13 |
14 | export const commentLoaderByReply = async (replyId: string) => {
15 | const commentFounded = await commentByReplyDataLoader.load(replyId);
16 | return commentFounded;
17 | }
18 |
19 | // TODO implements DataLoader for this
20 | export const commentsFromPostLoader = async (postId: string) => {
21 | const comments = Comment.findCommentsForPost(postId);
22 | return comments
23 | }
--------------------------------------------------------------------------------
/backend/src/modules/users/UserLoader.ts:
--------------------------------------------------------------------------------
1 | import userModel, { IUser } from './UserModel';
2 | import Dataloader from 'dataloader';
3 |
4 | console.log('load user module');
5 |
6 | const userLoader = new Dataloader((keys: string[]) => userModel.find({_id: {$in: keys}}));
7 |
8 | const loadUser = async (id: string) => {
9 |
10 | console.log('loaduser id: ', id);
11 | const user = await userLoader.load(id);
12 |
13 | console.log('user by dataloader: ', user);
14 | return user;
15 | }
16 |
17 | const userIdLoader = (user: IUser, field: keyof IUser) => {
18 | return field === 'tokens' ? user.tokens[0].token : user[field];
19 | }
20 |
21 | const loadLoggedUser = async (token: string) => {
22 | console.log('loggeduser token: ', token);
23 |
24 | const user = await userModel.find({tokens: [{token}]});
25 |
26 | console.log('logged user by dataloader: ', user);
27 | return user;
28 | }
29 |
30 | export { loadUser, loadLoggedUser, userLoader, userIdLoader };
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # socialnetwork:frontDev
2 | FROM node:16-alpine as development
3 |
4 | RUN --mount=type=bind,source=/package.json,target=/package.json \
5 | --mount=type=bind,source=/package-lock.json,target=/package-lock.json \
6 | npm ci
7 |
8 | CMD npm run start
9 |
10 | FROM node:16-alpine as setup
11 |
12 | WORKDIR /socialnetwork
13 |
14 | COPY . .
15 |
16 | ENV REACT_APP_GRAPHQL_URL=http://localhost:3332/graphql
17 |
18 | # update first from experimental versions to remove --force
19 | RUN npm ci --force
20 | RUN npm run build
21 |
22 | # socialnetwork:frontProd
23 | FROM node:16-alpine as production
24 |
25 | WORKDIR /socialnetwork
26 |
27 | RUN --mount=type=bind,from=setup \
28 | npm version
29 |
30 | RUN --mount=type=bind,from=setup \
31 | cd socialnetwork && ls
32 |
33 | RUN --mount=type=bind,from=setup \
34 | ls -a
35 |
36 | COPY --from=setup ./socialnetwork/build ./
37 |
38 | RUN npm install --global http-server
39 |
40 | EXPOSE 3100
41 | CMD http-server . -o -p 3100
--------------------------------------------------------------------------------
/backend/src/modules/users/mutations/Login.ts:
--------------------------------------------------------------------------------
1 | import { mutationWithClientMutationId } from "graphql-relay";
2 | import { GraphQLString, graphql } from "graphql";
3 | import userType from "../UserType";
4 | import User from "../UserModel";
5 |
6 |
7 | const mutation = mutationWithClientMutationId({
8 | name: 'Login',
9 | description: 'Login a user, generates new token',
10 | inputFields: {
11 | email: {
12 | type: GraphQLString
13 | },
14 | password: {
15 | type: GraphQLString
16 | }
17 | },
18 | outputFields: {
19 | user: {
20 | type: userType,
21 | resolve: (user) => user
22 | }
23 | },
24 | mutateAndGetPayload: async ({email, password}) => {
25 | try {
26 | const user = await User.findByCredentials(email, password);
27 | const token = await user.generateAuthToken();
28 | return user;
29 | } catch (err) {
30 | console.log('entrou erro catch');
31 | console.log(err);
32 | }
33 | }
34 | });
35 |
36 | export default mutation;
--------------------------------------------------------------------------------
/backend/src/graphql/registeredTypes.ts:
--------------------------------------------------------------------------------
1 | import { loadUser } from '../modules/users/UserLoader';
2 | import { postLoader } from '../modules/posts/PostLoader';
3 | import { commentLoader } from '../modules/comments/CommentLoader';
4 | import { replyLoader } from '../modules/reply/ReplyLoader';
5 | import User from '../modules/users/UserModel';
6 | import Post from '../modules/posts/PostModel';
7 | import Reply from '../modules/reply/ReplyModel';
8 | import Comment from '../modules/comments/CommentModel';
9 |
10 | const registeredTypes = [
11 | {
12 | name: 'User',
13 | qlType: 'UserType',
14 | dbType: User,
15 | loader: loadUser
16 | },
17 | {
18 | name: 'Post',
19 | qlType: 'PostType',
20 | dbType: Post,
21 | loader: postLoader
22 | },
23 | {
24 | name: 'Comment',
25 | qlType: 'CommentType',
26 | dbType: Comment,
27 | loader: commentLoader
28 | },
29 | {
30 | name: 'Reply',
31 | qlType: 'ReplyType',
32 | dbType: Reply,
33 | loader: replyLoader
34 | }
35 | ]
36 |
37 | export default registeredTypes;
--------------------------------------------------------------------------------
/backend/src/server.ts:
--------------------------------------------------------------------------------
1 | import App from './app.js';
2 | import getUser from './auth.js';
3 | import { execute, subscribe } from 'graphql';
4 |
5 | import { SubscriptionServer } from 'subscriptions-transport-ws';
6 | import { createServer } from 'http';
7 | import { Schema } from './schema/Schema';
8 |
9 | type ConnectionParams = {
10 | authorization?: string;
11 | };
12 |
13 | (async () => {
14 | const server = createServer(App.callback());
15 |
16 | server.listen(process.env.PORT ?? '3333', () => {
17 | console.log('O servidor foi iniciado');
18 | });
19 |
20 | const subscriptionServer = SubscriptionServer.create(
21 | {
22 | onConnect: async (connectionParams: ConnectionParams) => {
23 | const user = await getUser(connectionParams.authorization);
24 | return {
25 | req: {},
26 | user
27 | }
28 | },
29 | // eslint-disable-next-line
30 | onDisconnect: () => console.log('Client subscription disconnected!'),
31 | execute,
32 | subscribe,
33 | schema: Schema,
34 | },
35 | {
36 | server,
37 | path: '/subscriptions',
38 | },
39 | );
40 | })();
--------------------------------------------------------------------------------
/backend/src/modules/users/mutations/CreateUser.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from "graphql";
2 |
3 | import userType from "../UserType";
4 | import userModel from "../UserModel";
5 | import { mutationWithClientMutationId } from "graphql-relay";
6 | import { loadUser } from "../UserLoader";
7 |
8 | export const mutation = mutationWithClientMutationId({
9 | name: 'UserCreation',
10 | description: 'Create new user',
11 | inputFields: {
12 | name: {
13 | type: GraphQLString
14 | },
15 | password: {
16 | type: GraphQLString
17 | },
18 | email: {
19 | type: GraphQLString
20 | }
21 | },
22 | outputFields: {
23 | user: {
24 | type: userType,
25 | resolve: async (user) => await loadUser(user.id)
26 | }
27 | },
28 | mutateAndGetPayload: async ({name, password, email}) => {
29 | try {
30 | const newUser = new userModel({name, password, email});
31 | const returnNewUser = await newUser.save();
32 | return returnNewUser;
33 | } catch (err) {
34 | console.log(err)
35 | return err;
36 | }
37 | }
38 | });
39 |
40 | export default mutation;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Replies/ReplyCreation.tsx:
--------------------------------------------------------------------------------
1 | import React, { unstable_useTransition as useTransition } from 'react';
2 |
3 | const ReplyCreation = ({formSubmit, replyContentChange}: {
4 | formSubmit: (event: React.FormEvent) => void,
5 | replyContentChange: (event: string) => void
6 | }) => {
7 | const [startTransition, isPending] = useTransition({
8 | timeoutMs: 10000
9 | });
10 |
11 | const replyCreationSubmitTransition = (event: React.FormEvent) => {
12 | startTransition(() => {
13 | formSubmit(event);
14 | })
15 | };
16 | return(
17 |
26 | );
27 | };
28 |
29 | export default ReplyCreation;
--------------------------------------------------------------------------------
/backend/src/schema/QueryType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString, GraphQLList, GraphQLNonNull } from 'graphql';
2 |
3 | import { postsLoaderByAuthors } from '../modules/posts/PostLoader';
4 | import userType from '../modules/users/UserType';
5 | import { nodeField } from '../graphql/NodeDefinitions';
6 | import { nodesField } from '../graphql/NodeDefinitions';
7 | import { PostConnection } from '../modules/posts/PostType';
8 | import { connectionArgs, connectionFromArray } from 'graphql-relay';
9 |
10 |
11 | const QueryType = new GraphQLObjectType({
12 | name: 'Query',
13 | description: 'General QueryType',
14 | fields: () => ({
15 | node: nodeField,
16 | nodes: nodesField,
17 | myself: {
18 | type: userType,
19 | resolve: (value, args, {user}) => {
20 | return user ? user : null;
21 | }
22 | },
23 | myPosts: {
24 | type: PostConnection,
25 | args: connectionArgs,
26 | resolve: async (value, args, context) => {
27 | return connectionFromArray(
28 | await postsLoaderByAuthors([context.user.id, ...context.user.friends]),
29 | args
30 | )
31 | }
32 | }})
33 | });
34 |
35 | export default QueryType;
--------------------------------------------------------------------------------
/backend/src/modules/comments/CommentModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { mongo } from 'mongoose';
2 |
3 | export interface IComment extends mongoose.Document {
4 | author: string,
5 | content: string,
6 | likes: string[],
7 | createdAt: Date,
8 | updatedAt: Date,
9 | replies: string[]
10 | }
11 |
12 | export interface ICommentModel extends mongoose.Model {
13 | findCommentsForPost(postId: string): IComment[]
14 | }
15 |
16 | const commentSchema = new mongoose.Schema({
17 | author: {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: 'User',
20 | required: true
21 | },
22 | content: {
23 | type: String,
24 | required: true
25 | },
26 | likes: [{
27 | type: mongoose.Schema.Types.ObjectId,
28 | ref: 'User'
29 | }],
30 | replies: [{
31 | type: mongoose.Schema.Types.ObjectId,
32 | ref: 'Reply'
33 | }]
34 | }, {
35 | timestamps: true
36 | });
37 |
38 | commentSchema.statics.findCommentsForPost = async (postId: string) => {
39 | const commentsOfPost = await Comment.find({post: postId}).sort({createdAt: 1});
40 | return commentsOfPost;
41 | };
42 |
43 | const Comment = mongoose.model('Comment_SocialNetwork', commentSchema);
44 |
45 | export default Comment;
46 |
--------------------------------------------------------------------------------
/backend/src/modules/posts/subscriptions/PostCreation.ts:
--------------------------------------------------------------------------------
1 | import { subscriptionWithClientId } from 'graphql-relay-subscription';
2 | import { withFilter } from 'graphql-subscriptions';
3 |
4 | import PostType from '../PostType';
5 | import { postLoader } from '../PostLoader';
6 | import { IPost } from '../PostModel';
7 | import { pubsub } from '../../../app';
8 | import { loadUser } from '../../users/UserLoader';
9 |
10 | const PostCreationSubscription = subscriptionWithClientId({
11 | name: 'PostCreationSubscription',
12 | inputFields: {},
13 | outputFields: {
14 | post: {
15 | type: PostType,
16 | resolve: async (post: IPost, _: any, context: any) => await postLoader(post.id)
17 | }
18 | },
19 | subscribe: withFilter(
20 | (input: any, context: any) => {
21 | return pubsub.asyncIterator('newPost');
22 | },
23 | async (postCreated: IPost, variables: any) => {
24 | const loggedUser = variables.user;
25 | const author = await loadUser(postCreated.author);
26 |
27 | return `${loggedUser._id}` === `${author._id}` || !!author.friends.includes(loggedUser._id);
28 | }
29 | ),
30 | getPayload: async (obj: any) => ({
31 | id: obj.id
32 | })
33 | });
34 |
35 | export default PostCreationSubscription;
--------------------------------------------------------------------------------
/backend/src/modules/posts/mutations/PostCreation.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId } from 'graphql-relay';
3 |
4 | import PostType from '../PostType';
5 | import Post from '../PostModel';
6 | import { IUser } from '../../../modules/users/UserModel';
7 | import { pubsub } from '../../../app';
8 | import { postLoader } from '../PostLoader';
9 |
10 | const PostCreation = mutationWithClientMutationId({
11 | name: 'PostCreation',
12 | description: 'Post Creation',
13 | inputFields: {
14 | content: {
15 | type: GraphQLString
16 | }
17 | },
18 | outputFields: {
19 | post: {
20 | type: PostType,
21 | resolve: async (post) => await postLoader(post.id)
22 | }
23 | },
24 | mutateAndGetPayload: async ({content}, {user}: {user: IUser}) => {
25 | try {
26 |
27 | const postCreated = new Post({content, author: `${user.id}`});
28 | await postCreated.save();
29 |
30 | user.posts.push(`${postCreated.id}`);
31 | await user.save();
32 |
33 | pubsub.publish('newPost', postCreated);
34 |
35 | return postCreated;
36 | } catch (err) {
37 | console.log(err);
38 | }
39 | }
40 | });
41 |
42 | export default PostCreation;
--------------------------------------------------------------------------------
/backend/src/modules/posts/subscriptions/PostLike.ts:
--------------------------------------------------------------------------------
1 | import { subscriptionWithClientId } from "graphql-relay-subscription";
2 | import { withFilter } from "graphql-subscriptions";
3 |
4 | import PostType from "../PostType";
5 | import { pubsub } from "../../../app";
6 | import { IPost } from "../PostModel";
7 | import { postLoader } from "../PostLoader";
8 | import { loadUser } from "../../users/UserLoader";
9 |
10 | const PostLikeSubscription = subscriptionWithClientId({
11 | name: 'PostLikeSubscription',
12 | description: 'Post Like subscription',
13 | inputFields: {},
14 | outputFields: {
15 | post: {
16 | type: PostType,
17 | resolve: (post: IPost) => postLoader(post.id)
18 | }
19 | },
20 | subscribe: withFilter(
21 | (input: any, context: any) => {
22 | return pubsub.asyncIterator('postLike');
23 | },
24 | async (postLiked: IPost, variables: any) => {
25 | const loggedUser = variables.user;
26 | const author = await loadUser(postLiked.author);
27 |
28 | return `${loggedUser._id}` === `${author._id}` || !!author.friends.includes(loggedUser._id);
29 | }
30 | ),
31 | getPayload: (obj: any) => {
32 | return {
33 | id: obj.id
34 | }
35 | }
36 | });
37 |
38 | export default PostLikeSubscription
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Posts/PostCreation.tsx:
--------------------------------------------------------------------------------
1 | import React, { unstable_useTransition as useTransition } from 'react';
2 |
3 | const PostCreation = ({contentChange, formSubmit}:
4 | {
5 | contentChange: (content: string) => void,
6 | formSubmit: (event: React.FormEvent) => void
7 | }
8 | ) => {
9 |
10 | const [startTransition, isPending] = useTransition();
11 |
12 | const postSubmitTransition = (event: React.FormEvent) => {
13 | startTransition(() => {
14 | formSubmit(event);
15 | })
16 | }
17 |
18 | return (
19 |
27 | );
28 | };
29 |
30 | export default PostCreation;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Comments/CommentCreation.tsx:
--------------------------------------------------------------------------------
1 | import React, { unstable_useTransition as useTransition} from 'react';
2 |
3 | const CommentCreation = ({formSubmit, commentContentChange}: {
4 | formSubmit: (event: React.FormEvent) => void,
5 | commentContentChange: (event: string) => void
6 | }) => {
7 | const [startTransition, isPending] = useTransition({
8 | timeoutMs: 10000
9 | });
10 | const concurrentFormSubmit = (event: any) => {
11 | startTransition(() => {
12 | formSubmit(event);
13 | })
14 | };
15 | return(
16 |
28 | );
29 | };
30 |
31 | export default CommentCreation;
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, {Suspense, useState, useEffect} from 'react';
2 | import { RelayEnvironmentProvider, useRelayEnvironment } from 'react-relay/hooks';
3 |
4 | import environment from './relay/environment';
5 | import Routes from './routes';
6 | import { Layout } from './Components';
7 | import './App.css';
8 | import { BrowserRouter } from 'react-router-dom';
9 | import SubscriptionModule from './Services/Subscriptions';
10 |
11 |
12 |
13 | const App = () => {
14 | const [userIsLogged, setUserIsLogged] = useState(!!localStorage.getItem('authToken'));
15 | const [environment, setEnvironment] = useState(useRelayEnvironment());
16 |
17 |
18 | const handleLogoutLogin = () => {
19 | if (userIsLogged) {
20 | localStorage.removeItem('authToken');
21 | setUserIsLogged(false);
22 | }
23 | }
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | const AppRoot = () => (
38 |
39 |
40 |
41 | )
42 |
43 | export default AppRoot;
44 |
--------------------------------------------------------------------------------
/backend/src/modules/posts/PostLoader.ts:
--------------------------------------------------------------------------------
1 | import Post, { IPost } from './PostModel';
2 | import Dataloader from 'dataloader';
3 |
4 |
5 | const postDataLoader = new Dataloader((keys: string[]) => Post.find({_id: {$in: keys}}));
6 | const postByCommentDataLoader = new Dataloader((keys: string[]) => Post.find({comments: {$in: keys}}));
7 |
8 | export const postLoader = async (id: string) => {
9 |
10 | console.log('postloader call');
11 |
12 | const postFounded = await postDataLoader.load(id);
13 | console.log('post founded by dataloader: ', postFounded);
14 | return postFounded;
15 | };
16 |
17 | export const postLoaderByComment = async (commentId: string) => {
18 |
19 | console.log('postloader by comments call');
20 |
21 | const postFounded = await postByCommentDataLoader.load(commentId);
22 | return postFounded;
23 | }
24 |
25 | // TODO implement dataloader for this database call
26 | export const postsLoaderByAuthors = async (ids: string[]) => {
27 |
28 | const postList = await Post.findByAuthorIdList(ids);
29 | return postList;
30 | }
31 |
32 | // TODO implement dataloader for multiple post for one author if is logical
33 | export const authorPostsLoader = async (id: string) => {
34 |
35 | console.log('postloader by author call');
36 |
37 | const authorPosts = await Post.findAuthorPosts(id);
38 | return authorPosts
39 | };
40 |
41 | export const loggedUserPosts = async (token: string) => {
42 |
43 | console.log('postloader by loggeduser call');
44 |
45 | const posts = await Post.findLoggedUserPosts(token);
46 | return posts;
47 | };
--------------------------------------------------------------------------------
/backend/src/modules/reply/mutations/CreateReply.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString, GraphQLInt } from 'graphql';
2 | import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay';
3 |
4 | import ReplyType from '../ReplyType';
5 | import Reply from '../ReplyModel';
6 | import { commentLoader } from '../../../modules/comments/CommentLoader';
7 | import { pubsub } from '../../../app';
8 | import { replyLoader } from '../ReplyLoader';
9 |
10 | const CreateReply = mutationWithClientMutationId({
11 | name: 'CreateReply',
12 | description: 'Create Reply Mutation',
13 | inputFields: {
14 | content: {
15 | type: GraphQLString
16 | },
17 | comment: {
18 | type: GraphQLString
19 | }
20 | },
21 | outputFields: {
22 | reply: {
23 | type: ReplyType,
24 | resolve: async (reply) => await replyLoader(reply.id)
25 | }
26 | },
27 | mutateAndGetPayload: async ({content, comment}, {user}) => {
28 | try {
29 |
30 | const {type, id} = fromGlobalId(comment);
31 |
32 | const reply = new Reply({author: user.id, content});
33 | await reply.save();
34 |
35 | const commentReturned = await commentLoader(id);
36 | commentReturned.replies = [reply.id].concat(commentReturned.replies);
37 | await commentReturned.save();
38 |
39 | pubsub.publish('newReply', reply);
40 |
41 | return reply;
42 | } catch (err) {
43 | console.log(err);
44 | }
45 | }
46 | });
47 |
48 | export default CreateReply;
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/ReplyLikeSubscription.tsx:
--------------------------------------------------------------------------------
1 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
2 | import { Disposable, requestSubscription } from "react-relay";
3 | import { GraphQLSubscriptionConfig } from "relay-runtime";
4 | import graphql from "babel-plugin-relay/macro";
5 |
6 | const replyLikeSubscription = graphql `
7 | subscription ReplyLikeSubscription($clientSubscriptionId: String) {
8 | ReplyLikeSubscription(input: {clientSubscriptionId: $clientSubscriptionId}) {
9 | reply {
10 | id
11 | likes
12 | }
13 | }
14 | }
15 | `;
16 |
17 | const ReplyLikeSubscriptionModule = (environment: RelayModernEnvironment) => {
18 | let objDisposable: Disposable;
19 |
20 | const dispose = () => {
21 | if (objDisposable) objDisposable.dispose()
22 | };
23 |
24 | const subscribe = () => {
25 | objDisposable = requestSubscription(
26 | environment,
27 | generateSubscriptionConfigs()
28 | )
29 | };
30 |
31 | const generateSubscriptionConfigs = (): GraphQLSubscriptionConfig<{}> => ({
32 | subscription: replyLikeSubscription,
33 | variables: {
34 | clientSubscriptionId: localStorage.getItem('authToken') + '10'
35 | },
36 | onNext: (response: any) => {
37 | console.log('response like subscription: ', response)
38 | }
39 | });
40 |
41 | return {
42 | dispose,
43 | subscribe
44 | };
45 | };
46 |
47 | export default ReplyLikeSubscriptionModule
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/CommentLikeSubscription.tsx:
--------------------------------------------------------------------------------
1 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
2 | import graphql from "babel-plugin-relay/macro";
3 | import { Disposable, requestSubscription } from "react-relay";
4 | import { GraphQLSubscriptionConfig } from "relay-runtime";
5 |
6 | const commentLikeSubscription = graphql `
7 | subscription CommentLikeSubscription($clientSubscriptionId: String) {
8 | CommentLikeSubscription(input: {clientSubscriptionId: $clientSubscriptionId}) {
9 | comment {
10 | id,
11 | likes
12 | }
13 | }
14 | }
15 | `;
16 |
17 | const CommentLikeSubscriptionModule = (environment: RelayModernEnvironment) => {
18 | let objDisposable: Disposable;
19 |
20 | const dispose = () => {
21 | if (objDisposable) objDisposable.dispose();
22 | }
23 |
24 | const subscribe = () => {
25 | objDisposable = requestSubscription(
26 | environment,
27 | generateSubscriptionConfig()
28 | )
29 | }
30 |
31 | const generateSubscriptionConfig = (): GraphQLSubscriptionConfig<{}> => ({
32 | subscription: commentLikeSubscription,
33 | variables: {
34 | clientSubscriptionId: localStorage.getItem('authToken' + '8')
35 | },
36 | onNext: (response: any) => {
37 | console.log('response ws comment!: ', response)
38 | }
39 | });
40 |
41 | return {
42 | subscribe,
43 | dispose
44 | }
45 | };
46 |
47 | export default CommentLikeSubscriptionModule
--------------------------------------------------------------------------------
/backend/src/modules/reply/ReplyType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql';
2 | import { globalIdField, connectionDefinitions, connectionArgs, connectionFromArray } from 'graphql-relay';
3 |
4 | import userType from '../users/UserType';
5 | import { IReply } from './ReplyModel';
6 | import { loadUser } from '../users/UserLoader';
7 | import { nodeInterface } from '../../graphql/NodeDefinitions';
8 |
9 | const ReplyType = new GraphQLObjectType({
10 | name: 'ReplyType',
11 | description: 'Reply type',
12 | fields: () => ({
13 | id: globalIdField('Reply'),
14 | author: {
15 | type: userType,
16 | resolve: (reply) => loadUser(reply.author)
17 | },
18 | content: {
19 | type: GraphQLString,
20 | resolve: (reply) => reply.content
21 | },
22 | createdAt: {
23 | type: GraphQLString,
24 | resolve: (reply) => reply.createdAt
25 | },
26 | updatedAt: {
27 | type: GraphQLString,
28 | resolve: (reply) => reply.updatedAt
29 | },
30 | likes: {
31 | type: GraphQLInt,
32 | resolve: (reply) => reply.likes.length
33 | },
34 | userHasLiked: {
35 | type: GraphQLBoolean,
36 | resolve: (reply, args, {user}) => reply.likes.includes(user.id)
37 | }
38 | }),
39 | interfaces: [nodeInterface]
40 | });
41 |
42 | export const {connectionType: ReplyConnection} =
43 | connectionDefinitions({nodeType: ReplyType});
44 |
45 | export default ReplyType
--------------------------------------------------------------------------------
/backend/src/modules/comments/subscriptions/CommentLikeSubscription.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from "graphql";
2 | import { withFilter } from "graphql-subscriptions";
3 | import { subscriptionWithClientId } from "graphql-relay-subscription";
4 |
5 | import { commentLoader } from "../CommentLoader";
6 | import CommentType from "../CommentType";
7 | import { pubsub } from "../../../app";
8 | import { postLoaderByComment } from "../../posts/PostLoader";
9 | import { loadUser } from "../../users/UserLoader";
10 |
11 | const CommentLikeSubscription = subscriptionWithClientId({
12 | name: 'CommentLikeSubscription',
13 | description: 'Comment Like subscription',
14 | inputFields: {},
15 | outputFields: {
16 | comment: {
17 | type: CommentType,
18 | resolve: (commentObj: any) => commentLoader(commentObj.id)
19 | }
20 | },
21 | subscribe: withFilter(
22 | (input: any, context: any) => {
23 | return pubsub.asyncIterator('commentLike');
24 | },
25 | async (commentPayload: any, variables: any) => {
26 |
27 | const postFounded = await postLoaderByComment(commentPayload._id);
28 | const postFoundedAuthor = await loadUser(postFounded.author);
29 |
30 | const loggedUser = variables.user;
31 |
32 | return `${loggedUser._id}` === `${postFoundedAuthor._id}` || postFoundedAuthor.friends.includes(loggedUser._id);
33 | }
34 | ),
35 | getPayload: (payloadCommentLike: any) => {
36 |
37 | return {
38 | id: payloadCommentLike.id,
39 | }
40 | }
41 | });
42 |
43 | export default CommentLikeSubscription;
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/PostLikeSubscription.tsx:
--------------------------------------------------------------------------------
1 | import graphql from "babel-plugin-relay/macro";
2 | import { Disposable, requestSubscription } from "react-relay";
3 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
4 | import { GraphQLSubscriptionConfig, ConnectionHandler, RecordProxy, ROOT_ID } from "relay-runtime";
5 |
6 | const postLikeSubscription = graphql`
7 | subscription PostLikeSubscription ($clientSubscriptionId: String!) {
8 | PostLikeSubscription(input: {clientSubscriptionId: $clientSubscriptionId}) {
9 | post {
10 | id
11 | likes
12 | }
13 | }
14 | }
15 | `
16 |
17 | const PostLikeSubscriptionModule = (environment: RelayModernEnvironment) => {
18 | let objDisposable: Disposable;
19 |
20 |
21 | const dispose = () => {
22 | if (objDisposable) objDisposable.dispose();
23 | };
24 |
25 | const subscribe = () => {
26 | objDisposable = requestSubscription(
27 | environment,
28 | generateSubscriptionConfig()
29 | );
30 | };
31 |
32 | const generateSubscriptionConfig = (): GraphQLSubscriptionConfig<{}> => {
33 | return {
34 | subscription: postLikeSubscription,
35 | variables: {clientSubscriptionId: localStorage.getItem('authToken') + '2'},
36 | onNext: (response: any) => {
37 | console.log('response like subscription: ', response)
38 | }
39 | }
40 |
41 | }
42 |
43 | return {
44 | dispose,
45 | subscribe
46 | }
47 |
48 | }
49 |
50 | export default PostLikeSubscriptionModule;
--------------------------------------------------------------------------------
/backend/src/modules/posts/mutations/LikePost.ts:
--------------------------------------------------------------------------------
1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay";
2 | import { GraphQLString } from "graphql";
3 |
4 | import PostType from "../PostType";
5 | import { IUser } from "../../../modules/users/UserModel";
6 | import { postLoader } from "../PostLoader";
7 | import { pubsub } from "../../../app";
8 |
9 | const LikePost = mutationWithClientMutationId({
10 | name: 'LikePost',
11 | description: 'Mutation for like handling for posts',
12 | inputFields: {
13 | post: {
14 | type: GraphQLString
15 | }
16 | },
17 | outputFields: {
18 | post: {
19 | type: PostType,
20 | resolve: async (post) => await postLoader(post.id)
21 | }
22 | },
23 | mutateAndGetPayload: async ({post}: {post: string}, {user}: {user: IUser}) => {
24 | try {
25 |
26 | const {type, id} = fromGlobalId(post);
27 |
28 | const postId = id;
29 | const postFounded = await postLoader(postId);
30 |
31 | if (postFounded.likes.includes(user.id)) {
32 |
33 | const indexOf = postFounded.likes.indexOf(user.id);
34 | postFounded.likes.splice(indexOf, 1);
35 | await postFounded.save();
36 |
37 | pubsub.publish('postLike', postFounded);
38 |
39 | return postFounded;
40 | }
41 |
42 | postFounded.likes.push(user._id);
43 | await postFounded.save();
44 |
45 | pubsub.publish('postLike', postFounded);
46 |
47 | return postFounded;
48 | } catch(err) {
49 | console.log(err);
50 | }
51 | }
52 | });
53 |
54 | export default LikePost;
--------------------------------------------------------------------------------
/backend/src/modules/reply/subscriptions/ReplyLikeSubscription.ts:
--------------------------------------------------------------------------------
1 | import { subscriptionWithClientId } from "graphql-relay-subscription";
2 | import { withFilter } from "graphql-subscriptions";
3 |
4 | import { replyLoader } from "../ReplyLoader";
5 | import ReplyType from "../ReplyType";
6 | import { pubsub } from "../../../app";
7 | import { IReply } from "../ReplyModel";
8 | import { IComment } from "../../comments/CommentModel";
9 | import { commentLoaderByReply } from "../../comments/CommentLoader";
10 | import { IPost } from "../../posts/PostModel";
11 | import { postLoaderByComment } from "../../posts/PostLoader";
12 | import { loadUser } from "../../users/UserLoader";
13 |
14 | const replyLikeSubscription = subscriptionWithClientId({
15 | name: 'ReplyLikeSubscription',
16 | description: 'Subscription to fetch likes in replies',
17 | inputFields: {},
18 | outputFields: {
19 | reply: {
20 | type: ReplyType,
21 | resolve: (replyObj: any) => replyLoader((replyObj.id))
22 | }
23 | },
24 | subscribe: withFilter(
25 | (input: any, context: any)=>{
26 | return pubsub.asyncIterator('replyLike');
27 | },
28 | async (reply: IReply, variables: any)=>{
29 | const commentFounded: IComment = await commentLoaderByReply(reply._id);
30 | const postFounded: IPost = await postLoaderByComment(commentFounded._id);
31 | const postAuthor = await loadUser(postFounded.author);
32 |
33 | return `${postAuthor._id}` === `${reply.author}` || postAuthor.friends.includes(reply.author);
34 | }
35 | ),
36 | getPayload: (replyObj: any) => ({
37 | id: replyObj.id
38 | })
39 | });
40 |
41 | export default replyLikeSubscription;
--------------------------------------------------------------------------------
/backend/src/modules/comments/mutations/CreateComment.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from 'graphql';
2 | import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay';
3 |
4 | import Comment from '../CommentModel';
5 | import CommentType from '../CommentType';
6 | import { postLoader } from '../../../modules/posts/PostLoader';
7 | import { IUser } from '../../../modules/users/UserModel';
8 | import { pubsub } from '../../../app';
9 | import { commentLoader } from '../CommentLoader';
10 |
11 | const CreateComment = mutationWithClientMutationId({
12 | name: 'CreateComment',
13 | description: 'Create Comment Mutation',
14 | inputFields: {
15 | content: {
16 | type: GraphQLString
17 | },
18 | post: {
19 | type: GraphQLString
20 | }
21 | },
22 | outputFields: {
23 | comment: {
24 | type: CommentType,
25 | resolve: async (comment) => await commentLoader(comment)
26 | }
27 | },
28 | mutateAndGetPayload: async ({content, post} : {
29 | content: string,
30 | post: string
31 | }, {user}: {user: IUser}) => {
32 | try {
33 |
34 | const {type, id} = fromGlobalId(post);
35 |
36 | const postId = id;
37 |
38 | const comment = new Comment({author: user.id, content});
39 | await comment.save();
40 |
41 | const postFinded = await postLoader(postId);
42 | postFinded.comments.push(comment.id);
43 | await postFinded.save();
44 |
45 | pubsub.publish('newComment', comment);
46 |
47 | return comment;
48 | } catch (err) {
49 | console.log(err);
50 | }
51 | }
52 | });
53 |
54 | export default CreateComment;
--------------------------------------------------------------------------------
/backend/src/modules/posts/PostModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import jsonwebtoken from 'jsonwebtoken';
3 |
4 |
5 | export interface IPost extends mongoose.Document {
6 | author: string;
7 | content: string;
8 | createdAt: Date;
9 | updatedAt: Date;
10 | likes: string[];
11 | comments: string[]
12 | }
13 |
14 | export interface IPostModel extends mongoose.Model {
15 | findAuthorPosts(id: string): IPost[];
16 | findByAuthorIdList(ids: string[]): IPost[];
17 | findLoggedUserPosts(token: string): IPost[];
18 | }
19 |
20 | const postSchema = new mongoose.Schema({
21 | author: {
22 | type: Schema.Types.ObjectId,
23 | ref: 'User',
24 | required: true
25 | },
26 | content: {
27 | type: String,
28 | required: true
29 | },
30 | likes: [{
31 | type: Schema.Types.ObjectId,
32 | ref: 'User'
33 | }],
34 | comments: [{
35 | type: Schema.Types.ObjectId,
36 | ref: 'Comment'
37 | }]
38 | }, {
39 | timestamps: true
40 | });
41 |
42 | postSchema.statics.findAuthorPosts = async (id: string) => {
43 | const posts = await Post.find({author: id}).sort({createdAt: -1});
44 | return posts;
45 | }
46 |
47 | postSchema.statics.findByAuthorIdList = async (ids: string[]) => {
48 | const posts = await Post.find({author: {$in: ids}}).sort({createdAt: -1});
49 | return posts;
50 | }
51 |
52 | postSchema.statics.findLoggedUserPosts = async (token: string) => {
53 | const jsonPayload: any = jsonwebtoken.decode(token);
54 | return await Post.find({author: jsonPayload._id}).sort({createdAt: -1});
55 | }
56 |
57 |
58 | const Post = mongoose.model('Post_SocialNetwork', postSchema);
59 |
60 | export default Post;
--------------------------------------------------------------------------------
/frontend/src/Pages/RegisterPage/RegisterPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useHistory } from 'react-router';
3 | import graphql from 'babel-plugin-relay/macro';
4 | import { useMutation } from 'react-relay/lib/relay-experimental';
5 |
6 | import { RegisterForm } from './Components';
7 |
8 | const registerMutation = graphql`
9 | mutation RegisterPageLoginMutation($name: String!, $email: String!, $password: String!) {
10 | CreateUser(input: {name: $name, email: $email, password: $password, clientMutationId: "1"}) {
11 | user {
12 | name,
13 | email
14 | }
15 | }
16 | }
17 | `;
18 |
19 |
20 | const RegisterPage = () => {
21 | const history = useHistory();
22 |
23 | const [commit, isInFlight] = useMutation(registerMutation);
24 | let name = '';
25 | let email = '';
26 | let password = '';
27 | const handleFormSubmition = (event: React.FormEvent) => {
28 |
29 | event.preventDefault();
30 |
31 | const variables = {
32 | name,
33 | email,
34 | password
35 | };
36 |
37 | commit({
38 | variables,
39 | onCompleted(data: any) {
40 | console.log(data);
41 | history.push('/login');
42 | },
43 | });
44 | }
45 | if (isInFlight) {
46 | return (
47 |
48 | 'Loading...'
49 |
50 | );
51 | }
52 | return (
53 | {name = nameReturned}}
55 | emailChange={(emailReturned) => {email = emailReturned}}
56 | passwordChange={(passwordReturned) => {password = passwordReturned}}
57 | formSubmit={handleFormSubmition}/>
58 | );
59 | };
60 |
61 | export default RegisterPage;
--------------------------------------------------------------------------------
/backend/src/modules/reply/mutations/LikeReply.ts:
--------------------------------------------------------------------------------
1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay";
2 | import { GraphQLString } from "graphql";
3 |
4 | import ReplyType from "../ReplyType";
5 | import { IUser } from "../../../modules/users/UserModel";
6 | import Reply from "../ReplyModel";
7 | import { pubsub } from "../../../app";
8 | import { replyLoader } from "../ReplyLoader";
9 |
10 | const LikeReply = mutationWithClientMutationId({
11 | name: 'LikeReplay',
12 | description: 'Handle likes for replies',
13 | inputFields: {
14 | reply: {
15 | type: GraphQLString
16 | }
17 | },
18 | outputFields: {
19 | reply: {
20 | type: ReplyType,
21 | resolve: async (reply) => await replyLoader(reply.id)
22 | }
23 | },
24 | mutateAndGetPayload: async ({reply}: {reply: string}, {user}: {user: IUser}) => {
25 | try {
26 |
27 | const {type, id} = fromGlobalId(reply);
28 |
29 | const replyId = id;
30 | const replyFounded = await Reply.findById(replyId);
31 |
32 | if (replyFounded.likes.includes(user.id)) {
33 |
34 | const indexOf = replyFounded.likes.indexOf(user.id);
35 | replyFounded.likes.splice(indexOf, 1);
36 | await replyFounded.save();
37 |
38 | pubsub.publish('replyLike', replyFounded);
39 | return replyFounded;
40 | }
41 |
42 | replyFounded.likes.push(user.id);
43 | await replyFounded.save();
44 |
45 | pubsub.publish('replyLike', replyFounded);
46 |
47 | return replyFounded;
48 | } catch(error) {
49 | console.log(error);
50 | }
51 | }
52 | });
53 |
54 | export default LikeReply
--------------------------------------------------------------------------------
/backend/src/modules/comments/mutations/LikeComment.ts:
--------------------------------------------------------------------------------
1 | import { mutationWithClientMutationId, fromGlobalId } from "graphql-relay";
2 | import { GraphQLString } from "graphql";
3 |
4 | import CommentType from "../CommentType";
5 | import Comment from '../CommentModel';
6 | import { IUser } from "../../../modules/users/UserModel";
7 | import { pubsub } from "../../../app";
8 | import { commentLoader } from "../CommentLoader";
9 |
10 | const LikeComment = mutationWithClientMutationId({
11 | name: 'LikeComment',
12 | description: 'Update total likes for a comment type',
13 | inputFields: {
14 | comment: {
15 | type: GraphQLString
16 | }
17 | },
18 | outputFields: {
19 | comment: {
20 | type: CommentType,
21 | resolve: async (comment) => await commentLoader(comment)
22 | }
23 | },
24 | mutateAndGetPayload: async ({comment}, {user}: {user: IUser}) => {
25 | try {
26 |
27 | const {type, id} = fromGlobalId(comment);
28 |
29 | const commentId = id;
30 | const commentFound = await Comment.findOne({_id: commentId});
31 |
32 | if (commentFound.likes.includes(user.id)) {
33 |
34 | const indexOf = commentFound.likes.indexOf(user.id);
35 | commentFound.likes.splice(indexOf, 1);
36 | await commentFound.save();
37 |
38 | pubsub.publish('commentLike', commentFound);
39 | return commentFound
40 | };
41 |
42 | commentFound.likes.push(user.id);
43 | await commentFound.save();
44 |
45 | pubsub.publish('commentLike', commentFound);
46 |
47 | return commentFound;
48 | } catch (err) {
49 | console.log(err);
50 | }
51 | }
52 | });
53 |
54 | export default LikeComment;
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/backend/src/modules/comments/subscriptions/CreateCommentSubscription.ts:
--------------------------------------------------------------------------------
1 | import { subscriptionWithClientId } from "graphql-relay-subscription";
2 | import { withFilter } from "graphql-subscriptions";
3 |
4 | import CommentType from "../CommentType";
5 | import { commentLoader } from "../CommentLoader";
6 | import { pubsub } from "../../../app";
7 | import { IComment } from "../CommentModel";
8 | import { postLoaderByComment } from "../../posts/PostLoader";
9 | import { loadUser } from "../../users/UserLoader";
10 | import { GraphQLString } from "graphql";
11 | import PostType from "../../posts/PostType";
12 |
13 | const CreateCommentSubscription = subscriptionWithClientId({
14 | name: "CreateCommentSubscription",
15 | description: "Create Comment Subscription",
16 | inputFields: {},
17 | outputFields: {
18 | comment: {
19 | type: CommentType,
20 | resolve: async (comment: any) => await commentLoader(comment.id)
21 | },
22 | post: {
23 | type: PostType,
24 | resolve: async (comment: any) =>{
25 | const postFounded = await postLoaderByComment(comment.id);
26 | return postFounded
27 | }
28 | }
29 | },
30 | subscribe: withFilter(
31 | (input: any, context: any) => {
32 | return pubsub.asyncIterator('newComment');
33 | }, async (comment: IComment, variables: any) => {
34 | const postFounded = await postLoaderByComment(comment._id);
35 | const postFoundedAuthor = await loadUser(postFounded.author);
36 |
37 | const loggedUser = variables.user;
38 |
39 | return `${loggedUser._id}` === `${postFoundedAuthor._id}` || postFoundedAuthor.friends.includes(loggedUser._id);
40 | }
41 | ),
42 | getPayload: (obj: any) => {
43 | return {
44 | id: obj.id
45 | }
46 | }
47 | });
48 |
49 | export default CreateCommentSubscription;
--------------------------------------------------------------------------------
/backend/src/modules/reply/subscriptions/ReplyCreationSubscription.ts:
--------------------------------------------------------------------------------
1 | import { subscriptionWithClientId } from "graphql-relay-subscription";
2 | import { withFilter } from "graphql-subscriptions";
3 |
4 | import ReplyType from "../ReplyType";
5 | import { replyLoader } from "../ReplyLoader";
6 | import CommentType from "../../comments/CommentType";
7 | import { commentLoaderByReply } from "../../comments/CommentLoader";
8 | import { IReply } from "../ReplyModel";
9 | import { pubsub } from "../../../app";
10 | import { IComment } from "../../comments/CommentModel";
11 | import { IPost } from "../../posts/PostModel";
12 | import { postLoaderByComment } from "../../posts/PostLoader";
13 | import { loadUser } from "../../users/UserLoader";
14 |
15 | const ReplyCreationSubscription = subscriptionWithClientId({
16 | name: 'ReplyCreationSubscription',
17 | description: 'Reply Creation Subscription',
18 | inputFields: {},
19 | outputFields: {
20 | reply: {
21 | type: ReplyType,
22 | resolve: (replyObj: any) => replyLoader(replyObj.id)
23 | },
24 | comment: {
25 | type: CommentType,
26 | resolve: (replyObj: any) => commentLoaderByReply(replyObj.id)
27 | }
28 | },
29 | subscribe: withFilter(
30 | (input: any, context: any) => {
31 | return pubsub.asyncIterator('newReply')
32 | },
33 | async (reply: IReply, variables: any) => {
34 | const commentFounded: IComment = await commentLoaderByReply(reply._id);
35 | const postFounded: IPost = await postLoaderByComment(commentFounded._id);
36 | const postAuthor = await loadUser(postFounded.author);
37 |
38 | return `${postAuthor._id}` === `${reply.author}` || postAuthor.friends.includes(reply.author);
39 | }
40 | ),
41 | getPayload: (replyObj: any) => ({
42 | id: replyObj.id
43 | })
44 | });
45 |
46 | export default ReplyCreationSubscription;
--------------------------------------------------------------------------------
/backend/src/app.ts:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 | import dotenv from 'dotenv';
3 | import path from 'path';
4 | import { GraphQLError } from 'graphql';
5 | import { PubSub } from 'graphql-subscriptions';
6 | import Koa from 'koa';
7 | import Router from 'koa-router';
8 | import logger from 'koa-logger';
9 | import cors from 'kcors';
10 | import {graphqlHTTP} from 'koa-graphql';
11 | import koaPlayground from 'graphql-playground-middleware-koa';
12 |
13 | import { Schema } from './schema/Schema';
14 | import User, { IUser } from './modules/users/UserModel';
15 | import getUser from './auth';
16 |
17 | dotenv.config({path: path.join(__dirname, '/./../.env')});
18 |
19 | mongoose.connect(process.env.MONGODB_URL, {useNewUrlParser: true, useUnifiedTopology: true});
20 |
21 | const router = new Router();
22 | const app = new Koa();
23 |
24 | app.use(logger());
25 | app.use(cors());
26 |
27 | const graphQLHttpSettings = async (req: any) => {
28 | const user: IUser | {user: null} = await getUser(req.headers.authorization);
29 | return {
30 | graphql: true,
31 | schema: Schema,
32 | context: {
33 | user,
34 | req
35 | },
36 | formatError: (error: GraphQLError) => {
37 | console.log(error.message);
38 | console.log(error.locations);
39 | console.log(error.stack);
40 | return {
41 | message: error.message,
42 | locations: error.locations,
43 | stack: error.stack
44 | }
45 | }
46 | }
47 | }
48 |
49 | const graphqlServerConfig = graphqlHTTP(graphQLHttpSettings);
50 |
51 | router.all('/graphql', graphqlServerConfig);
52 | router.all('/graphql', koaPlayground({
53 | endpoint: 'graphql',
54 | subscriptionEndpoint: '/subscriptions'
55 | }));
56 |
57 | app.use(router.routes()).use(router.allowedMethods());
58 |
59 |
60 | export default app;
61 |
62 |
63 |
64 | const pubsub = new PubSub();
65 | export { pubsub };
--------------------------------------------------------------------------------
/backend/src/modules/posts/PostType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql';
2 | import { connectionDefinitions, connectionArgs, connectionFromArray, globalIdField } from 'graphql-relay';
3 |
4 | import userType from '../users/UserType';
5 | import { IPost } from './PostModel';
6 | import { CommentConnection } from '../comments/CommentType';
7 | import { commentLoader } from '../comments/CommentLoader';
8 | import { loadUser } from '../users/UserLoader';
9 | import { nodeInterface } from '../../graphql/NodeDefinitions';
10 |
11 | const PostType = new GraphQLObjectType({
12 | name: 'PostType',
13 | description: 'Post type',
14 | fields: () => ({
15 | id: globalIdField('Post'),
16 | author: {
17 | type: userType,
18 | resolve: async (post) => await loadUser(post.author)
19 | },
20 | content: {
21 | type: GraphQLString,
22 | resolve: (post) => post.content
23 | },
24 | likes: {
25 | type: GraphQLInt,
26 | resolve: (post) => post.likes.length
27 | },
28 | userHasLiked: {
29 | type: GraphQLBoolean,
30 | resolve: (post, args, {user}) => post.likes.includes(user.id)
31 | },
32 | createdAt: {
33 | type: GraphQLString,
34 | resolve: (post) => post.createdAt
35 | },
36 | updatedAt: {
37 | type: GraphQLString,
38 | resolve: (post) => post.updatedAt
39 | },
40 | comments: {
41 | type: CommentConnection,
42 | args: connectionArgs,
43 | resolve: (post, args) => {
44 | return connectionFromArray(
45 | post.comments.map(commentLoader),
46 | args
47 | )
48 | }
49 | }
50 | }),
51 | interfaces: [nodeInterface]
52 | });
53 |
54 | export const {connectionType: PostConnection} =
55 | connectionDefinitions({nodeType: PostType});
56 |
57 | export default PostType;
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/Subscriptions.tsx:
--------------------------------------------------------------------------------
1 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
2 |
3 | import {
4 | NewPostsSubscriptionModule,
5 | PostLikeSubscriptionModule,
6 |
7 | NewCommentsSubscriptionModule,
8 | CommentLikeSubscriptionModule,
9 |
10 | NewRepliesSubscriptionModule,
11 | ReplyLikeSubscriptionModule
12 | } from './'
13 |
14 | const SubscriptionModule = (environment: RelayModernEnvironment) => {
15 | const newPostsSubscriptionModule = NewPostsSubscriptionModule(environment);
16 | const postLikeSubscriptionModule = PostLikeSubscriptionModule(environment);
17 |
18 | const newCommentsSubscriptionModule = NewCommentsSubscriptionModule(environment);
19 | const commentLikeSubscriptionModule = CommentLikeSubscriptionModule(environment);
20 |
21 | const newRepliesSubscriptionModule = NewRepliesSubscriptionModule(environment);
22 | const replyLikeSubscriptionModule = ReplyLikeSubscriptionModule(environment);
23 |
24 | const disposeAll = () => {
25 | newPostsSubscriptionModule.dispose()
26 | postLikeSubscriptionModule.dispose()
27 | newCommentsSubscriptionModule.dispose()
28 | commentLikeSubscriptionModule.dispose()
29 | newRepliesSubscriptionModule.dispose()
30 | replyLikeSubscriptionModule.dispose()
31 | }
32 |
33 | const subscribeAll = () => {
34 | newPostsSubscriptionModule.subscribe()
35 | postLikeSubscriptionModule.subscribe()
36 | newCommentsSubscriptionModule.subscribe()
37 | commentLikeSubscriptionModule.subscribe()
38 | newRepliesSubscriptionModule.subscribe()
39 | replyLikeSubscriptionModule.subscribe()
40 | }
41 |
42 | return {
43 | newPostsSubscriptionModule,
44 | postLikeSubscriptionModule,
45 | newCommentsSubscriptionModule,
46 | commentLikeSubscriptionModule,
47 | newRepliesSubscriptionModule,
48 | replyLikeSubscriptionModule,
49 | disposeAll,
50 | subscribeAll
51 | }
52 | };
53 |
54 | export default SubscriptionModule
--------------------------------------------------------------------------------
/backend/src/modules/comments/CommentType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from 'graphql';
2 | import { connectionDefinitions, connectionArgs, connectionFromArray, globalIdField } from 'graphql-relay';
3 |
4 | import userType from '../users/UserType';
5 | import { IComment } from './CommentModel';
6 | import { loadUser } from '../users/UserLoader';
7 | import { ReplyConnection } from '../reply/ReplyType';
8 | import { replyLoader } from '../reply/ReplyLoader';
9 | import { nodeInterface } from '../../graphql/NodeDefinitions';
10 |
11 | const CommentType = new GraphQLObjectType({
12 | name: 'CommentType',
13 | description: 'Comment type',
14 | fields: () => ({
15 | id: globalIdField('Comment'),
16 | author: {
17 | type: userType,
18 | resolve: (comment) => loadUser(comment.author)
19 | },
20 | content: {
21 | type: GraphQLString,
22 | resolve: (comment) => comment.content
23 | },
24 | likes: {
25 | type: GraphQLInt,
26 | resolve: (comment) => comment.likes.length
27 | },
28 | userHasLiked: {
29 | type: GraphQLBoolean,
30 | resolve: (comment, args, {user}) => comment.likes.includes(user.id)
31 | },
32 | createdAt: {
33 | type: GraphQLString,
34 | resolve: (comment) => comment.createdAt
35 | },
36 | updatedAt: {
37 | type: GraphQLString,
38 | resolve: (comment) => comment.updatedAt
39 | },
40 | replies: {
41 | type: ReplyConnection,
42 | args: connectionArgs,
43 | resolve: (comment, args) => {
44 | return connectionFromArray(
45 | comment.replies.map(replyLoader),
46 | args
47 | )
48 | }
49 | }
50 | }),
51 | interfaces: [nodeInterface]
52 | });
53 |
54 | export const {connectionType: CommentConnection} =
55 | connectionDefinitions({nodeType: CommentType});
56 |
57 | export default CommentType
--------------------------------------------------------------------------------
/frontend/src/Pages/LoginPage/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useHistory } from 'react-router';
3 | import graphql from 'babel-plugin-relay/macro';
4 | import { useMutation } from 'react-relay/lib/relay-experimental';
5 |
6 | import { LoginForm } from './Components';
7 |
8 | const loginMutation = graphql`
9 | mutation LoginPageLoginMutation($email: String!, $password: String!) {
10 | Login(input: {email: $email, password: $password, clientMutationId: "1"}) {
11 | user {
12 | name
13 | token
14 | }
15 | }
16 | }
17 | `;
18 |
19 | const LoginPage = ({setUserIsLogged}: {
20 | setUserIsLogged: (userIsLogged: boolean) => void
21 | }) => {
22 | const history = useHistory();
23 |
24 | const [commit, isInFlight] = useMutation(loginMutation);
25 |
26 | let email = '';
27 | let password = '';
28 |
29 | const handleSubmit = (event: React.FormEvent) => {
30 | event.preventDefault();
31 | setUserIsLogged(true);
32 |
33 | const variables = {
34 | email,
35 | password
36 | }
37 | commit({
38 | variables,
39 | onCompleted: (data: any) => {
40 |
41 | console.log('data: ', data);
42 |
43 | if (data?.Login?.user?.token) {
44 |
45 | localStorage.setItem('authToken', data.Login.user.token);
46 | history.push('/');
47 | }
48 |
49 | setUserIsLogged(data?.Login?.user?.token ? true : false);
50 |
51 | }
52 | })
53 | }
54 | if (isInFlight) {
55 | return(
56 |
57 | Loading...
58 |
59 | );
60 | }
61 | return (
62 |
63 | {email = emailReturned}}
65 | passwordChange={(passwordReturned) => {password = passwordReturned}}
66 | formSubmit={handleSubmit}
67 | />
68 |
69 | );
70 | }
71 |
72 | export default LoginPage;
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "socialnetworkbackend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "dist/server",
6 | "scripts": {
7 | "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
8 | "babel": "babel-node --extensions \".es6,.js,.es,.jsx,.mjs,.ts,.tsx\"",
9 | "build": "npm run clear && babel src --extensions \".es6,.js,.es,.jsx,.mjs,.ts\" --ignore *.spec.js --out-dir dist --copy-files",
10 | "clear": "rimraf ./dist",
11 | "prestart": "npm run build",
12 | "start": "nodemon .",
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "author": "",
16 | "license": "ISC",
17 | "devDependencies": {
18 | "@babel/cli": "^7.8.4",
19 | "@babel/core": "^7.9.0",
20 | "@babel/node": "^7.8.7",
21 | "@babel/plugin-transform-runtime": "^7.10.5",
22 | "@babel/preset-env": "^7.9.0",
23 | "@babel/preset-typescript": "^7.10.4",
24 | "@types/bcrypt": "^3.0.0",
25 | "@types/graphql-relay": "^0.4.11",
26 | "@types/ioredis": "^4.16.2",
27 | "@types/jsonwebtoken": "^8.3.8",
28 | "@types/kcors": "^2.2.3",
29 | "@types/koa": "^2.11.3",
30 | "@types/koa-bodyparser": "^4.3.0",
31 | "@types/koa-graphql": "^0.8.3",
32 | "@types/koa-logger": "^3.1.1",
33 | "@types/koa-router": "^7.4.1",
34 | "@types/mongoose": "^5.7.8",
35 | "typescript": "^3.8.3"
36 | },
37 | "dependencies": {
38 | "bcrypt": "^5.0.0",
39 | "dataloader": "^2.0.0",
40 | "dotenv": "^8.2.0",
41 | "graphql": "^15.7.2",
42 | "graphql-playground-middleware-koa": "^1.6.13",
43 | "graphql-redis-subscriptions": "^2.6.1",
44 | "graphql-relay": "^0.9.0",
45 | "graphql-relay-subscription": "^0.2.1",
46 | "graphql-subscriptions": "^2.0.0",
47 | "ioredis": "^4.17.1",
48 | "jsonwebtoken": "^8.5.1",
49 | "kcors": "^2.2.2",
50 | "koa": "^2.12.0",
51 | "koa-graphql": "^0.12.0",
52 | "koa-logger": "^3.2.1",
53 | "koa-router": "^8.0.8",
54 | "mongoose": "^5.9.7",
55 | "nodemon": "^2.0.2",
56 | "redis": "^3.0.2",
57 | "rimraf": "^3.0.2",
58 | "subscriptions-transport-ws": "^0.11.0",
59 | "tslint": "^6.1.1"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "social-network",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "test": "react-scripts test",
7 | "eject": "react-scripts eject",
8 | "tailwind:css": "tailwind build src/tailwind.css -c tailwind.config.js -o src/index.css",
9 | "start": "npm run relay && npm run tailwind:css && react-scripts start",
10 | "build": "npm run relay && react-scripts build",
11 | "relay": "npx relay-compiler --schema ./schema/schema.graphql --src ./src/ --watchman false $@ --language typescript"
12 | },
13 | "eslintConfig": {
14 | "extends": "react-app"
15 | },
16 | "browserslist": {
17 | "production": [
18 | ">0.2%",
19 | "not dead",
20 | "not op_mini all"
21 | ],
22 | "development": [
23 | "last 1 chrome version",
24 | "last 1 firefox version",
25 | "last 1 safari version"
26 | ]
27 | },
28 | "engines": {
29 | "node": "=16"
30 | },
31 | "dependencies": {
32 | "@fortawesome/fontawesome-svg-core": "^1.2.28",
33 | "@fortawesome/free-regular-svg-icons": "^5.13.0",
34 | "@fortawesome/react-fontawesome": "^0.1.9",
35 | "@testing-library/jest-dom": "^4.2.4",
36 | "@testing-library/react": "^9.5.0",
37 | "@testing-library/user-event": "^7.2.1",
38 | "babel-plugin-relay": "^9.0.0",
39 | "react": "0.0.0-experimental-33c3af284",
40 | "react-dom": "0.0.0-experimental-33c3af284",
41 | "react-relay": "0.0.0-experimental-8cc94ddc",
42 | "react-router": "5.2.0",
43 | "react-router-dom": "^5.2.0",
44 | "react-scripts": "3.4.1",
45 | "relay-runtime": "^9.0.0",
46 | "subscriptions-transport-ws": "^0.9.16",
47 | "typescript": "^3.7.5"
48 | },
49 | "devDependencies": {
50 | "@types/jest": "^24.9.1",
51 | "@types/node": "^12.12.36",
52 | "@types/react": "^16.9.35",
53 | "@types/react-dom": "^16.9.8",
54 | "@types/react-relay": "^7.0.7",
55 | "@types/react-router-dom": "^5.1.5",
56 | "@types/relay-runtime": "^8.0.8",
57 | "autoprefixer": "^9.7.6",
58 | "graphql": "^14",
59 | "postcss-cli": "^7.1.0",
60 | "relay-compiler": "^9.0.0",
61 | "relay-compiler-language-typescript": "^12.0.1",
62 | "tailwindcss": "^1.2.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Comments/Comments.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { useFragment, usePaginationFragment } from 'react-relay/hooks';
3 | import graphql from 'babel-plugin-relay/macro';
4 | import { Comment } from './'
5 |
6 | const commentsTypeFragment = graphql`
7 | fragment CommentsTypeFragment on PostType @argumentDefinitions(
8 | first: {type: "Int", defaultValue: 4},
9 | last: {type: "Int"},
10 | before: {type: "String"},
11 | after: {type: "String"}
12 | ) @refetchable(queryName: "CommentsListPagination") {
13 | comments (
14 | first: $first
15 | last: $last
16 | before: $before
17 | after: $after
18 | ) @connection(key: "CommentsTypeFragment_comments") {
19 | edges {
20 | ...CommentTypeFragment
21 | }
22 | pageInfo {
23 | startCursor
24 | endCursor
25 | hasNextPage
26 | hasPreviousPage
27 | }
28 | }
29 | }
30 | `;
31 |
32 | const Comments = ({comments}: {
33 | comments: any
34 | }) => {
35 | const {
36 | data,
37 | loadNext,
38 | loadPrevious,
39 | hasNext,
40 | hasPrevious,
41 | isLoadingNext,
42 | isLoadingPrevious,
43 | refetch // For refetching connection
44 | } = usePaginationFragment(commentsTypeFragment, comments);
45 |
46 | return (
47 |
48 | {
49 | data && data.comments && data.comments.edges.length > 0 ?
50 | data.comments.edges.map((edge: any, index: number) => {
51 | return (
52 |
53 |
54 |
55 |
56 |
57 | )
58 | }):
59 | null
60 | }
61 | {
62 | hasNext && data && data.comments && data.comments.edges.length > 0 ?
63 |
:
66 | null
67 | }
68 |
69 | );
70 | };
71 |
72 | export default Comments;
--------------------------------------------------------------------------------
/backend/src/modules/users/UserType.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql';
2 |
3 | import { IUser } from './UserModel';
4 | import { loadUser, userIdLoader } from './UserLoader';
5 | import { connectionDefinitions, connectionArgs, connectionFromArray } from 'graphql-relay';
6 | import PostType, { PostConnection } from '../posts/PostType';
7 |
8 |
9 | const userType = new GraphQLObjectType({
10 | name: 'UserType',
11 | description: 'User type',
12 | fields: () => (
13 | {
14 | name: {
15 | type: GraphQLString,
16 | resolve: (user, _) => {
17 | return user.name
18 | }
19 | },
20 | password: {
21 | type: GraphQLString,
22 | resolve: (user, _) => user.password
23 | },
24 | email: {
25 | type: GraphQLString,
26 | resolve: (user, _) => user.email
27 | },
28 | createdAt: {
29 | type: GraphQLString,
30 | resolve: (user) => user.createdAt
31 | },
32 | updatedAt: {
33 | type: GraphQLString,
34 | resolve: (user) => user.updatedAt
35 | },
36 | token: {
37 | type: GraphQLString,
38 | resolve: (user, _) => user.tokens[0].token
39 | },
40 | friends: {
41 | type: UserConnection,
42 | args: connectionArgs,
43 | resolve: (user, args) => {
44 | return connectionFromArray(
45 | user.friends.map(id => loadUser(id)),
46 | args
47 | )
48 | }
49 | },
50 | posts: {
51 | type: PostConnection,
52 | args: connectionArgs,
53 | resolve: (user, args) => {
54 | return connectionFromArray(
55 | user.posts.map(id => loadUser(id)),
56 | args
57 | )
58 | }
59 | },
60 | _id: {
61 | type: GraphQLString,
62 | resolve: (user, _) => userIdLoader(user, '_id')
63 | }
64 | }
65 | )
66 | });
67 |
68 | const {connectionType: UserConnection} =
69 | connectionDefinitions({nodeType: userType});
70 |
71 | export default userType;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Replies/Replies.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import graphql from 'babel-plugin-relay/macro';
3 |
4 | import { Reply, ReplyCreation } from './';
5 | import { useFragment, usePaginationFragment } from 'react-relay/hooks';
6 |
7 | const repliesTypeFragment = graphql`
8 | fragment RepliesTypeFragment on CommentType @argumentDefinitions(
9 | first: {type: "Int", defaultValue: 1}
10 | last: {type: "Int"},
11 | before: {type: "String"},
12 | after: {type: "String"}
13 | ) @refetchable(queryName: "RepliesListPagination"){
14 | replies(
15 | first: $first,
16 | last: $last,
17 | before: $before,
18 | after: $after
19 | ) @connection(key: "RepliesTypeFragment_replies") {
20 | edges {
21 | ...ReplyTypeFragment
22 | }
23 | pageInfo {
24 | startCursor
25 | endCursor
26 | hasNextPage
27 | hasPreviousPage
28 | }
29 | }
30 | }
31 | `;
32 |
33 | const Replies = ({replies}: any) => {
34 | const {
35 | data,
36 | hasNext,
37 | loadNext,
38 | hasPrevious,
39 | loadPrevious,
40 | isLoadingNext,
41 | isLoadingPrevious,
42 | refetch
43 | } = usePaginationFragment(repliesTypeFragment, replies);
44 |
45 | return (
46 |
47 | {
48 | data.replies && data.replies.edges && data.replies.edges.length > 0 ?
49 | data.replies.edges.map((edge: any, index: number) => (
50 |
51 |
52 |
53 | )) :
54 | null
55 | }
56 |
57 | {
58 | hasNext ?
59 | data.replies && data.replies.edges && data.replies.edges.length <= 0 ?
60 | :
63 | :
66 | null
67 | }
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default Replies;
--------------------------------------------------------------------------------
/frontend/src/Components/Layout/Header/Navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const Navbar = ({userIsLogged, handleLogoutLogin}: {
5 | userIsLogged: boolean,
6 | handleLogoutLogin: () => void
7 | }) => {
8 |
9 | return (
10 |
46 | )
47 | };
48 |
49 | export default Navbar;
--------------------------------------------------------------------------------
/frontend/src/Pages/LoginPage/Components/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import React, {unstable_useTransition as useTransition} from 'react';
2 |
3 | const LoginForm = ({emailChange, passwordChange, formSubmit}:
4 | {
5 | emailChange: (email: string) => void,
6 | passwordChange: (password: string) => void,
7 | formSubmit: (event: React.FormEvent) => void
8 | }
9 | ) => {
10 |
11 | const [startTransition, isPending] = useTransition({
12 | timeoutMs: 10000
13 | });
14 |
15 | const handleSubmit = (event: React.FormEvent) => {
16 | startTransition(() => {
17 | formSubmit(event);
18 | })
19 | };
20 |
21 | return (
22 |
23 | {isPending ? 'Loading...' : null}
24 |
48 |
49 | )
50 | };
51 |
52 | export default LoginForm
--------------------------------------------------------------------------------
/frontend/src/relay/fetchGraphQL.tsx:
--------------------------------------------------------------------------------
1 | import { RequestParameters } from 'relay-runtime/lib/util/RelayConcreteNode';
2 | import { Variables, Disposable } from 'relay-runtime/lib/util/RelayRuntimeTypes';
3 | import { SubscriptionClient, Observer } from 'subscriptions-transport-ws';
4 |
5 | import config from '../config';
6 | import { ExecutionResult } from 'graphql';
7 | import { Observable, SubscribeFunction, Subscribable, GraphQLResponse } from 'relay-runtime';
8 | import { RelayObservable } from 'relay-runtime/lib/network/RelayObservable';
9 |
10 | export function getToken(): string {
11 | const currentToken = localStorage.getItem('authToken');
12 | return currentToken ? currentToken : '';
13 | }
14 | // your-app-name/src/fetchGraphQL.js
15 | async function fetchGraphQL(request: RequestParameters, variables: Variables) {
16 | const loggedUser = Promise.resolve().then(() => getToken());
17 |
18 | // Fetch data from GitHub's GraphQL API:
19 | const response = await fetch(config.GRAPHQL_URL as string, {
20 | method: 'POST',
21 | headers: {
22 | Accept: 'application/json',
23 | 'Content-type': 'application/json',
24 | Authorization: await loggedUser
25 | },
26 | body: JSON.stringify({
27 | query: request.text,
28 | variables,
29 | }),
30 | });
31 |
32 | // Get the response as JSON
33 | return await response.json();
34 | }
35 |
36 |
37 | const setupSubscription: SubscribeFunction = (operation, variables, cacheConfig) => {
38 |
39 |
40 | const subscriptionClient = new SubscriptionClient(
41 | 'ws://localhost:3333/subscriptions',
42 | {
43 | reconnect: true,
44 | connectionParams: {
45 | authorization: localStorage.getItem('authToken')
46 | },
47 | reconnectionAttempts: 0
48 | },
49 | );
50 |
51 | const query = operation.text;
52 | const client = subscriptionClient.request({ query: query!, variables });
53 | let subscription: any;
54 | const subscribable = {
55 | subscribe: (observer: Observer) => {
56 | if (!subscription) {
57 | subscription = client.subscribe({
58 | next: result => {
59 | if (observer.next) observer.next({ data: result.data });
60 | },
61 | complete: () => {
62 | if (observer.complete) observer.complete();
63 | },
64 | error: error => {
65 | if (observer.error) observer.error(error);
66 | }
67 | });
68 | }
69 | return {
70 | unsubscribe: () => {
71 | if (subscription) {
72 | subscriptionClient.close();
73 | subscription.unsubscribe();
74 | }
75 | }
76 | }
77 | }
78 | };
79 |
80 | return (Observable.from(subscribable as Subscribable) as RelayObservable | Disposable);
81 | }
82 |
83 | export { fetchGraphQL, setupSubscription };
84 |
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/NewPostsSubscription.tsx:
--------------------------------------------------------------------------------
1 | import graphql from "babel-plugin-relay/macro";
2 | import { GraphQLSubscriptionConfig, RecordProxy, ConnectionHandler, ROOT_ID, Disposable } from "relay-runtime";
3 | import { requestSubscription } from "react-relay";
4 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
5 |
6 | const postTypeSubscription = graphql`
7 | subscription NewPostsSubscription ($clientSubscriptionId: String!) {
8 | PostCreationSubscription(input: {clientSubscriptionId: $clientSubscriptionId}) {
9 | post {
10 | id
11 | author {
12 | name
13 | }
14 | content
15 | likes
16 | userHasLiked
17 | createdAt
18 | updatedAt
19 | ...CommentsTypeFragment
20 | }
21 | }
22 | }
23 | `;
24 |
25 | const PostCreationSubscriptionModule = (environment: RelayModernEnvironment): {
26 | dispose: () => void,
27 | subscribe: () => void
28 | } => {
29 |
30 | let config: GraphQLSubscriptionConfig<{}>;
31 | let objDisposable: Disposable;
32 |
33 | const dispose = () => {
34 | if (objDisposable) objDisposable.dispose();
35 | };
36 |
37 | const subscribe = () => {
38 | objDisposable = requestSubscription(
39 | environment,
40 | generatePostRequestSubscriptionConfig()
41 | );
42 | }
43 |
44 | const generatePostRequestSubscriptionConfig = (): GraphQLSubscriptionConfig<{}> => {
45 | return {
46 | subscription: postTypeSubscription,
47 | variables: {clientSubscriptionId: localStorage.getItem('authToken')+'1'},
48 | onNext: (response: any) => {
49 | console.log('response ws: ', response)
50 | },
51 | updater: store => {
52 | const postNode = (store.getRootField('PostCreationSubscription') as RecordProxy<{}>).getLinkedRecord('post') as RecordProxy<{}>;
53 |
54 | const conn = ConnectionHandler.getConnection(store.getRoot() as RecordProxy<{}>, 'PostsTypeFragment_myPosts') as RecordProxy<{}>;
55 |
56 | const myPosts = (store.get(ROOT_ID) as RecordProxy<{}>).getLinkedRecord('myPosts') as RecordProxy<{}>;
57 |
58 | let postEdge = null;
59 | if (store && (conn || myPosts) && postNode) {
60 | postEdge = ConnectionHandler.createEdge(store, conn, postNode, 'PostTypeEdge');
61 | }
62 | if (!conn) {
63 | // eslint-disable-next-line
64 | console.log('maybe this connection is not in relay store: ', 'PostsTypeFragment');
65 | return;
66 | }
67 | if (postEdge) {
68 | ConnectionHandler.insertEdgeBefore((conn ? conn : myPosts), postEdge);
69 | }
70 | }
71 | }
72 | }
73 |
74 | return {
75 | dispose,
76 | subscribe
77 | }
78 | }
79 |
80 | export default PostCreationSubscriptionModule
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Replies/Reply.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useMutation } from 'react-relay/lib/relay-experimental';
4 | import graphql from 'babel-plugin-relay/macro';
5 | import { useFragment } from 'react-relay/hooks';
6 |
7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
8 | import { faThumbsUp } from '@fortawesome/free-regular-svg-icons'
9 |
10 | const replyTypeFragment = graphql`
11 | fragment ReplyTypeFragment on ReplyTypeEdge {
12 | cursor
13 | node {
14 | id
15 | author {
16 | name
17 | }
18 | content
19 | likes
20 | userHasLiked
21 | createdAt
22 | updatedAt
23 | }
24 | }
25 | `;
26 |
27 | const replyLikeMutation = graphql`
28 | mutation ReplyLikeMutation($reply: String!) {
29 | LikeReply(input: {reply: $reply, clientMutationId: "7"}) {
30 | reply {
31 | likes
32 | userHasLiked
33 | }
34 | }
35 | }
36 | `;
37 |
38 | const Reply = ({reply}: any) => {
39 | const replyFragmentReturn = useFragment(replyTypeFragment, reply);
40 | const [likes, setLikes] = useState(replyFragmentReturn.node.likes);
41 | const [hasLiked, setHasLiked] = useState(replyFragmentReturn.node.userHasLiked);
42 |
43 | const [commitLikeMut, likeMutIsInFlight] = useMutation(replyLikeMutation);
44 |
45 | const handleLike = () => {
46 | setLikes(hasLiked ? likes - 1 : likes + 1);
47 | setHasLiked(hasLiked ? false : true);
48 |
49 | const variables = {
50 | reply: replyFragmentReturn.node.id
51 | }
52 |
53 | commitLikeMut({
54 | variables,
55 | onCompleted: ({LikeReply}: any) => {
56 | setLikes(LikeReply.reply.likes);
57 | setHasLiked(LikeReply.reply.userHasLiked);
58 | console.log(LikeReply);
59 | }
60 | })
61 | }
62 |
63 | return (
64 |
65 |
66 |
67 |
68 | {replyFragmentReturn.node.author.name}
69 |
70 |
71 |
72 | {replyFragmentReturn.node.content}
73 |
74 |
75 |
76 |
77 | {likeMutIsInFlight ? likes : replyFragmentReturn.node.likes}
78 |
79 |
80 | {
81 | (likeMutIsInFlight ? hasLiked : replyFragmentReturn.node.userHasLiked) ?
82 | <>Liked> :
83 | <>Like>
84 | }
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default Reply;
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/NewCommentsSubscription.tsx:
--------------------------------------------------------------------------------
1 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
2 | import graphql from "babel-plugin-relay/macro";
3 | import { Disposable, requestSubscription } from "react-relay";
4 | import { GraphQLSubscriptionConfig, RecordProxy, ConnectionHandler, ROOT_ID } from "relay-runtime";
5 |
6 | const commentsCreationSubscription = graphql`
7 | subscription NewCommentsSubscription($clientSubscriptionId: String) {
8 | CreateCommentSubscription(input :{clientSubscriptionId: $clientSubscriptionId}) {
9 | comment {
10 | id
11 | author {
12 | name
13 | }
14 | content
15 | likes
16 | userHasLiked
17 | createdAt
18 | updatedAt
19 | ...RepliesTypeFragment
20 | }
21 | post {
22 | id
23 | }
24 | }
25 | }
26 | `;
27 |
28 | const NewCommentsSubscription = (environment: RelayModernEnvironment) => {
29 | let objDisposable: Disposable;
30 |
31 | const dispose = () => {
32 | if (objDisposable) objDisposable.dispose()
33 | };
34 |
35 | const subscribe = () => {
36 | objDisposable = requestSubscription(
37 | environment,
38 | generateSubscriptionConfigs()
39 | )
40 | };
41 |
42 | const generateSubscriptionConfigs = (): GraphQLSubscriptionConfig<{}> => {
43 | return {
44 | subscription: commentsCreationSubscription,
45 | variables: {clientSubscriptionId: localStorage.getItem('authToken') + '3'},
46 | onNext: (response: any) => {
47 | console.log('response ws comment!: ', response)
48 | },
49 | updater: store => {
50 | const commentNode = (store.getRootField('CreateCommentSubscription') as RecordProxy<{}>).getLinkedRecord('comment') as RecordProxy<{}>;
51 | const post = (store.getRootField('CreateCommentSubscription') as RecordProxy<{}>).getLinkedRecord('post') as RecordProxy<{}>;
52 | const conn = ConnectionHandler.getConnection(post, 'CommentsTypeFragment_comments') as RecordProxy<{}>;
53 | const hasNextPage = (conn.getLinkedRecord('pageInfo') as RecordProxy<{}>).getValue('hasNextPage');
54 | let commentEdge = null;
55 | if (store && conn && commentNode) {
56 | commentEdge = ConnectionHandler.createEdge(store, conn, commentNode, 'CommentTypeEdge');
57 | //commentEdge.setValue
58 | }
59 | if (!conn) {
60 | // eslint-disable-next-line
61 | console.log('maybe this connection is not in relay store: ', 'CommentsTypeFragment_comments');
62 | return;
63 | }
64 | if (commentEdge && !hasNextPage) {
65 | ConnectionHandler.insertEdgeAfter(conn, commentEdge);
66 | }
67 | }
68 | }
69 | };
70 |
71 | return {
72 | dispose,
73 | subscribe
74 | }
75 | };
76 |
77 | export default NewCommentsSubscription
--------------------------------------------------------------------------------
/backend/src/modules/users/UserModel.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from 'mongoose';
2 | import bcrypt from 'bcrypt';
3 | import jsonwebtoken from 'jsonwebtoken';
4 |
5 |
6 | export interface IUser extends mongoose.Document {
7 | name: string;
8 | password: string;
9 | email: string;
10 | createdAt: Date;
11 | updatedAt: Date;
12 | tokens:[{token: string}];
13 | friends: string[];
14 | posts: string[];
15 | generateAuthToken(): string;
16 | verifyAuthToken(): void;
17 | }
18 |
19 | export interface IUserModel extends mongoose.Model{
20 | findByCredentials(email: string, password: string): IUser;
21 | findByToken(token: string): IUser
22 | }
23 |
24 | const userSchema = new mongoose.Schema({
25 | name: {
26 | type: String,
27 | required: true,
28 | trim: true
29 | },
30 | password: {
31 | type: String,
32 | required: true,
33 | minlength: 7
34 | },
35 | email: {
36 | type: String,
37 | required: true,
38 | unique: true,
39 | lowercase: true
40 | },
41 | tokens: [{
42 | token: {
43 | type: String,
44 | required: false
45 | }
46 | }],
47 | friends: [{
48 | type: Schema.Types.ObjectId,
49 | ref: 'User'
50 | }],
51 | posts: [{
52 | type: Schema.Types.ObjectId,
53 | ref: 'Post'
54 | }]
55 | }, {
56 | timestamps: true
57 | });
58 |
59 | userSchema.pre('save', async function (next) {
60 | if (this.isModified('password')) {
61 | this.password = await bcrypt.hash(this.password, 8);
62 | }
63 | next();
64 | });
65 |
66 | userSchema.methods.generateAuthToken = async function() {
67 | const token = jsonwebtoken.sign({_id: this._id}, process.env.JWT_KEY, {expiresIn: 60 * 30});
68 | this.tokens = [{token}].concat(this.tokens);
69 | this.save();
70 | return token;
71 | }
72 |
73 | userSchema.methods.verifyAuthToken = function(callbackSuccess?: () => {}, callbackError?: (error: any) => {}) {
74 | const actualToken = this.tokens[0].token;
75 | try {
76 | jsonwebtoken.verify(actualToken, process.env.JWT_KEY);
77 | if (callbackSuccess) {
78 | callbackSuccess();
79 | }
80 | } catch (err) {
81 | if (callbackError) {
82 | callbackError(err);
83 | }
84 | }
85 | }
86 |
87 | userSchema.statics.findByCredentials = async (email: string, password: string) => {
88 | const user = await User.findOne({email});
89 | if (!user) {
90 | throw new Error('Invalid login credentials');
91 | }
92 | const isPasswordMatch = await bcrypt.compare(password, user.password);
93 | if (!isPasswordMatch) {
94 | throw new Error('Invalid password');
95 | }
96 |
97 | return user;
98 | }
99 |
100 | userSchema.statics.findByToken = async (token: string) => {
101 | const jsonPayload: any = jsonwebtoken.decode(token);
102 | const user = await User.findOne({_id: jsonPayload._id});
103 |
104 | return user;
105 | }
106 |
107 | const User = mongoose.model('User_SocialNetwork', userSchema);
108 |
109 |
110 | export default User;
--------------------------------------------------------------------------------
/frontend/src/Services/Subscriptions/NewRepliesSubscription.tsx:
--------------------------------------------------------------------------------
1 | import { RelayModernEnvironment } from "relay-runtime/lib/store/RelayModernEnvironment";
2 | import { Disposable, requestSubscription } from "react-relay";
3 | import graphql from "babel-plugin-relay/macro";
4 | import { GraphQLSubscriptionConfig, RecordProxy, ConnectionHandler } from "relay-runtime";
5 |
6 | const newRepliesSubscription = graphql`
7 | subscription NewRepliesSubscription($clientSubscriptionId: String) {
8 | ReplyCreationSubscription(input: {clientSubscriptionId: $clientSubscriptionId}) {
9 | reply {
10 | id
11 | author {
12 | name
13 | }
14 | content
15 | likes
16 | userHasLiked
17 | createdAt
18 | updatedAt
19 | }
20 | comment {
21 | id
22 | }
23 | }
24 | }
25 | `;
26 |
27 | const newRepliesSubscriptionModule = (environment: RelayModernEnvironment) => {
28 | let objDisposable: Disposable;
29 |
30 | const dispose = () => {
31 | if (objDisposable) objDisposable.dispose();
32 | };
33 |
34 | const subscribe = () => {
35 | objDisposable = requestSubscription(
36 | environment,
37 | generateSubscriptionConfigs()
38 | )
39 | };
40 |
41 | const generateSubscriptionConfigs = (): GraphQLSubscriptionConfig<{}> => {
42 | return {
43 | subscription: newRepliesSubscription,
44 | variables: {
45 | clientSubscriptionId: localStorage.getItem('authToken') + '5'
46 | },
47 | onNext: (response: any) => {
48 | console.log('response like subscription: ', response)
49 | },
50 | updater: store => {
51 | const replyNode = (store.getRootField('ReplyCreationSubscription') as RecordProxy<{}>).getLinkedRecord('reply');
52 |
53 | const commentNode = (store.getRootField('ReplyCreationSubscription') as RecordProxy<{}>).getLinkedRecord('comment') as RecordProxy<{}>;
54 |
55 | const repliesConnection = ConnectionHandler.getConnection(commentNode, 'RepliesTypeFragment_replies') as RecordProxy<{}>;
56 |
57 | const hasPreviousPage = (repliesConnection.getLinkedRecord('pageInfo') as RecordProxy<{}>).getValue('hasPreviousPage');
58 |
59 | let replyEdge;
60 | if(store && repliesConnection && replyNode) {
61 | replyEdge = ConnectionHandler.createEdge(store, repliesConnection, replyNode, 'ReplyTypeEdge');
62 | }
63 | if (!repliesConnection) {
64 | // eslint-disable-next-line
65 | console.log('maybe this connection is not in relay store: ', 'RepliesTypeFragment_replies');
66 | return;
67 | }
68 | if (replyEdge && !hasPreviousPage) {
69 | ConnectionHandler.insertEdgeBefore(repliesConnection, replyEdge);
70 | }
71 | }
72 | }
73 | };
74 |
75 | return {
76 | subscribe,
77 | dispose
78 | }
79 |
80 | };
81 |
82 | export default newRepliesSubscriptionModule
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Posts/Posts.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useState } from 'react';
2 |
3 | import { usePaginationFragment } from 'react-relay/lib/relay-experimental';
4 | import graphql from 'babel-plugin-relay/macro';
5 |
6 | import Post from './Post';
7 |
8 |
9 |
10 | const postsTypeFragment = graphql`
11 | fragment PostsTypeFragment on Query
12 | @argumentDefinitions(
13 | first: {type: "Int", defaultValue: 2},
14 | last: {type: "Int"},
15 | before: {type: "String"},
16 | after: {type: "String"}
17 | )
18 | @refetchable(queryName: "PostListPagination") {
19 | myPosts (
20 | first: $first,
21 | last: $last,
22 | before: $before,
23 | after: $after,
24 | ) @connection(key: "PostsTypeFragment_myPosts"){
25 | edges {
26 | cursor
27 | node {
28 | ...PostTypeFragment
29 | }
30 | }
31 | pageInfo {
32 | startCursor
33 | endCursor
34 | hasNextPage
35 | hasPreviousPage
36 | }
37 | }
38 | }`
39 |
40 | const Posts = ({posts}: any) => {
41 | const [isPaginating, setIsPaginating] = useState(false);
42 |
43 | const {
44 | data,
45 | loadNext,
46 | loadPrevious,
47 | hasNext,
48 | hasPrevious,
49 | isLoadingNext,
50 | isLoadingPrevious,
51 | refetch // For refetching connection
52 | }: any = usePaginationFragment(postsTypeFragment,
53 | posts);
54 |
55 | useEffect(() => {
56 | window.addEventListener('scroll', handleScroll);
57 | return () => window.removeEventListener('scroll', handleScroll);
58 | }, [loadNext]);
59 |
60 | const handleScroll = () => {
61 | if (window.innerHeight + document.documentElement.scrollTop !== document.documentElement.offsetHeight) return;
62 | console.log('isLoadingNext: ', isLoadingNext);
63 | if (hasNext && !isLoadingNext && !isPaginating) {
64 | setIsPaginating(true);
65 | handleLoadNext();
66 | };
67 | }
68 |
69 | const handleLoadNext = () => {
70 | loadNext(2, {onComplete: () => {
71 | console.log('completed load next');
72 | setIsPaginating(false);
73 | }});
74 | }
75 |
76 | return (
77 |
78 | {
79 | data && data.myPosts && data.myPosts.edges && data.myPosts.edges.length > 0 ?
80 | data.myPosts.edges.filter((postEdge: any) => !!postEdge.node).map((postEdge: any, index: number) => {
81 | return (
82 |
83 |
86 |
87 | )
88 | }) :
89 | null
90 | }
91 | {
92 | hasNext ?
93 |
Loading...
:
94 |
No more posts to load
95 | }
96 |
97 | );
98 | };
99 |
100 | export default Posts;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/FeedPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect } from 'react';
2 | import { useLazyLoadQuery } from 'react-relay/hooks';
3 |
4 | import {Posts, PostCreation} from './Components'
5 | import graphql from 'babel-plugin-relay/macro';
6 | import { useMutation } from 'react-relay/lib/relay-experimental';
7 | import { useHistory } from 'react-router';
8 | import environment from 'src/relay/environment';
9 | import SubscriptionModule from 'src/Services/Subscriptions';
10 |
11 |
12 | const postCreationMutation = graphql`
13 | mutation FeedPagePostCreationMutation($content: String!) {
14 | PostCreation(input: {content: $content, clientMutationId: "1"}) {
15 | post {
16 | author {
17 | name
18 | }
19 | content
20 | likes
21 | }
22 | }
23 | }
24 | `;
25 |
26 |
27 | const FeedPage = ({userIsLogged} : {
28 | userIsLogged: boolean
29 | }) => {
30 | useEffect(() => {
31 | const subscriptionModule = SubscriptionModule(environment);
32 | subscriptionModule.subscribeAll();
33 | return () => {
34 | subscriptionModule.disposeAll();
35 | }
36 | }, [environment]);
37 |
38 | const history = useHistory();
39 | useEffect(() => {
40 | if (userIsLogged) {
41 | return;
42 | }
43 | history.push('/login');
44 | }, [userIsLogged]);
45 |
46 | const [commit, isInFlight] = useMutation(postCreationMutation);
47 | const userPostsQuery: any = useLazyLoadQuery(graphql`
48 | query FeedPageMyselfQuery {
49 | ...PostsTypeFragment
50 | }`,
51 | {},
52 | {fetchPolicy: 'store-or-network'}
53 | );
54 |
55 | let content = "";
56 | const handlePostFormCreationSubmit = (event: React.FormEvent) => {
57 | event.preventDefault();
58 | const variables = {
59 | content
60 | }
61 | commit({
62 | variables,
63 | onCompleted: (data: any) => {
64 | }
65 | });
66 | }
67 |
68 | return (
69 | {
70 | return (
71 | "is Loading"
72 | )
73 | }}>
74 |
75 |
76 |
77 | {
78 | isInFlight ? 'Loading' : null
79 | }
80 |
{content = postContent}} formSubmit={handlePostFormCreationSubmit}/>
81 |
82 |
83 |
{
84 | return (
85 | "is Loading"
86 | )
87 | }}>
88 | {
89 | userPostsQuery && userPostsQuery ?
90 | :
91 | null
92 | }
93 |
94 |
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export default FeedPage;
--------------------------------------------------------------------------------
/frontend/src/Pages/RegisterPage/Components/RegisterForm.tsx:
--------------------------------------------------------------------------------
1 | import React, {unstable_useTransition as useTransition} from 'react';
2 |
3 | const RegisterForm = ({emailChange, passwordChange, nameChange, formSubmit}:
4 | {
5 | emailChange: (email: string) => void,
6 | passwordChange: (password: string) => void,
7 | nameChange: (name: string) => void,
8 | formSubmit: (event: React.FormEvent) => void
9 | }
10 | ) => {
11 |
12 | const [startTransition, isPending] = useTransition({
13 | timeoutMs: 10000
14 | });
15 |
16 | const handleFormSubmit = (event: React.FormEvent) => {
17 | startTransition(() => {
18 | formSubmit(event);
19 | })
20 | };
21 |
22 | return (
23 |
24 | {isPending ? 'Loading...' : null}
25 |
55 |
56 | )
57 | };
58 |
59 | export default RegisterForm;
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Posts/Post.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useState } from 'react';
2 | import { Comments, CommentCreation } from '../';
3 |
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
5 | import { faThumbsUp, faThumbsDown } from '@fortawesome/free-regular-svg-icons'
6 |
7 | import { useFragment } from 'react-relay/hooks';
8 | import { useMutation } from 'react-relay/lib/relay-experimental';
9 | import graphql from 'babel-plugin-relay/macro';
10 |
11 |
12 |
13 | const commentCreationMutation = graphql`
14 | mutation PostCommentCreationMutation($content: String!, $post: String!) {
15 | CreateComment(input: {content: $content, post: $post, clientMutationId: "2"}) {
16 | comment {
17 | id
18 | content
19 | author {
20 | name
21 | }
22 | }
23 | }
24 | }
25 | `;
26 |
27 | const postTypeFragment = graphql`
28 | fragment PostTypeFragment on PostType @argumentDefinitions(
29 | first: {type: "Int"}
30 | last: {type: "Int"}
31 | before: {type: "String"}
32 | after: {type: "String"}
33 | ) {
34 | id
35 | author {
36 | name
37 | }
38 | content
39 | likes
40 | userHasLiked
41 | createdAt
42 | updatedAt
43 | ...CommentsTypeFragment @arguments(
44 | first: $first,
45 | last: $last,
46 | before: $before,
47 | after: $after
48 | )
49 | }
50 | `;
51 |
52 | const postLikeMutation = graphql`
53 | mutation PostLikeMutation($postId: String!) {
54 | LikePost(input: {post: $postId, clientMutationId: "5"}) {
55 | post {
56 | likes
57 | userHasLiked
58 | }
59 | }
60 | }
61 | `;
62 |
63 | const Post = ({post}: any) => {
64 | let commentContent = '';
65 |
66 | const postEdge = useFragment(postTypeFragment, post);
67 |
68 | const [likes, setLikes] = useState(postEdge.likes);
69 | const [hasLiked, setHasLiked] = useState(postEdge.userHasLiked);
70 |
71 | const [commentCreationCommit, cmtCrtIsInFlight] = useMutation(commentCreationMutation);
72 | const [likeCrtCommit, likeCrtIsInFlight] = useMutation(postLikeMutation)
73 |
74 | const commentCreation = (event: React.FormEvent) => {
75 | event.preventDefault();
76 |
77 | const variables = {
78 | content: commentContent,
79 | post: postEdge.id
80 | }
81 | commentCreationCommit({
82 | variables,
83 | onCompleted: (data: any) => {
84 | }
85 | });
86 | }
87 |
88 | const likesHandler = () => {
89 | setLikes(hasLiked ? likes - 1 : likes + 1);
90 | setHasLiked(hasLiked ? false : true);
91 |
92 | const variables = {
93 | postId: postEdge.id
94 | }
95 |
96 | likeCrtCommit({
97 | variables,
98 | onCompleted: ({LikePost}: any) => {
99 | setLikes(LikePost.post.likes);
100 | setHasLiked(LikePost.post.userHasLiked);
101 | }
102 | });
103 | };
104 |
105 |
106 | return (
107 |
108 |
109 |
110 |
111 |
112 |
113 | {postEdge.author.name}
114 |
115 |
116 |
117 | {postEdge.content}
118 |
119 |
120 |
121 |
122 | {likeCrtIsInFlight ? likes : postEdge.likes}
123 |
124 |
125 |
126 |
127 | {
128 | (likeCrtIsInFlight ? hasLiked : postEdge.userHasLiked) ?
129 | <> Liked> :
130 | <> Like>
131 | }
132 |
133 |
134 |
135 |
136 | {
137 | postEdge && postEdge ?
138 | :
139 | null
140 | }
141 |
142 |
143 |
144 |
145 |
146 | {
147 | commentContent = newContent
148 | }}/>
149 |
150 |
151 |
152 | );
153 | };
154 |
155 | export default Post;
--------------------------------------------------------------------------------
/frontend/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready
142 | .then(registration => {
143 | registration.unregister();
144 | })
145 | .catch(error => {
146 | console.error(error.message);
147 | });
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/frontend/src/Pages/FeedPage/Components/Comments/Comment.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useState } from 'react';
2 | import { useFragment } from 'react-relay/hooks';
3 | import { useMutation } from 'react-relay/lib/relay-experimental';
4 | import graphql from 'babel-plugin-relay/macro';
5 |
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
7 | import { faThumbsUp } from '@fortawesome/free-regular-svg-icons'
8 |
9 | import { Replies, ReplyCreation } from '../';
10 |
11 | const commentEdgeFragment = graphql`
12 | fragment CommentTypeFragment on CommentTypeEdge {
13 | cursor
14 | node {
15 | id
16 | author {
17 | name
18 | }
19 | content
20 | likes
21 | userHasLiked
22 | createdAt
23 | updatedAt
24 | ...RepliesTypeFragment
25 | }
26 |
27 | }`;
28 |
29 | const commentReplyCreationMutation = graphql`
30 | mutation CommentReplyCreationMutation ($content: String!, $comment: String!) {
31 | CreateReply (input: {content: $content, comment: $comment, clientMutationId: "3"}) {
32 | reply {
33 | content,
34 | author {
35 | name
36 | }
37 | }
38 | }
39 | }
40 | `;
41 |
42 | const commentLikeMutation = graphql `
43 | mutation CommentLikeMutation($commentId: String!) {
44 | LikeComment (input: {comment: $commentId, clientMutationId: "6"}) {
45 | comment {
46 | likes
47 | userHasLiked
48 | }
49 | }
50 | }
51 | `;
52 |
53 | const Comment = ({comment}: any) => {
54 | const commentEdge = useFragment(commentEdgeFragment, comment);
55 | const [likes, setLikes] = useState(commentEdge.node ? commentEdge.node.likes : 0);
56 | const [hasLiked, setHasLiked] = useState(commentEdge.node ? commentEdge.node.userHasLiked : false);
57 |
58 | const [showReplyCreation, setShowReplyCreation] = useState(false);
59 |
60 | const [commitReplyCre, replyCreIsInFlight] = useMutation(commentReplyCreationMutation);
61 | const [commitLikeMut, likeMutIsInFlight] = useMutation(commentLikeMutation);
62 |
63 | let replyContent = '';
64 |
65 | const replyCreationFormSubmit = (event: React.FormEvent) => {
66 | event.preventDefault();
67 | repliesHandler();
68 | const variables = {
69 | content: replyContent,
70 | comment: commentEdge.node ? commentEdge.node.id : null
71 | }
72 | if (variables.comment && variables.content) {
73 | commitReplyCre({
74 | variables,
75 | onCompleted: (data: any) => {
76 | }
77 | })
78 | }
79 | };
80 |
81 | const likeHandler = () => {
82 | setLikes(hasLiked ? likes - 1 : likes + 1);
83 | setHasLiked(hasLiked ? false : true);
84 | const variables = {
85 | commentId: commentEdge.node ? commentEdge.node.id : null
86 | }
87 | if (variables.commentId) {
88 | commitLikeMut({
89 | variables,
90 | onCompleted: ({LikeComment}: any) => {
91 | setLikes(LikeComment.comment.likes);
92 | setHasLiked(LikeComment.comment.userHasLiked);
93 | }
94 | })
95 | }
96 | }
97 |
98 | const repliesHandler = () => {
99 | setShowReplyCreation(showReplyCreation ? false : true);
100 | }
101 |
102 | return (
103 |
104 |
105 |
106 |
107 | {commentEdge.node.author.name}
108 |
109 |
110 |
111 |
112 | {commentEdge.node ? commentEdge.node.content : null}
113 |
114 |
115 |
116 |
117 | {likeMutIsInFlight ? likes : commentEdge.node.likes}
118 |
119 |
120 | {
121 | (likeMutIsInFlight ? hasLiked : commentEdge.node.userHasLiked) ?
122 | <>Liked> :
123 | <>Like>
124 | }
125 |
126 |
127 | {
128 | showReplyCreation ?
129 | <>Replying> :
130 | <>Reply>
131 | }
132 |
133 |
134 |
135 |
136 | {
137 | showReplyCreation && commentEdge && commentEdge.node ?
138 |
replyContent = newContent}/> :
139 | null
140 | }
141 |
142 |
143 | {
144 | commentEdge && commentEdge.node ?
145 | :
146 | null
147 | }
148 |
149 |
150 |
151 |
152 | );
153 | };
154 |
155 | export default Comment
--------------------------------------------------------------------------------
/frontend/types/react-router-dom.d.ts:
--------------------------------------------------------------------------------
1 | export class BrowserRouter {
2 | constructor(...args: any[]);
3 | componentDidMount(): void;
4 | forceUpdate(callback: any): void;
5 | render(): any;
6 | setState(partialState: any, callback: any): void;
7 | }
8 | export namespace BrowserRouter {
9 | namespace propTypes {
10 | function basename(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
11 | namespace basename {
12 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
13 | }
14 | function children(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
15 | namespace children {
16 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
17 | }
18 | function forceRefresh(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
19 | namespace forceRefresh {
20 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
21 | }
22 | function getUserConfirmation(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
23 | namespace getUserConfirmation {
24 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
25 | }
26 | function keyLength(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
27 | namespace keyLength {
28 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
29 | }
30 | }
31 | }
32 | export class HashRouter {
33 | constructor(...args: any[]);
34 | componentDidMount(): void;
35 | forceUpdate(callback: any): void;
36 | render(): any;
37 | setState(partialState: any, callback: any): void;
38 | }
39 | export namespace HashRouter {
40 | namespace propTypes {
41 | function basename(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
42 | namespace basename {
43 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
44 | }
45 | function children(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
46 | namespace children {
47 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
48 | }
49 | function getUserConfirmation(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
50 | namespace getUserConfirmation {
51 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
52 | }
53 | function hashType(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
54 | namespace hashType {
55 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
56 | }
57 | }
58 | }
59 | export namespace Link {
60 | const $$typeof: symbol;
61 | const displayName: any;
62 | namespace propTypes {
63 | function innerRef(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
64 | namespace innerRef {
65 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
66 | }
67 | function onClick(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
68 | namespace onClick {
69 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
70 | }
71 | function replace(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
72 | namespace replace {
73 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
74 | }
75 | function target(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
76 | namespace target {
77 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
78 | }
79 | function to(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
80 | }
81 | function render(_ref2: any, forwardedRef: any): any;
82 | namespace render {
83 | const displayName: string;
84 | }
85 | }
86 | export const MemoryRouter: any;
87 | export namespace NavLink {
88 | const $$typeof: symbol;
89 | const displayName: any;
90 | namespace propTypes {
91 | function activeClassName(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
92 | namespace activeClassName {
93 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
94 | }
95 | function activeStyle(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
96 | namespace activeStyle {
97 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
98 | }
99 | function className(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
100 | namespace className {
101 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
102 | }
103 | function exact(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
104 | namespace exact {
105 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
106 | }
107 | function innerRef(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
108 | namespace innerRef {
109 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
110 | }
111 | function isActive(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
112 | namespace isActive {
113 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
114 | }
115 | function location(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
116 | namespace location {
117 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
118 | }
119 | function onClick(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
120 | namespace onClick {
121 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
122 | }
123 | function replace(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
124 | namespace replace {
125 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
126 | }
127 | function sensitive(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
128 | namespace sensitive {
129 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
130 | }
131 | function strict(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
132 | namespace strict {
133 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
134 | }
135 | function style(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
136 | namespace style {
137 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
138 | }
139 | function target(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
140 | namespace target {
141 | function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
142 | }
143 | function to(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any;
144 | }
145 | function render(_ref: any, forwardedRef: any): any;
146 | namespace render {
147 | const displayName: string;
148 | }
149 | }
150 | export const Prompt: any;
151 | export const Redirect: any;
152 | export const Route: any;
153 | export const Router: any;
154 | export const StaticRouter: any;
155 | export const Switch: any;
156 | export const generatePath: any;
157 | export const matchPath: any;
158 | export const useHistory: any;
159 | export const useLocation: any;
160 | export const useParams: any;
161 | export const useRouteMatch: any;
162 | export const withRouter: any;
163 |
--------------------------------------------------------------------------------
/frontend/schema/schema.graphql:
--------------------------------------------------------------------------------
1 | schema {
2 | query: Query
3 | mutation: MutationType
4 | subscription: SubscriptionType
5 | }
6 |
7 | input CommentLikeSubscriptionInput {
8 | clientSubscriptionId: String
9 | }
10 |
11 | type CommentLikeSubscriptionPayload {
12 | comment: CommentType
13 | clientSubscriptionId: String
14 | }
15 |
16 | """Comment type"""
17 | type CommentType implements Node {
18 | """The ID of an object"""
19 | id: ID!
20 | author: UserType
21 | content: String
22 | likes: Int
23 | userHasLiked: Boolean
24 | createdAt: String
25 | updatedAt: String
26 | replies(after: String, first: Int, before: String, last: Int): ReplyTypeConnection
27 | }
28 |
29 | """A connection to a list of items."""
30 | type CommentTypeConnection {
31 | """Information to aid in pagination."""
32 | pageInfo: PageInfo!
33 |
34 | """A list of edges."""
35 | edges: [CommentTypeEdge]
36 | }
37 |
38 | """An edge in a connection."""
39 | type CommentTypeEdge {
40 | """The item at the end of the edge"""
41 | node: CommentType
42 |
43 | """A cursor for use in pagination"""
44 | cursor: String!
45 | }
46 |
47 | input CreateCommentInput {
48 | content: String
49 | post: String
50 | clientMutationId: String
51 | }
52 |
53 | type CreateCommentPayload {
54 | comment: CommentType
55 | clientMutationId: String
56 | }
57 |
58 | input CreateCommentSubscriptionInput {
59 | clientSubscriptionId: String
60 | }
61 |
62 | type CreateCommentSubscriptionPayload {
63 | comment: CommentType
64 | post: PostType
65 | clientSubscriptionId: String
66 | }
67 |
68 | input CreateReplyInput {
69 | content: String
70 | comment: String
71 | clientMutationId: String
72 | }
73 |
74 | type CreateReplyPayload {
75 | reply: ReplyType
76 | clientMutationId: String
77 | }
78 |
79 | input LikeCommentInput {
80 | comment: String
81 | clientMutationId: String
82 | }
83 |
84 | type LikeCommentPayload {
85 | comment: CommentType
86 | clientMutationId: String
87 | }
88 |
89 | input LikePostInput {
90 | post: String
91 | clientMutationId: String
92 | }
93 |
94 | type LikePostPayload {
95 | post: PostType
96 | clientMutationId: String
97 | }
98 |
99 | input LikeReplayInput {
100 | reply: String
101 | clientMutationId: String
102 | }
103 |
104 | type LikeReplayPayload {
105 | reply: ReplyType
106 | clientMutationId: String
107 | }
108 |
109 | input LoginInput {
110 | email: String
111 | password: String
112 | clientMutationId: String
113 | }
114 |
115 | type LoginPayload {
116 | user: UserType
117 | clientMutationId: String
118 | }
119 |
120 | """Mutation Type"""
121 | type MutationType {
122 | """Create new user"""
123 | CreateUser(input: UserCreationInput!): UserCreationPayload
124 |
125 | """Login a user, generates new token"""
126 | Login(input: LoginInput!): LoginPayload
127 |
128 | """Post Creation"""
129 | PostCreation(input: PostCreationInput!): PostCreationPayload
130 |
131 | """Mutation for like handling for posts"""
132 | LikePost(input: LikePostInput!): LikePostPayload
133 |
134 | """Create Comment Mutation"""
135 | CreateComment(input: CreateCommentInput!): CreateCommentPayload
136 |
137 | """Update total likes for a comment type"""
138 | LikeComment(input: LikeCommentInput!): LikeCommentPayload
139 |
140 | """Create Reply Mutation"""
141 | CreateReply(input: CreateReplyInput!): CreateReplyPayload
142 |
143 | """Handle likes for replies"""
144 | LikeReply(input: LikeReplayInput!): LikeReplayPayload
145 | }
146 |
147 | """An object with an ID"""
148 | interface Node {
149 | """The id of the object."""
150 | id: ID!
151 | }
152 |
153 | """Information about pagination in a connection."""
154 | type PageInfo {
155 | """When paginating forwards, are there more items?"""
156 | hasNextPage: Boolean!
157 |
158 | """When paginating backwards, are there more items?"""
159 | hasPreviousPage: Boolean!
160 |
161 | """When paginating backwards, the cursor to continue."""
162 | startCursor: String
163 |
164 | """When paginating forwards, the cursor to continue."""
165 | endCursor: String
166 | }
167 |
168 | input PostCreationInput {
169 | content: String
170 | clientMutationId: String
171 | }
172 |
173 | type PostCreationPayload {
174 | post: PostType
175 | clientMutationId: String
176 | }
177 |
178 | input PostCreationSubscriptionInput {
179 | clientSubscriptionId: String
180 | }
181 |
182 | type PostCreationSubscriptionPayload {
183 | post: PostType
184 | clientSubscriptionId: String
185 | }
186 |
187 | input PostLikeSubscriptionInput {
188 | clientSubscriptionId: String
189 | }
190 |
191 | type PostLikeSubscriptionPayload {
192 | post: PostType
193 | clientSubscriptionId: String
194 | }
195 |
196 | """Post type"""
197 | type PostType implements Node {
198 | """The ID of an object"""
199 | id: ID!
200 | author: UserType
201 | content: String
202 | likes: Int
203 | userHasLiked: Boolean
204 | createdAt: String
205 | updatedAt: String
206 | comments(after: String, first: Int, before: String, last: Int): CommentTypeConnection
207 | }
208 |
209 | """A connection to a list of items."""
210 | type PostTypeConnection {
211 | """Information to aid in pagination."""
212 | pageInfo: PageInfo!
213 |
214 | """A list of edges."""
215 | edges: [PostTypeEdge]
216 | }
217 |
218 | """An edge in a connection."""
219 | type PostTypeEdge {
220 | """The item at the end of the edge"""
221 | node: PostType
222 |
223 | """A cursor for use in pagination"""
224 | cursor: String!
225 | }
226 |
227 | """General QueryType"""
228 | type Query {
229 | """Fetches an object given its ID"""
230 | node(
231 | """The ID of an object"""
232 | id: ID!
233 | ): Node
234 |
235 | """Fetches objects given their IDs"""
236 | nodes(
237 | """The IDs of objects"""
238 | ids: [ID!]!
239 | ): [Node]!
240 | myself: UserType
241 | myPosts(after: String, first: Int, before: String, last: Int): PostTypeConnection
242 | }
243 |
244 | input ReplyCreationSubscriptionInput {
245 | clientSubscriptionId: String
246 | }
247 |
248 | type ReplyCreationSubscriptionPayload {
249 | reply: ReplyType
250 | comment: CommentType
251 | clientSubscriptionId: String
252 | }
253 |
254 | input ReplyLikeSubscriptionInput {
255 | clientSubscriptionId: String
256 | }
257 |
258 | type ReplyLikeSubscriptionPayload {
259 | reply: ReplyType
260 | clientSubscriptionId: String
261 | }
262 |
263 | """Reply type"""
264 | type ReplyType implements Node {
265 | """The ID of an object"""
266 | id: ID!
267 | author: UserType
268 | content: String
269 | createdAt: String
270 | updatedAt: String
271 | likes: Int
272 | userHasLiked: Boolean
273 | }
274 |
275 | """A connection to a list of items."""
276 | type ReplyTypeConnection {
277 | """Information to aid in pagination."""
278 | pageInfo: PageInfo!
279 |
280 | """A list of edges."""
281 | edges: [ReplyTypeEdge]
282 | }
283 |
284 | """An edge in a connection."""
285 | type ReplyTypeEdge {
286 | """The item at the end of the edge"""
287 | node: ReplyType
288 |
289 | """A cursor for use in pagination"""
290 | cursor: String!
291 | }
292 |
293 | type SubscriptionType {
294 | PostCreationSubscription(input: PostCreationSubscriptionInput!): PostCreationSubscriptionPayload
295 |
296 | """Post Like subscription"""
297 | PostLikeSubscription(input: PostLikeSubscriptionInput!): PostLikeSubscriptionPayload
298 |
299 | """Create Comment Subscription"""
300 | CreateCommentSubscription(input: CreateCommentSubscriptionInput!): CreateCommentSubscriptionPayload
301 |
302 | """Comment Like subscription"""
303 | CommentLikeSubscription(input: CommentLikeSubscriptionInput!): CommentLikeSubscriptionPayload
304 |
305 | """Reply Creation Subscription"""
306 | ReplyCreationSubscription(input: ReplyCreationSubscriptionInput!): ReplyCreationSubscriptionPayload
307 |
308 | """Subscription to fetch likes in replies"""
309 | ReplyLikeSubscription(input: ReplyLikeSubscriptionInput!): ReplyLikeSubscriptionPayload
310 | }
311 |
312 | input UserCreationInput {
313 | name: String
314 | password: String
315 | email: String
316 | clientMutationId: String
317 | }
318 |
319 | type UserCreationPayload {
320 | user: UserType
321 | clientMutationId: String
322 | }
323 |
324 | """User type"""
325 | type UserType {
326 | name: String
327 | password: String
328 | email: String
329 | createdAt: String
330 | updatedAt: String
331 | token: String
332 | friends(after: String, first: Int, before: String, last: Int): UserTypeConnection
333 | posts(after: String, first: Int, before: String, last: Int): PostTypeConnection
334 | _id: String
335 | }
336 |
337 | """A connection to a list of items."""
338 | type UserTypeConnection {
339 | """Information to aid in pagination."""
340 | pageInfo: PageInfo!
341 |
342 | """A list of edges."""
343 | edges: [UserTypeEdge]
344 | }
345 |
346 | """An edge in a connection."""
347 | type UserTypeEdge {
348 | """The item at the end of the edge"""
349 | node: UserType
350 |
351 | """A cursor for use in pagination"""
352 | cursor: String!
353 | }
354 |
355 |
--------------------------------------------------------------------------------