├── user-client
├── src
│ ├── App.css
│ ├── views
│ │ ├── shared
│ │ │ ├── Dropdownui.module.css
│ │ │ ├── individualthread
│ │ │ │ ├── Individualthreadquill.css
│ │ │ │ ├── point
│ │ │ │ │ └── Point.js
│ │ │ │ ├── Individualthread.module.css
│ │ │ │ └── Individualthread.js
│ │ │ ├── editor
│ │ │ │ ├── Editor.module.css
│ │ │ │ └── Editor.js
│ │ │ ├── elements
│ │ │ │ ├── Imageblot.js
│ │ │ │ ├── Fileblot.js
│ │ │ │ └── Videoblot.js
│ │ │ └── Dropdownui.js
│ │ ├── frontpage
│ │ │ ├── Frontpage.module.css
│ │ │ ├── Frontpagewrapper.js
│ │ │ ├── Sectioncard.js
│ │ │ ├── Sectioncards.module.css
│ │ │ └── Frontpage.js
│ │ ├── threadslistview
│ │ │ ├── Threadlist.module.css
│ │ │ ├── Threadslistwrapper.js
│ │ │ └── Threadslist.js
│ │ ├── threadfullview
│ │ │ ├── Threadfullview.module.css
│ │ │ ├── comments
│ │ │ │ ├── Publishcomment.module.css
│ │ │ │ ├── Individualcomment.module.css
│ │ │ │ ├── Publishcomment.js
│ │ │ │ └── Individualcomment.js
│ │ │ ├── Threadfullviewwrapper.js
│ │ │ └── Threadfullview.js
│ │ ├── signin
│ │ │ ├── Signin.module.css
│ │ │ ├── Signinwrapper.js
│ │ │ └── Signin.js
│ │ ├── signup
│ │ │ ├── Signup.module.css
│ │ │ ├── Signupwrapper.js
│ │ │ └── Signup.js
│ │ ├── updatethread
│ │ │ ├── Updatethread.module.css
│ │ │ ├── Updatethreadwrapper.js
│ │ │ └── Updatethread.js
│ │ ├── publishthread
│ │ │ ├── Publishthread.module.css
│ │ │ ├── Publishthreadwrapper.js
│ │ │ └── Publishthread.js
│ │ └── landing
│ │ │ ├── Landingwrapper.js
│ │ │ ├── Landing.module.css
│ │ │ └── Landing.js
│ ├── index.css
│ ├── setupTests.js
│ ├── App.test.js
│ ├── helper
│ │ ├── Const.js
│ │ ├── Dimensions.js
│ │ └── Timesince.js
│ ├── App.js
│ ├── store
│ │ ├── Store.js
│ │ ├── reducers
│ │ │ ├── Composethread.js
│ │ │ ├── Auth.js
│ │ │ └── Threads.js
│ │ └── actions
│ │ │ ├── Auth.js
│ │ │ ├── Composethread.js
│ │ │ └── Threads.js
│ ├── index.js
│ ├── antd-style
│ │ └── LICENSE
│ ├── handler
│ │ ├── Uploadhandler.js
│ │ └── Queryhandler.js
│ ├── resources
│ │ ├── default-monochrome.svg
│ │ └── images
│ │ │ ├── tech.svg
│ │ │ ├── space.svg
│ │ │ ├── finance.svg
│ │ │ ├── all.svg
│ │ │ └── programming.svg
│ └── serviceWorker.js
├── public
│ ├── _redirects
│ ├── robots.txt
│ ├── favicon.png
│ ├── logo192.png
│ ├── logo512.png
│ ├── readme
│ │ └── natforums vid.gif
│ ├── manifest.json
│ ├── index.html
│ └── logo.svg
├── Dockerfile
└── package.json
├── server
├── Procfile
├── Dockerfile
├── helper
│ └── Const.js
├── models
│ ├── Comment.js
│ ├── User.js
│ └── Thread.js
├── package.json
├── auth
│ └── Auth.js
├── graphql
│ └── Schema.js
└── app.js
├── .gitattributes
├── .gitignore
├── LICENSE
└── README.md
/user-client/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/Procfile:
--------------------------------------------------------------------------------
1 | web: node app.js
--------------------------------------------------------------------------------
/user-client/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | user-client/src/antd-style/* linguist-generated
--------------------------------------------------------------------------------
/user-client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | server/node_modules/
2 | server/public
3 | server/.env
4 | user-client/node_modules/
5 | user-client/build/
--------------------------------------------------------------------------------
/user-client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izkshitiz/natforums/HEAD/user-client/public/favicon.png
--------------------------------------------------------------------------------
/user-client/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izkshitiz/natforums/HEAD/user-client/public/logo192.png
--------------------------------------------------------------------------------
/user-client/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izkshitiz/natforums/HEAD/user-client/public/logo512.png
--------------------------------------------------------------------------------
/user-client/src/views/shared/Dropdownui.module.css:
--------------------------------------------------------------------------------
1 | .dropdownui{
2 | font-size: .5em;
3 | cursor: pointer;
4 | }
--------------------------------------------------------------------------------
/user-client/src/views/shared/individualthread/Individualthreadquill.css:
--------------------------------------------------------------------------------
1 | .ql-align-center{
2 | text-align: center;
3 | }
--------------------------------------------------------------------------------
/user-client/public/readme/natforums vid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/izkshitiz/natforums/HEAD/user-client/public/readme/natforums vid.gif
--------------------------------------------------------------------------------
/user-client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #f7f7f7;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | }
6 |
--------------------------------------------------------------------------------
/user-client/src/views/frontpage/Frontpage.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | grid-column: auto/span 16;
3 | display: grid;
4 | grid-template-columns: repeat(16, 1fr);
5 | grid-template-rows: auto;
6 | }
7 |
--------------------------------------------------------------------------------
/user-client/src/views/threadslistview/Threadlist.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | grid-column: auto/span 16;
3 | }
4 |
5 | @media only screen and (min-width: 992px) {
6 | .contentwrapper {
7 | grid-column: 4/span 10;
8 | }
9 | }
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/Threadfullview.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | grid-column: auto/span 16;
3 | }
4 |
5 | @media only screen and (min-width: 992px) {
6 | .contentwrapper {
7 | grid-column: 4/span 10;
8 | }
9 | }
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | # create node 18 base image on alpine
2 | FROM node:18-alpine
3 |
4 | RUN mkdir -p /usr/src/app
5 |
6 | WORKDIR /usr/src/app
7 |
8 | COPY package.json package.json
9 |
10 | RUN npm i
11 |
12 | COPY . .
13 |
14 | CMD [ "npm", "run", "start" ]
15 |
--------------------------------------------------------------------------------
/server/helper/Const.js:
--------------------------------------------------------------------------------
1 | // Don't use capital letters for admin username
2 | exports.ADMIN_USERNAME = process.env.HOST_ADMIN_USERNAME
3 | ? process.env.HOST_ADMIN_USERNAME
4 | : "superadmin";
5 |
6 | exports.UPLOAD_SUPPORT =
7 | process.env.HOST_UPLOADS === "enabled" ? "enabled" : "disabled";
8 |
--------------------------------------------------------------------------------
/user-client/src/setupTests.js:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/editor/Editor.module.css:
--------------------------------------------------------------------------------
1 | .editorcontainer{
2 | /* background-color: red; */
3 | position: relative;
4 | }
5 | .loading {
6 | position: absolute;
7 | background-color: rgba(255, 255, 255, 1);
8 | opacity: .5;
9 | width: 100%;
10 | height: 100%;
11 | z-index: 2;
12 | }
--------------------------------------------------------------------------------
/user-client/src/App.test.js:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/user-client/Dockerfile:
--------------------------------------------------------------------------------
1 | # create node 16 base image on alpine
2 | FROM node:16-alpine
3 |
4 | RUN mkdir -p /usr/src/app
5 |
6 | WORKDIR /usr/src/app
7 |
8 | COPY package.json package.json
9 |
10 | RUN npm i
11 |
12 | COPY . .
13 |
14 | RUN npm run build
15 |
16 | RUN rm -rf ./node_modules
17 |
18 | RUN npm i -g serve
19 |
20 | CMD [ "serve", "-s", "build" ]
21 |
--------------------------------------------------------------------------------
/user-client/src/helper/Const.js:
--------------------------------------------------------------------------------
1 | export const HOST_URL = process.env.REACT_APP_HOST
2 | ? process.env.REACT_APP_HOST
3 | : "http://localhost:8080";
4 |
5 | export const UPLOAD_TYPE =
6 | process.env.CLIENT_UPLOAD_TYPE === "cloud" ? "cloud" : "host";
7 |
8 | export const UPLOAD_SUPPORT =
9 | process.env.REACT_APP_UPLOADS === "enabled" ? "enabled" : "disabled";
10 |
--------------------------------------------------------------------------------
/server/models/Comment.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const commentSchema = new Schema({
5 | commentauthor: {
6 | username: String
7 | },
8 | thread: {
9 | type: String,
10 | ref: 'Thread'
11 | },
12 | content: String,
13 | votes: Number,
14 | }, { timestamps: true });
15 |
16 |
17 | module.exports = mongoose.model('Comment', commentSchema);
--------------------------------------------------------------------------------
/user-client/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import Landingwrapper from './views/landing/Landingwrapper';
4 | import './antd-style/red-antd.css';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/user-client/src/views/signin/Signin.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | align-self: flex-start;
3 | background-color: white;
4 | grid-column: auto/span 16;
5 | display: flex;
6 | padding: 20px;
7 | }
8 |
9 | .signinformcontainer {
10 | margin: 0 auto;
11 | align-self: center;
12 | }
13 |
14 | @media only screen and (min-width: 992px) {
15 | .contentwrapper {
16 | grid-column: 4/span 10;
17 | }
18 | }
--------------------------------------------------------------------------------
/user-client/src/views/signup/Signup.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | align-self: flex-start;
3 | background-color: white;
4 | grid-column: auto/span 16;
5 | display: flex;
6 | padding: 20px;
7 | }
8 |
9 | .signupformcontainer {
10 | margin: 0 auto;
11 | align-self: center;
12 | }
13 |
14 | @media only screen and (min-width: 992px) {
15 | .contentwrapper {
16 | grid-column: 4/span 10;
17 | }
18 | }
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/comments/Publishcomment.module.css:
--------------------------------------------------------------------------------
1 | .publishcommentcontainer {
2 | grid-column: 4/span 10;
3 | background-color: white;
4 | text-align: end;
5 | margin: 10px;
6 | padding: 15px 20px;
7 | position: relative;
8 | }
9 |
10 | .loading {
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | background-color: rgba(255, 255, 255, 1);
15 | opacity: .5;
16 | width: 100%;
17 | height: 100%;
18 | z-index: 2;
19 | }
--------------------------------------------------------------------------------
/user-client/src/store/Store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import Auth from './reducers/Auth';
4 | import Threads from './reducers/Threads';
5 | import Composethread from './reducers/Composethread';
6 |
7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 |
9 | const store = createStore(
10 | combineReducers({ Auth, Composethread, Threads }),
11 | composeEnhancers(
12 | applyMiddleware(thunk)
13 | )
14 | );
15 |
16 | export default store;
--------------------------------------------------------------------------------
/user-client/src/views/updatethread/Updatethread.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | align-self: flex-start;
3 | background-color: white;
4 | grid-column: auto/span 16;
5 | display: flex;
6 | padding: 20px;
7 | }
8 |
9 | .updatethreadcontainer {
10 | flex-basis: 100%;
11 | margin: 0 auto;
12 | align-self: center;
13 | }
14 |
15 | .options {
16 | margin-bottom: 20px;
17 | }
18 |
19 | h3 {
20 | text-align: left;
21 | padding-top: 10px;
22 | }
23 |
24 | @media only screen and (min-width: 992px) {
25 | .contentwrapper {
26 | grid-column: 4/span 10;
27 | }
28 | }
--------------------------------------------------------------------------------
/user-client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "natforums",
3 | "name": "nat community forums",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
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 |
--------------------------------------------------------------------------------
/user-client/src/views/publishthread/Publishthread.module.css:
--------------------------------------------------------------------------------
1 | .contentwrapper {
2 | align-self: flex-start;
3 | background-color: white;
4 | grid-column: auto/span 16;
5 | display: flex;
6 | padding: 20px;
7 | }
8 |
9 | .publishthreadcontainer {
10 | flex-basis: 100%;
11 | margin: 0 auto;
12 | align-self: center;
13 | }
14 |
15 | .options {
16 | margin-bottom: 20px;
17 | }
18 |
19 | h3 {
20 | text-align: left;
21 | padding-top: 10px;
22 | }
23 |
24 |
25 | @media only screen and (min-width: 992px) {
26 | .contentwrapper {
27 | grid-column: 4/span 10;
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const userSchema = new Schema({
5 | username: {
6 | type: String,
7 | required: true,
8 | unique: true
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true
14 | },
15 | password: {
16 | type: String,
17 | required: true
18 | },
19 | threads: [
20 | {
21 | type: String,
22 | ref: 'Thread'
23 | }
24 | ]
25 |
26 | })
27 |
28 | module.exports = mongoose.model('User', userSchema);
29 |
--------------------------------------------------------------------------------
/user-client/src/helper/Dimensions.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | function getWindowDimensions() {
4 | const { innerWidth: width } = window;
5 | return { width };
6 | }
7 |
8 | export default function Dimensions() {
9 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
10 |
11 | useEffect(() => {
12 | function handleResize() {
13 | setWindowDimensions(getWindowDimensions());
14 | }
15 |
16 | window.addEventListener('resize', handleResize);
17 | return () => window.removeEventListener('resize', handleResize);
18 | });
19 |
20 | return windowDimensions;
21 | }
--------------------------------------------------------------------------------
/user-client/src/views/landing/Landingwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import Landing from './Landing';
5 | import { signoutUserAction } from '../../store/actions/Auth';
6 |
7 | const mapStateToProps = state => ({
8 | username: state.Auth.username,
9 | token: state.Auth.token
10 | });
11 |
12 | const mapDispatchToProps = { signoutUserAction };
13 | const enhance = compose(
14 | withRouter,
15 | connect(
16 | mapStateToProps,
17 | mapDispatchToProps
18 | )
19 | );
20 |
21 | const Landingwrapper = enhance(Landing);
22 |
23 | export default Landingwrapper;
24 |
--------------------------------------------------------------------------------
/user-client/src/views/signin/Signinwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { signinUserAction } from '../../store/actions/Auth';
5 | import Signin from './Signin';
6 |
7 | const mapStateToProps = state => ({
8 | requestPending: state.Auth.requestPending,
9 | token: state.Auth.token
10 | });
11 |
12 | const mapDispatchToProps = { signinUserAction };
13 |
14 | const enhance = compose(
15 | withRouter,
16 | connect(
17 | mapStateToProps,
18 | mapDispatchToProps
19 | )
20 | );
21 |
22 | const Signinwrapper = enhance(Signin);
23 |
24 | export default Signinwrapper;
25 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/elements/Imageblot.js:
--------------------------------------------------------------------------------
1 | import { Quill } from 'react-quill';
2 |
3 | const BlockEmbed = Quill.import('blots/block/embed');
4 |
5 | class ImageBlot extends BlockEmbed {
6 |
7 | static create(value) {
8 | const node = super.create();
9 | node.setAttribute('src', value.src);
10 | node.setAttribute('alt', value.alt);
11 | return node;
12 | }
13 |
14 | static value(node) {
15 | return { src: node.getAttribute('src'), alt: node.getAttribute('alt') };
16 | }
17 |
18 | }
19 |
20 | ImageBlot.blotName = 'image';
21 | ImageBlot.tagName = 'img';
22 | ImageBlot.className = 'thread-images';
23 |
24 | export default ImageBlot;
--------------------------------------------------------------------------------
/user-client/src/views/signup/Signupwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { signupUserAction } from '../../store/actions/Auth';
5 | import Signup from './Signup';
6 |
7 | const mapStateToProps = state => ({
8 | requestPending: state.Auth.requestPending,
9 | token: state.Auth.token
10 | });
11 |
12 | const mapDispatchToProps = { signupUserAction };
13 |
14 | const enhance = compose(
15 | withRouter,
16 | connect(
17 | mapStateToProps,
18 | mapDispatchToProps
19 | )
20 | );
21 |
22 | const SignupFormContainer = enhance(Signup);
23 |
24 | export default SignupFormContainer;
25 |
--------------------------------------------------------------------------------
/user-client/src/views/frontpage/Frontpagewrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { getMetaAction } from '../../store/actions/Threads'
5 | import Frontpage from './Frontpage';
6 |
7 | const mapStateToProps = state => ({
8 | getMetaRequestPending: state.Threads.getMetaRequestPending,
9 | meta: state.Threads.meta
10 | });
11 |
12 | const mapDispatchToProps = { getMetaAction };
13 |
14 | const enhance = compose(
15 | withRouter,
16 | connect(
17 | mapStateToProps,
18 | mapDispatchToProps
19 | )
20 | );
21 |
22 | const Frontpagewrapper = enhance(Frontpage);
23 |
24 | export default Frontpagewrapper;
25 |
--------------------------------------------------------------------------------
/user-client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import * as serviceWorker from './serviceWorker';
5 | import App from './App';
6 | import store from './store/Store';
7 | import './index.css';
8 |
9 | ReactDOM.render(
10 | //
11 |
12 |
13 |
14 | //
15 | ,
16 | document.getElementById('root')
17 | );
18 |
19 | // If you want your app to work offline and load faster, you can change
20 | // unregister() to register() below. Note this comes with some pitfalls.
21 | // Learn more about service workers: https://bit.ly/CRA-PWA
22 | serviceWorker.unregister();
23 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/elements/Fileblot.js:
--------------------------------------------------------------------------------
1 | import { Quill } from 'react-quill';
2 |
3 | const BlockEmbed = Quill.import('blots/block/embed');
4 |
5 | class FileBlot extends BlockEmbed {
6 |
7 | static create(value) {
8 | let link = document.createElement('a');
9 | link.setAttribute('href', value.href);
10 | link.setAttribute("target", "_blank");
11 | let textnode = document.createTextNode(value.name);
12 | link.appendChild(textnode);
13 | const node = super.create();
14 | node.appendChild(link);
15 | return node;
16 | }
17 |
18 | static value() { }
19 |
20 | }
21 |
22 | FileBlot.blotName = 'file';
23 | FileBlot.tagName = 'p';
24 | FileBlot.className = 'thread-file-links';
25 |
26 | export default FileBlot;
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/comments/Individualcomment.module.css:
--------------------------------------------------------------------------------
1 | .individualcommentcontainer {
2 | /* Width of this container is managed by parent container */
3 | display: grid;
4 | grid-template-rows: 30px auto 30px;
5 | height: 160px;
6 | background-color: white;
7 | text-align: start;
8 | margin: 10px;
9 | padding: 15px 20px;
10 | position: relative;
11 | }
12 |
13 | .loading {
14 | position: absolute;
15 | background-color: rgba(255, 255, 255, 1);
16 | opacity: .5;
17 | width: 100%;
18 | height: 100%;
19 | z-index: 2;
20 | }
21 |
22 | .individualcommenttopbox {
23 | display: flex;
24 | justify-content: space-between;
25 | }
26 |
27 | .date {
28 | margin-left: 10px;
29 | color: grey;
30 | font-size: 12px;
31 | }
32 |
33 | .individualcommentcontentbox p {
34 | color: grey;
35 | }
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "Server for natforums",
5 | "main": "app.js",
6 | "engines": {
7 | "node": "v12.18.1",
8 | "npm": "6.14.5"
9 | },
10 | "scripts": {
11 | "start": "nodemon app.js",
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "author": "kshitiz",
15 | "license": "MIT",
16 | "dependencies": {
17 | "aws-sdk": "^2.805.0",
18 | "bcryptjs": "^2.4.3",
19 | "dompurify": "^2.1.1",
20 | "dotenv": "^8.2.0",
21 | "express": "^4.17.1",
22 | "express-graphql": "^0.11.0",
23 | "graphql": "^15.3.0",
24 | "jsdom": "^16.4.0",
25 | "jsonwebtoken": "^8.5.1",
26 | "mongoose": "^5.10.5",
27 | "multer": "^1.4.2",
28 | "shortid": "^2.2.15",
29 | "speakingurl": "^14.0.1",
30 | "validator": "^13.1.1"
31 | },
32 | "devDependencies": {
33 | "nodemon": "^2.0.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/auth/Auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const secret = process.env.SECRET || '1a51ab5a8fd4d130ec4e56a922cd4287932ae34a37ced3f5dd76ac71127f94b42c0ab8c492e4d675f83a4e709cbf1d8ea4f038af7d0fc6f30ea1030e758339be';
3 |
4 | module.exports = (req, res, next) => {
5 |
6 | const authHeader = req.get('Authorization');
7 | if (!authHeader) {
8 | req.isAuth = false;
9 | return next();
10 | }
11 |
12 | const token = authHeader.split(' ')[1];
13 | let decodedToken;
14 |
15 | try {
16 | decodedToken = jwt.verify(token, secret);
17 | } catch (err) {
18 | req.isAuth = false;
19 | return next();
20 | }
21 |
22 | if (!decodedToken) {
23 | req.isAuth = false;
24 | return next();
25 | }
26 |
27 | req.username = decodedToken.username;
28 | req.userId = decodedToken.userId;
29 | req.weightage = decodedToken.weightage;
30 | req.accessLevel = decodedToken.accessLevel;
31 | req.isAuth = true;
32 |
33 | next();
34 | };
35 |
--------------------------------------------------------------------------------
/user-client/src/views/threadslistview/Threadslistwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { getThreadsAction, deleteThreadAction } from '../../store/actions/Threads';
5 | import Threadslist from './Threadslist';
6 |
7 | export const mapStateToProps = state => ({
8 | myUsername: state.Auth.username,
9 | accessLevel: state.Auth.accessLevel,
10 | token: state.Auth.token,
11 | threads: state.Threads.threads,
12 | totalThreads: state.Threads.totalThreads&&state.Threads.totalThreads,
13 | deleteThreadRequestPending:state.Threads.deleteThreadRequestPending,
14 | getThreadsRequestPending: state.Threads.getThreadsRequestPending,
15 | initRequestPending: state.Threads.initRequestPending
16 | });
17 |
18 | const mapDispatchToProps = { getThreadsAction, deleteThreadAction };
19 |
20 | const enhance = compose(
21 | withRouter,
22 | connect(
23 | mapStateToProps,
24 | mapDispatchToProps
25 | )
26 | )
27 |
28 | const Threadslistwrappper = enhance(Threadslist);
29 |
30 | export default Threadslistwrappper;
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 kshitiz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/user-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "antd": "^4.7.0",
10 | "jwt-decode": "^3.0.0-beta.2",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-quill": "^2.0.0-beta.2",
14 | "react-redux": "^7.2.1",
15 | "react-router-dom": "^5.2.0",
16 | "react-scripts": "3.4.3",
17 | "redux": "^4.0.5",
18 | "redux-thunk": "^2.3.0"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "devDependencies": {}
42 | }
43 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/elements/Videoblot.js:
--------------------------------------------------------------------------------
1 | import { Quill } from 'react-quill';
2 |
3 | const BlockEmbed = Quill.import('blots/block/embed');
4 |
5 | class VideoBlot extends BlockEmbed {
6 |
7 | static create(value) {
8 | if (value && value.src) {
9 | const node = super.create();
10 | node.setAttribute('src', value.src);
11 | node.setAttribute('title', value.title);
12 | node.setAttribute('width', '100%');
13 | node.setAttribute('controls', '');
14 | return node;
15 | } else {
16 | const node = document.createElement('iframe');
17 | node.setAttribute('src', value);
18 | node.setAttribute('frameborder', '0');
19 | node.setAttribute('allowfullscreen', true);
20 | node.setAttribute('width', '100%');
21 | return node;
22 | }
23 | }
24 |
25 | static value(node) {
26 | return { src: node.getAttribute('src'), alt: node.getAttribute('title') };
27 | }
28 |
29 | }
30 |
31 | VideoBlot.blotName = 'video';
32 | VideoBlot.tagName = 'video';
33 | VideoBlot.calssName = 'thread-videos';
34 |
35 | export default VideoBlot;
--------------------------------------------------------------------------------
/server/models/Thread.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const threadSchema = new Schema({
5 | _id: {
6 | type: String
7 | },
8 | title: {
9 | type: String,
10 | required: true
11 | },
12 | slug: {
13 | type: String,
14 | },
15 | summary: {
16 | type: String,
17 | required: true
18 | },
19 | content: {
20 | type: String,
21 | required: true
22 | },
23 | section: {
24 | type: String,
25 | required: true
26 | },
27 | tags: {
28 | type: String,
29 | },
30 | comments: [{
31 | type: Schema.Types.ObjectId,
32 | ref: 'Comment'
33 | }],
34 | totalpoints: {
35 | type: Number,
36 | default: 0
37 | },
38 | userspoints: {
39 | type: Map,
40 | of: Number,
41 | default: {
42 | "dummy": 0
43 | }
44 | },
45 | author: {
46 | type: Schema.Types.ObjectId,
47 | ref: 'User',
48 | required: true
49 | },
50 | },
51 | { timestamps: true }
52 | )
53 |
54 | module.exports = mongoose.model('Thread', threadSchema);
55 |
--------------------------------------------------------------------------------
/user-client/src/antd-style/LICENSE:
--------------------------------------------------------------------------------
1 | MIT LICENSE
2 |
3 | Copyright (c) 2015-present Ant UED, https://xtech.antfin.com/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/Threadfullviewwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { getThreadAction, deleteThreadAction, publishCommentAction, deleteCommentAction } from '../../store/actions/Threads';
5 | import Threadfullview from './Threadfullview';
6 |
7 | const mapStateToProps = state => ({
8 | myUsername: state.Auth.username,
9 | accessLevel: state.Auth.accessLevel,
10 | token: state.Auth.token,
11 | publishCommentRequestPending: state.Threads.publishCommentRequestPending,
12 | deleteThreadRequestPending:state.Threads.deleteThreadRequestPending,
13 | deleteCommentRequestPending: state.Threads.deleteCommentRequestPending,
14 | deletedThreadId: state.Threads.deletedThreadId,
15 | thread: state.Threads.thread && state.Threads.thread
16 | });
17 |
18 | const mapDispatchToProps = {
19 | getThreadAction,
20 | deleteThreadAction,
21 | publishCommentAction,
22 | deleteCommentAction
23 | };
24 |
25 | const enhance = compose(
26 | withRouter,
27 | connect(
28 | mapStateToProps,
29 | mapDispatchToProps
30 | )
31 | );
32 |
33 | const Threadfullviewwrapper = enhance(Threadfullview);
34 |
35 | export default Threadfullviewwrapper;
36 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/Dropdownui.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Menu, Dropdown } from 'antd';
3 | import {Link} from 'react-router-dom';
4 | import { DownOutlined } from '@ant-design/icons';
5 | import classes from './Dropdownui.module.css';
6 | const Dropdownui = () => {
7 |
8 | return
9 | e.preventDefault()}>
10 | Sections
11 |
12 |
13 |
14 | }
15 |
16 | const menu = (
17 |
18 |
19 |
20 | All
21 |
22 |
23 |
24 |
25 | Books
26 |
27 |
28 |
29 |
30 | Finance
31 |
32 |
33 |
34 |
35 | Programming
36 |
37 |
38 |
39 |
40 | Science
41 |
42 |
43 |
44 |
45 | Space
46 |
47 |
48 |
49 |
50 | Technology
51 |
52 |
53 |
54 | );
55 |
56 |
57 | export default Dropdownui;
--------------------------------------------------------------------------------
/user-client/src/views/landing/Landing.module.css:
--------------------------------------------------------------------------------
1 | .Container {
2 | display: grid;
3 | grid-template-columns: repeat(12, 1fr);
4 | grid-template-rows: 50px 100px auto;
5 | }
6 |
7 | .navbarcontainer {
8 | grid-column: auto/span 12;
9 | display: flex;
10 | justify-content: space-between;
11 | align-items: center;
12 | color: white;
13 | padding: 10px;
14 | border-bottom: 1px solid #eee;
15 | background-color: #fff;
16 | -webkit-box-shadow: -1px 12px 12px -20px rgba(0,0,0,0.75);
17 | -moz-box-shadow: -1px 12px 12px -20px rgba(0,0,0,0.75);
18 | box-shadow: -1px 12px 12px -20px rgba(0,0,0,0.75);
19 | }
20 |
21 | .secondnavigation {
22 | grid-column: auto/span 12;
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | padding: 10px;
27 | border-bottom: 1px solid #eee;
28 | }
29 |
30 | .secondnavigationsectiontext {
31 | font-size: 2em;
32 | }
33 |
34 | .logocol a {
35 | display: flex;
36 | align-items: center;
37 | list-style: none;
38 | color: black;
39 | outline: none;
40 | }
41 |
42 | .logocol>a>span {
43 | margin: 5px;
44 | }
45 |
46 | .contentcontainer {
47 | grid-column: auto /span 12;
48 | display: grid;
49 | grid-template-columns: repeat(16, 1fr);
50 | }
51 |
52 | @media only screen and (min-width: 992px) {
53 | .navbarcontainer {
54 | justify-content: space-evenly;
55 | }
56 | .secondnavigation {
57 | grid-column: 3/span 8;
58 | }
59 | }
--------------------------------------------------------------------------------
/user-client/src/helper/Timesince.js:
--------------------------------------------------------------------------------
1 | const timeSince = (dateAndTime) => {
2 | let seconds = Math.floor((new Date() - dateAndTime) / 1000);
3 | let instant = seconds / 31536000;
4 |
5 | if (instant > 1) {
6 | if (instant >= 2) {
7 | return Math.floor(instant) + " years ago";
8 | }
9 | return Math.floor(instant) + " year ago";
10 | }
11 |
12 | instant = seconds / 2592000;
13 | if (instant > 1) {
14 | if (instant >= 2) {
15 | return Math.floor(instant) + " months ago";
16 | }
17 | return Math.floor(instant) + " month ago";
18 | }
19 |
20 | instant = seconds / 86400;
21 | if (instant > 1) {
22 | if (instant >= 2) {
23 | return Math.floor(instant) + " days ago";
24 | }
25 | return Math.floor(instant) + " day ago";
26 | }
27 |
28 | instant = seconds / 3600;
29 | if (instant > 1) {
30 | if (instant >= 2) {
31 | return Math.floor(instant) + " hours ago";
32 | }
33 | return Math.floor(instant) + " hour ago";
34 | }
35 |
36 | instant = seconds / 60;
37 | if (instant > 1) {
38 | if (instant >= 2) {
39 | return Math.floor(instant) + " minutes ago";
40 | }
41 | return Math.floor(instant) + " minute ago";
42 | }
43 | if (seconds <= 1) {
44 | return Math.floor(seconds) + " second ago";
45 | }
46 | return Math.floor(seconds) + " seconds ago";
47 | }
48 |
49 | export default timeSince;
50 |
51 |
--------------------------------------------------------------------------------
/user-client/src/views/publishthread/Publishthreadwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { withRouter } from 'react-router-dom';
4 | import { uploadImageAction, uploadVideoAction, uploadFileAction } from '../../store/actions/Composethread';
5 | import { publishThreadAction } from '../../store/actions/Threads';
6 | import Publishthread from './Publishthread';
7 |
8 | const mapStateToProps = state => ({
9 | token: state.Auth.token,
10 | publishThreadRequestPending: state.Threads.publishThreadRequestPending,
11 | threadPublishedAt: state.Threads.publishedThread && state.Threads.publishedThread.createdAt,
12 | slug: state.Threads.publishedThread && state.Threads.publishedThread._id,
13 | section: state.Threads.publishedThread && state.Threads.publishedThread.section,
14 | uploadedImage: state.Composethread.image && state.Composethread.image,
15 | uploadedVideo: state.Composethread.video && state.Composethread.video,
16 | uploadedFile: state.Composethread.file && state.Composethread.file,
17 | uploadPending: state.Composethread.uploadPending && state.Composethread.uploadPending,
18 | });
19 |
20 | const mapDispatchToProps = {
21 | uploadImageAction,
22 | uploadVideoAction,
23 | uploadFileAction,
24 | publishThreadAction
25 | };
26 |
27 | const enhance = compose(
28 | withRouter,
29 | connect(
30 | mapStateToProps,
31 | mapDispatchToProps
32 | )
33 | );
34 |
35 | const Publishthreadwrapper = enhance(Publishthread);
36 |
37 | export default Publishthreadwrapper;
38 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/individualthread/point/Point.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { PlusOutlined, MinusOutlined } from '@ant-design/icons'
4 | import { castPointAction } from "../../../../store/actions/Threads";
5 |
6 | export const Point = (props) => {
7 | const { compact, threadId, charge, userpoints } = props;
8 | let negativePointsColor, positivePointsColor;
9 |
10 | if (userpoints < 0) {
11 | negativePointsColor = "red";
12 | }
13 |
14 | if (userpoints > 0) {
15 | positivePointsColor = "red";
16 | }
17 |
18 | const token = useSelector(state => state.Auth.token);
19 | const weightage = useSelector(state => state.Auth.weightage);
20 | const requestPending = useSelector(state => state.Threads.castPointRequestPending);
21 | const dispatch = useDispatch();
22 |
23 | return (
24 | charge === "positive" ?
25 | { dispatch(castPointAction(compact, token, threadId, charge)) }} >
28 | {weightage}
29 | :
30 | dispatch(castPointAction(compact, token, threadId, charge))} >
33 |
34 | {weightage}
35 |
36 | );
37 | };
--------------------------------------------------------------------------------
/user-client/src/views/updatethread/Updatethreadwrapper.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { compose } from 'redux';
3 | import { updateThreadAction, getThreadAction } from '../../store/actions/Threads';
4 | import { uploadImageAction, uploadVideoAction, uploadFileAction } from '../../store/actions/Composethread';
5 | import Updatethread from './Updatethread';
6 | import { withRouter } from 'react-router-dom';
7 |
8 |
9 | const mapStateToProps = state => ({
10 | token: state.Auth.token,
11 | thread: state.Threads.thread,
12 | updateThreadRequestPending: state.Threads.updateThreadRequestPending,
13 | threadUpdatedAt: state.Threads.updatedThread && state.Threads.updatedThread.updatedAt,
14 | slug: state.Threads.updatedThread && state.Threads.updatedThread._id,
15 | section: state.Threads.updatedThread && state.Threads.updatedThread.section,
16 | uploadedImage: state.Composethread.image && state.Composethread.image,
17 | uploadedVideo: state.Composethread.video && state.Composethread.video,
18 | uploadedFile: state.Composethread.file && state.Composethread.file,
19 | uploadPending: state.Composethread.uploadPending && state.Composethread.uploadPending
20 | });
21 |
22 | const mapDispatchToProps = {
23 | getThreadAction,
24 | uploadImageAction,
25 | uploadVideoAction,
26 | uploadFileAction,
27 | updateThreadAction
28 | };
29 |
30 | const enhance = compose(
31 | withRouter,
32 | connect(
33 | mapStateToProps,
34 | mapDispatchToProps
35 | )
36 | );
37 |
38 | const Updatethreadwrapper = enhance(Updatethread);
39 |
40 | export default Updatethreadwrapper;
41 |
--------------------------------------------------------------------------------
/user-client/src/store/reducers/Composethread.js:
--------------------------------------------------------------------------------
1 | import {
2 | UPLOAD_IMAGE_REQUEST,
3 | UPLOAD_IMAGE_SUCCESS,
4 | UPLOAD_IMAGE_FAIL,
5 | UPLOAD_VIDEO_REQUEST,
6 | UPLOAD_VIDEO_SUCCESS,
7 | UPLOAD_VIDEO_FAIL,
8 | UPLOAD_FILE_REQUEST,
9 | UPLOAD_FILE_SUCCESS,
10 | UPLOAD_FILE_FAIL
11 | } from '../actions/Composethread';
12 |
13 | const initialState = {
14 | image: null,
15 | video: null,
16 | file: null
17 | }
18 |
19 | export default (state = initialState, action) => {
20 | switch (action.type) {
21 |
22 | case UPLOAD_IMAGE_REQUEST:
23 | return { ...state, uploadPending: true, image: null }
24 | case UPLOAD_IMAGE_SUCCESS:
25 | return { ...state, uploadPending: false, image: action.uploadedImage }
26 | case UPLOAD_IMAGE_FAIL:
27 | return { ...state, uploadPending: false, error: action.error }
28 |
29 | case UPLOAD_VIDEO_REQUEST:
30 | return { ...state, uploadPending: true, video: null }
31 | case UPLOAD_VIDEO_SUCCESS:
32 | return { ...state, uploadPending: false, video: action.uploadedVideo }
33 | case UPLOAD_VIDEO_FAIL:
34 | return { ...state, uploadPending: false, error: action.error }
35 |
36 | case UPLOAD_FILE_REQUEST:
37 | return { ...state, uploadPending: true, file: null }
38 | case UPLOAD_FILE_SUCCESS:
39 | return { ...state, uploadPending: false, file: action.uploadedFile }
40 | case UPLOAD_FILE_FAIL:
41 | return { ...state, uploadPending: false, error: action.error }
42 |
43 | default:
44 | return state;
45 | }
46 | };
--------------------------------------------------------------------------------
/user-client/src/store/actions/Auth.js:
--------------------------------------------------------------------------------
1 | //Naming convention : verb-noun-info
2 | import { signupUserHandler, signinUserHandler } from '../../handler/Queryhandler';
3 |
4 | export const SIGNIN_USER_REQUEST = 'SIGNIN_USER_REQUEST';
5 | export const SIGNIN_USER_SUCCESS = 'SIGNIN_USER_SUCCESS';
6 | export const SIGNIN_USER_FAIL = 'SIGNIN_USER_FAIL';
7 |
8 | export const SIGNUP_USER_REQUEST = 'SIGNUP_USER_REQUEST';
9 | export const SIGNUP_USER_SUCCESS = 'SIGNUP_USER_SUCCESS';
10 | export const SIGNUP_USER_FAIL = 'SIGNUP_USER_FAIL';
11 |
12 | export const SIGNOUT_USER_SUCCESS = 'SIGNOUT_USER_SUCCESS';
13 |
14 | const signinUserRequest = { type: SIGNIN_USER_REQUEST };
15 | const signinUserSuccess = token => ({ type: SIGNIN_USER_SUCCESS, token });
16 | const signinUserFail = error => ({ type: SIGNIN_USER_FAIL, error });
17 | export const signinUserAction = (email, password) => async dispatch => {
18 | dispatch(signinUserRequest);
19 | try {
20 | const token = await signinUserHandler(email, password);
21 | dispatch(signinUserSuccess(token));
22 | } catch (error) {
23 | dispatch(signinUserFail(error));
24 | }
25 | };
26 |
27 | const signupUserRequest = { type: SIGNUP_USER_REQUEST };
28 | const signupUserSuccess = token => ({ type: SIGNUP_USER_SUCCESS, token });
29 | const signupUserFail = error => ({ type: SIGNUP_USER_FAIL, error });
30 | export const signupUserAction = (username, email, password) => async dispatch => {
31 | dispatch(signupUserRequest);
32 | try {
33 | const token = await signupUserHandler(username, email, password);
34 | dispatch(signupUserSuccess(token));
35 | } catch (error) {
36 | dispatch(signupUserFail(error));
37 | }
38 | };
39 |
40 | export const signoutUserAction = () => ({ type: SIGNOUT_USER_SUCCESS });
41 |
--------------------------------------------------------------------------------
/user-client/src/views/signin/Signin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Input, Button, Checkbox } from 'antd';
3 | import classes from './Signin.module.css'
4 |
5 | class Signin extends React.Component {
6 | componentDidMount() {
7 | this.redirectIfLoggedIn();
8 | }
9 |
10 | componentDidUpdate() {
11 | this.redirectIfLoggedIn();
12 | }
13 |
14 | redirectIfLoggedIn() {
15 | if (this.props.token) this.props.history.push('/');
16 | }
17 |
18 | onFinish = values => {
19 | const { email, password } = values
20 | this.props.signinUserAction(email, password);
21 | };
22 |
23 | render() {
24 | return (
25 |
26 |
27 |
39 |
40 |
41 |
42 |
47 |
48 |
49 |
50 |
51 | Remember me
52 |
53 |
54 |
55 |
56 | Submit
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default Signin;
67 |
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/comments/Publishcomment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Button, Input } from 'antd';
3 | import classes from './Publishcomment.module.css';
4 | import { Link } from 'react-router-dom';
5 |
6 | const { TextArea } = Input;
7 |
8 | class Publishcomment extends Component {
9 |
10 | state = {
11 | comment: "",
12 | }
13 |
14 | componentDidUpdate(prevProp) {
15 |
16 | if (this.props.updatedAt > prevProp.updatedAt) {// If comment is published clear the text.
17 | this.setState({ comment: "" })
18 | }
19 |
20 | }
21 |
22 | onTextChange = (e) => {
23 | this.setState({ comment: e.target.value })
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 | { /* Loading Modal */
31 | !this.props.token ?
32 |
33 | Sign in to leave a comment.
37 |
38 |
39 | :
40 | null
41 | }
42 |
43 |
44 |
this.props.publishCommentAction
48 | (
49 | this.props.token,
50 | this.props.threadId,
51 | this.state.comment
52 | )
53 | }
54 | >Publish
55 |
56 | );
57 | }
58 | }
59 |
60 | export default Publishcomment;
--------------------------------------------------------------------------------
/user-client/src/store/reducers/Auth.js:
--------------------------------------------------------------------------------
1 | import jwtDecode from 'jwt-decode';
2 | import {
3 | SIGNIN_USER_REQUEST,
4 | SIGNIN_USER_SUCCESS,
5 | SIGNIN_USER_FAIL,
6 | SIGNUP_USER_REQUEST,
7 | SIGNUP_USER_SUCCESS,
8 | SIGNUP_USER_FAIL,
9 | SIGNOUT_USER_SUCCESS
10 | } from '../actions/Auth';
11 |
12 | const token = localStorage.getItem('token');
13 | const username = token && jwtDecode(token).username;
14 | const weightage = token && jwtDecode(token).weightage;
15 | const accessLevel = token && jwtDecode(token).accessLevel;
16 |
17 | const initialState = {
18 | token,
19 | username,
20 | weightage,
21 | accessLevel
22 | };
23 |
24 | export default (state = initialState, action) => {
25 |
26 | let username, weightage, accessLevel;
27 |
28 | switch (action.type) {
29 |
30 | case SIGNIN_USER_REQUEST:
31 | return { ...state, requestPending: true };
32 | case SIGNIN_USER_SUCCESS:
33 | username = jwtDecode(action.token).username;
34 | weightage = jwtDecode(action.token).weightage;
35 | accessLevel = jwtDecode(action.token).accessLevel;
36 | localStorage.setItem('token', action.token);
37 | return {
38 | ...state, token: action.token, username, weightage, accessLevel, requestPending: false
39 | };
40 | case SIGNIN_USER_FAIL:
41 | return { ...state, error: action.error, requestPending: false };
42 |
43 | case SIGNUP_USER_REQUEST:
44 | return { ...state, requestPending: true }
45 | case SIGNUP_USER_SUCCESS:
46 | username = jwtDecode(action.token).username;
47 | weightage = jwtDecode(action.token).weightage;
48 | accessLevel = jwtDecode(action.token).accessLevel;
49 | localStorage.setItem('token', action.token);
50 | return {
51 | ...state, token: action.token, newUser: true, username, weightage, accessLevel, requestPending: false
52 | };
53 | case SIGNUP_USER_FAIL:
54 | return { ...state, error: action.error, requestPending: false };
55 |
56 | case SIGNOUT_USER_SUCCESS:
57 | localStorage.removeItem('token');
58 | return { ...state, token: null, username: null, weightage: null };
59 |
60 | default:
61 | return state;
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/user-client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
20 |
21 |
30 | nat forums
31 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/individualthread/Individualthread.module.css:
--------------------------------------------------------------------------------
1 | .individualthreadcontainer {
2 | /* Width of this container is managed by parent container*/
3 | display: grid;
4 | /* grid-template-rows: 30px auto 30px; */
5 | height: auto;
6 | background-color: white;
7 | text-align: start;
8 | margin: 20px 10px;
9 | padding: 15px 20px;
10 | position: relative;
11 | }
12 |
13 | .loading {
14 | position: absolute;
15 | background-color: rgba(255, 255, 255, 1);
16 | opacity: .5;
17 | width: 100%;
18 | height: 100%;
19 | z-index: 2;
20 | }
21 |
22 | .individualthreadtopbox {
23 | display: flex;
24 | justify-content: space-between;
25 | }
26 |
27 | .leftwrapper {}
28 |
29 | .threadoptionsmenu {}
30 |
31 | .date {
32 | margin-left: 10px;
33 | color: grey;
34 | font-size: 12px;
35 | }
36 |
37 | .publishedtext {
38 | margin-left: 10px;
39 | color: grey;
40 | font-size: 12px;
41 | }
42 |
43 | .sectiontext {
44 | margin-left: 10px;
45 | color: grey!important;
46 | font-size: 12px;
47 | }
48 |
49 | .optionsmenutext {
50 | font-size: 10px;
51 | }
52 |
53 | .individualthreadcontentbox p {
54 | color: grey;
55 | }
56 |
57 | .individualthreadcontentbox img, video, iframe {
58 | width: 100%;
59 | }
60 |
61 | .individualthreadcontentboxcompact {
62 | height: 120px;
63 | overflow: hidden;
64 | position: relative;
65 | }
66 |
67 | .individualthreadcontentboxcompact p {
68 | color: #0d0d0d
69 | }
70 |
71 | .individualthreadcontentboxcompact ::after {
72 | content: "";
73 | position: absolute;
74 | top: 0;
75 | bottom: 0;
76 | left: 0px;
77 | right: 0px;
78 | -webkit-box-shadow: inset 0px -80px 57px -72px rgba(255, 255, 255, 1);
79 | -moz-box-shadow: inset 0px -80px 57px -72px rgba(255, 255, 255, 1);
80 | box-shadow: inset 0px -80px 57px -72px rgba(255, 255, 255, 1);
81 | }
82 |
83 | .individualthreadcontentboxcompact img, video, iframe {
84 | width: 100%;
85 | }
86 |
87 | .individualthreadoptionsbox {
88 | font-size: 20px;
89 | display: flex;
90 | align-items: center;
91 | justify-content: space-evenly;
92 | }
93 |
94 | @media only screen and (min-width: 992px) {}
--------------------------------------------------------------------------------
/user-client/src/views/signup/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Form, Input, Button, Checkbox } from 'antd';
3 | import classes from './Signup.module.css'
4 |
5 | class Signup extends Component {
6 | componentDidMount() {
7 | this.redirectIfLoggedIn();
8 | }
9 |
10 | componentDidUpdate() {
11 | this.redirectIfLoggedIn();
12 | }
13 |
14 | redirectIfLoggedIn() {
15 | if (this.props.token) this.props.history.push('/');
16 | }
17 |
18 | onFinish = values => {
19 | const { username, email, password } = values
20 | this.props.signupUserAction(username, email, password);
21 | };
22 |
23 | onFinishFailed = errorInfo => {
24 | };
25 |
26 | render() {
27 | return (
28 |
73 | );
74 | }
75 | }
76 |
77 | export default Signup;
78 |
--------------------------------------------------------------------------------
/user-client/src/views/frontpage/Sectioncard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import classes from './Sectioncards.module.css';
4 | import timeSince from '../../helper/Timesince';
5 | import Dimensions from '../../helper/Dimensions';
6 |
7 |
8 | const renderTitles = (i) => {
9 | return
10 |
11 | {i.title.replace(/^(.{110}[^\s]*).*/, "$1")}
12 |
13 | {i.title.length > 110 ? "..." : null}
14 |
15 |
16 | {" -" + timeSince(i.createdAt)}
17 |
18 |
19 |
20 | }
21 |
22 | const Sectioncard = (props) => {
23 | const { width } = Dimensions();
24 | const { image, title, description, meta } = props;
25 | let bgImage = (width > 991 ?
26 | null :
27 | `linear-gradient( 130deg, rgb(255, 255, 255,1) 0%, rgba(255, 255, 255, 0.8) 100%),url(${image})`
28 | );
29 |
30 | return (
31 |
32 |
33 | {width >= 992 ?
34 |
35 |
36 |
:
37 | null}
38 |
39 | {width >= 992 ?
40 |
41 |
42 |
{title}
43 |
44 |
{description}
45 |
:
46 | null}
47 |
48 |
49 |
50 | {width < 992 ?
51 |
52 |
53 | {title}
54 |
55 | :
56 | null}
57 |
58 |
59 | {title !== "All" ? "Latest Threads" : "Top Threads"}
60 |
61 | {meta.map(i => renderTitles(i))}
62 |
63 |
64 | )
65 | }
66 | export default Sectioncard;
--------------------------------------------------------------------------------
/user-client/src/store/actions/Composethread.js:
--------------------------------------------------------------------------------
1 | import { cloudUploadHandler, uploadMediaHandler, uploadFileHandler } from '../../handler/Uploadhandler';
2 |
3 | export const UPLOAD_IMAGE_REQUEST = 'UPLOAD_IMAGE_REQUEST';
4 | export const UPLOAD_IMAGE_SUCCESS = 'UPLOAD_IMAGE_SUCCESS';
5 | export const UPLOAD_IMAGE_FAIL = 'UPLOAD_IMAGE_FAIL';
6 |
7 | export const UPLOAD_VIDEO_REQUEST = 'UPLOAD_VIDEO_REQUEST';
8 | export const UPLOAD_VIDEO_SUCCESS = 'UPLOAD_VIDEO_SUCCESS';
9 | export const UPLOAD_VIDEO_FAIL = 'UPLOAD_VIDEO_FAIL';
10 |
11 | export const UPLOAD_FILE_REQUEST = 'UPLOAD_FILE_REQUEST';
12 | export const UPLOAD_FILE_SUCCESS = 'UPLOAD_FILE_SUCCESS';
13 | export const UPLOAD_FILE_FAIL = 'UPLOAD_FILE_FAIL';
14 |
15 |
16 | const uploadImageRequest = { type: UPLOAD_IMAGE_REQUEST };
17 | const uploadImageSuccess = uploadedImage => ({ type: UPLOAD_IMAGE_SUCCESS, uploadedImage });
18 | const uploadImageFail = error => ({ type: UPLOAD_IMAGE_FAIL, error });
19 | export const uploadImageAction = (token, uploadType, file) => async dispatch => {
20 | dispatch(uploadImageRequest);
21 | try {
22 | if (uploadType === "cloud") {
23 | const uploadedImage = await cloudUploadHandler(token, file);
24 | dispatch(uploadImageSuccess(uploadedImage));
25 | } else {
26 | const uploadedImage = await uploadMediaHandler(token, file);
27 | dispatch(uploadImageSuccess(uploadedImage));
28 | }
29 | } catch (error) {
30 | dispatch(uploadImageFail(error));
31 | }
32 | };
33 |
34 | const uploadVideoRequest = { type: UPLOAD_VIDEO_REQUEST };
35 | const uploadVideoSuccess = uploadedVideo => ({ type: UPLOAD_VIDEO_SUCCESS, uploadedVideo });
36 | const uploadVideoFail = error => ({ type: UPLOAD_VIDEO_FAIL, error });
37 | export const uploadVideoAction = (token, uploadType, file) => async dispatch => {
38 | dispatch(uploadVideoRequest);
39 | try {
40 | if (uploadType === "cloud") {
41 | const uploadedVideo = await cloudUploadHandler(token, file);
42 | dispatch(uploadVideoSuccess(uploadedVideo));
43 | } else {
44 | const uploadedVideo = await uploadMediaHandler(token, file);
45 | dispatch(uploadVideoSuccess(uploadedVideo));
46 | }
47 | } catch (error) {
48 | dispatch(uploadVideoFail(error));
49 | }
50 | };
51 |
52 | const uploadFileRequest = { type: UPLOAD_FILE_REQUEST };
53 | const uploadFileSuccess = uploadedFile => ({ type: UPLOAD_FILE_SUCCESS, uploadedFile });
54 | const uploadFileFail = error => ({ type: UPLOAD_FILE_FAIL, error });
55 | export const uploadFileAction = (token, uploadType, file) => async dispatch => {
56 | dispatch(uploadFileRequest);
57 | try {
58 | if (uploadType === "cloud") {
59 | const uploadedFile = await cloudUploadHandler(token, file);
60 | dispatch(uploadFileSuccess(uploadedFile));
61 | } else {
62 | const uploadedFile = await uploadFileHandler(token, file);
63 | dispatch(uploadFileSuccess(uploadedFile));
64 | }
65 | } catch (error) {
66 | dispatch(uploadFileFail(error));
67 | }
68 | };
69 |
70 |
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/Threadfullview.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { LoadingOutlined } from '@ant-design/icons';
3 | import { Spin, List } from 'antd';
4 | import Individualthread from '../shared/individualthread/Individualthread';
5 | import Individualcomment from './comments/Individualcomment';
6 | import Publishcomment from './comments/Publishcomment';
7 | import classes from './Threadfullview.module.css';
8 |
9 | const loadingIcon = ;
10 |
11 | class Threadfullview extends Component {
12 | state = {
13 | loading: true
14 | }
15 |
16 | componentDidMount() {
17 | this.props.getThreadAction(this.props.match.params.threadslug, this.props.token);
18 | }
19 |
20 | componentDidUpdate() {
21 | this.redirectIfThreadDeleted();
22 | }
23 |
24 | redirectIfThreadDeleted = () => {
25 | if (this.props.deletedThreadId && (this.props.deletedThreadId === this.props.match.params.threadslug)) {
26 | this.props.history.push('/');
27 | }
28 | }
29 |
30 | renderIndividualComments(item) {
31 | return
40 | }
41 |
42 | render() {
43 |
44 | if (!this.props.thread || this.props.thread._id !== this.props.match.params.threadslug) {
45 | return
46 |
47 | }
48 |
49 | return (
50 |
51 |
58 |
59 |
65 |
66 |
this.renderIndividualComments(item)}
69 | >
70 |
71 |
72 | )
73 |
74 | }
75 | }
76 |
77 | export default Threadfullview;
--------------------------------------------------------------------------------
/server/graphql/Schema.js:
--------------------------------------------------------------------------------
1 | const { buildSchema } = require('graphql');
2 |
3 | module.exports = buildSchema(`
4 |
5 | type Thread {
6 | _id: String!
7 | title: String!
8 | section: String!
9 | slug: String
10 | summary:String
11 | content: String
12 | author: User
13 | totalpoints: Int
14 | userpoints:Int
15 | userspoints:[AuthorsPoints]
16 | comments: [Comment!]!
17 | tags: String
18 | createdAt: String!
19 | updatedAt: String!
20 | }
21 |
22 | type AuthorsPoints{
23 | username:Int
24 | }
25 |
26 | type User {
27 | _id: ID!
28 | username: String!
29 | email: String!
30 | password: String
31 | threads: [Thread!]!
32 | }
33 |
34 | type Comment {
35 | _id: ID!
36 | commentauthor: CommentAuthor!
37 | thread: Thread!
38 | content: String!
39 | createdAt: String!
40 | updatedAt: String!
41 | }
42 |
43 | type CommentAuthor{
44 | username:String!
45 | }
46 |
47 |
48 | type AuthData {
49 | token: String!
50 | userId: String!
51 | }
52 |
53 | type ThreadsData{
54 | threads: [Thread!]!
55 | totalThreads: Int!
56 | }
57 |
58 | type Meta{
59 | _id:String
60 | title:String,
61 | section:String
62 | createdAt:String
63 | }
64 |
65 | type Sections{
66 | All:[Meta],
67 | Books:[Meta],
68 | Finance:[Meta]
69 | Programming:[Meta]
70 | Science:[Meta]
71 | Space:[Meta]
72 | Technology:[Meta]
73 | }
74 |
75 | input UserInputData {
76 | username: String!
77 | email: String!
78 | password: String!
79 | }
80 |
81 | input ThreadInputData {
82 | id:String
83 | title: String!
84 | content: String!
85 | section: String!
86 | }
87 |
88 | input CommentInputData{
89 | threadId: String!
90 | comment: String!
91 | }
92 |
93 | input PointInput{
94 | compact:Boolean
95 | threadId: String!
96 | charge: String!
97 | }
98 |
99 | type RootQuery {
100 | signin(email: String!, password: String!): AuthData!
101 | getMeta(sections: String!): Sections!
102 | getThread(slug: String!): Thread!
103 | getThreads(classifier: String!, parameter: String!, anchor:String!): ThreadsData!
104 | }
105 |
106 | type RootMutation {
107 | castPoint(pointInput: PointInput): Thread!
108 | publishThread(threadInput: ThreadInputData): Thread!
109 | createUser(userInput: UserInputData): AuthData!
110 | deleteComment(threadId: ID!, commentId: ID!): String!
111 | deleteThread(threadId: ID!): String!
112 | publishComment(commentInput: CommentInputData): Thread!
113 | updateThread(threadInput: ThreadInputData): Thread!
114 | }
115 |
116 | schema {
117 | query: RootQuery
118 | mutation: RootMutation
119 | }
120 | `);
121 |
--------------------------------------------------------------------------------
/user-client/src/handler/Uploadhandler.js:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { HOST_URL } from "../helper/Const";
3 |
4 | export const cloudUploadHandler = async (token, file) => {
5 |
6 | let signResponse;
7 | try {
8 | signResponse = await fetch(`${HOST_URL}/sign-s3?file-name=${file.name}&file-type=${file.type}`, {
9 | method: 'GET',
10 | headers: {
11 | Authorization: 'Bearer ' + token,
12 | 'Accept': 'application/json'
13 | },
14 | })
15 | }
16 | catch (error) {
17 | message.error(error);
18 | }
19 |
20 | if (signResponse.ok) {
21 | const responseJson = await signResponse.json();
22 | const signedRequest = responseJson.signedRequest;
23 | const url = responseJson.url;
24 |
25 | let cloudUploadResponse;
26 | try {
27 | cloudUploadResponse = await fetch(signedRequest, {
28 | method: 'PUT',
29 | body: file
30 | })
31 | }
32 | catch (error) {
33 | message.error(error);
34 | }
35 |
36 | if (cloudUploadResponse.ok) {
37 | const uploadedFile = {
38 | url,
39 | filename: file.name
40 | }
41 | return uploadedFile;
42 | } else {
43 | message.error("Unable to upload file.");
44 | }
45 |
46 | } else {
47 | message.error("Unable to connect to the server.");
48 | }
49 |
50 | };
51 |
52 | export const uploadMediaHandler = async (token, file) => {
53 |
54 | let formData = new FormData();
55 | formData.append("media", file);
56 |
57 | let response;
58 |
59 | try {
60 | response = await fetch(`${HOST_URL}/mediaUpload`, {
61 | method: 'POST',
62 | headers: {
63 | Authorization: 'Bearer ' + token
64 | },
65 | body: formData
66 | })
67 | }
68 | catch (error) {
69 | message.error(error);
70 | }
71 |
72 | if (response.ok) {
73 | const responseJson = await response.json();
74 | return responseJson;
75 | } else {
76 | const responseJson = await response.json();
77 | message.error(responseJson.errors[0].message);
78 | }
79 |
80 |
81 | };
82 |
83 | export const uploadFileHandler = async (token, file) => {
84 |
85 | let formData = new FormData();
86 | formData.append("file", file);
87 |
88 | let response;
89 |
90 | try {
91 | response = await fetch(`${HOST_URL}/fileUpload`, {
92 | method: 'POST',
93 | headers: {
94 | Authorization: 'Bearer ' + token
95 | },
96 | body: formData
97 | })
98 | }
99 | catch (error) {
100 | message.error(error);
101 | }
102 |
103 | if (response.ok) {
104 | const responseJson = await response.json();
105 | return responseJson;
106 | } else {
107 | const responseJson = await response.json();
108 | message.error(responseJson.errors[0].message);
109 | }
110 |
111 |
112 | };
113 |
--------------------------------------------------------------------------------
/user-client/src/views/frontpage/Sectioncards.module.css:
--------------------------------------------------------------------------------
1 | .cardcontainer {
2 | grid-column: auto/span 16;
3 | display: grid;
4 | background-color: white;
5 | margin: 10px;
6 | }
7 |
8 | .imagesection {
9 | display: none;
10 | }
11 |
12 | .textsection {
13 | display: none;
14 | }
15 |
16 | .summarysection {
17 | background-size: contain;
18 | background-repeat: no-repeat;
19 | background-position: center;
20 | background-origin: content-box;
21 | grid-column: auto/span 12;
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: space-between;
25 | text-align: start;
26 | padding: 15px;
27 | }
28 |
29 | .summarysubtext {
30 | font-size: .8em;
31 | display: inline;
32 | margin-bottom: 10px;
33 | align-self: flex-start;
34 | padding: 1px 8px;
35 | color: rgb(0, 0, 0);
36 | background-color: rgb(255, 223, 223);
37 | border-radius: 5px;
38 | }
39 |
40 | .summarytext {
41 | color: rgb(105, 105, 105);
42 | }
43 |
44 | .summarytext>p {
45 | padding-left: 10px;
46 | border-left: 1px solid rgb(255, 116, 116);
47 | }
48 |
49 | .time {
50 | font-size: small;
51 | color: rgb(145, 143, 143);
52 | }
53 |
54 | @media only screen and (min-width: 992px) {
55 | .cardcontainer {
56 | grid-template-columns: repeat(12, 1fr);
57 | grid-column: 3/span 12;
58 | }
59 | .imagesection {
60 | display: block;
61 | grid-column: auto/span 3;
62 | padding-left: 10px;
63 | border-left: 2px solid rgb(253, 203, 203);
64 | }
65 | .imagesection img {
66 | width: 100%;
67 | height: 100%;
68 | max-width: 200px;
69 | padding: 20px 0px;
70 | }
71 | .textsection {
72 | display: block;
73 | margin-left: 15px;
74 | grid-column: 4/span 4;
75 | align-self: center;
76 | text-align: start;
77 | font-size: 1em;
78 | }
79 | .textsection>a>h2 {
80 | display: inline;
81 | position: relative;
82 | border-left: 1px solid rgb(253, 203, 203);
83 | padding: 0px 10px;
84 | text-decoration: none;
85 | transition: background-color .5s ease-in-out;
86 | }
87 | .textsection>a>h2::after {
88 | z-index: 1;
89 | content: "";
90 | width: 0%;
91 | height: 100%;
92 | background: rgb(253, 203, 203, .3);
93 | bottom: 0;
94 | left: 0;
95 | position: absolute;
96 | -webkit-transition: height 250ms;
97 | transition: all 250ms;
98 | }
99 | .textsection>a>h2:hover {
100 | border-color: transparent;
101 | }
102 | .textsection>a>h2:hover::after {
103 | width: 100%;
104 | }
105 | .textsection>p {
106 | margin-top: .5em;
107 | }
108 | .summarysection {
109 | grid-column: 8/span 5;
110 | font-size: .8em;
111 | display: flex;
112 | flex-direction: column;
113 | align-self: center;
114 | justify-content: space-between;
115 | text-align: start;
116 | margin-left: 15px;
117 | padding: 15px;
118 | border-left: 2px solid #fef;
119 | }
120 | .summarytext>p {
121 | border-left: 0px;
122 | }
123 | }
--------------------------------------------------------------------------------
/user-client/src/views/threadfullview/comments/Individualcomment.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Spin, Dropdown, Menu } from 'antd';
3 | import { StopOutlined, DeleteOutlined, MoreOutlined, LoadingOutlined, UserOutlined } from '@ant-design/icons';
4 | import timeSince from '../../../helper/Timesince';
5 | import classes from './Individualcomment.module.css';
6 | import { Link } from 'react-router-dom';
7 | const loadingIcon = ;
8 |
9 | const commentOptions = (deleteCommentAction, token, _id, threadId, myUsername, accessLevel, username, deleteCommentRequestPending) => (
10 |
11 |
12 |
13 |
14 | Report
15 |
16 |
17 |
18 | { myUsername === username || accessLevel === 7 ?
19 | (
20 | deleteCommentAction(token, threadId, _id)} >
21 | Delete comment
22 |
23 |
24 | ) :
25 | null}
26 |
27 |
28 | );
29 |
30 | class Individualcomment extends Component {
31 |
32 | render() {
33 | const { deleteCommentAction,
34 | deleteCommentRequestPending,
35 | token,
36 | _id,
37 | threadId,
38 | content,
39 | createdAt,
40 | myUsername,
41 | accessLevel } = this.props;
42 | //_id is commentId, myUsername is username decoded from token
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | {this.props.commentauthor.username}
50 | {timeSince(createdAt)}
51 |
52 |
53 | {deleteCommentRequestPending === _id ?
54 |
55 |
59 |
60 |
61 | :
62 | null}
63 | commentOptions(deleteCommentAction, token, _id, threadId, myUsername, accessLevel, this.props.commentauthor.username, deleteCommentRequestPending)}>
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 | )
78 |
79 | }
80 |
81 | }
82 |
83 | export default Individualcomment;
--------------------------------------------------------------------------------
/user-client/src/views/threadslistview/Threadslist.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { LoadingOutlined } from '@ant-design/icons';
3 | import { Button, List, Skeleton} from 'antd';
4 | import Individualthread from '../shared/individualthread/Individualthread';
5 | import classes from './Threadlist.module.css';
6 |
7 | const loadingIcon = ;
8 | let classifier, parameter, anchor;
9 |
10 | class Threadslist extends PureComponent {
11 | componentDidMount() {
12 | anchor = 1;
13 | this.getThreads(anchor);
14 | }
15 |
16 | componentDidUpdate(prevProps) {
17 | if (this.props.match.url !== prevProps.match.url) {
18 | anchor = 1;
19 | this.getThreads(anchor);
20 | }
21 | }
22 |
23 | getThreads = (anchor) => {
24 |
25 | if (this.props.match.path === "/all") {
26 | classifier = "indexPage"
27 | parameter = "all";
28 | this.props.getThreadsAction(classifier, parameter, anchor, this.props.token)
29 | }
30 | if (this.props.match.params.section) {
31 | classifier = "section"
32 | parameter = this.props.match.params.section;
33 | this.props.getThreadsAction(classifier, parameter, anchor, this.props.token)
34 | }
35 | if (this.props.match.params.user) {
36 | classifier = "user"
37 | parameter = this.props.match.params.user;
38 | this.props.getThreadsAction(classifier, parameter, anchor, this.props.token)
39 | }
40 |
41 | }
42 |
43 | onLoadMore = () => {
44 | anchor = this.props.threads.length > 0 ?
45 | this.props.threads[this.props.threads.length - 1].createdAt :
46 | 1;
47 |
48 | this.getThreads(anchor);
49 | }
50 |
51 | renderIndividualThreads(item) {
52 | return (
53 |
54 |
55 |
56 |
65 |
66 |
67 |
68 | )
69 | }
70 |
71 | render() {
72 | const threads = this.props.threads;
73 | const totalThreads = this.props.totalThreads;
74 | const loadMore =
75 | !this.props.getThreadsRequestPending ? (
76 |
82 | {threads.length === totalThreads ? "That's all folks :)" : "Loading More"}
83 |
84 | ) : null;
85 |
86 | if (this.props.threads) {
87 | return (
88 |
89 | this.renderIndividualThreads(item)}
94 | >
95 |
96 |
97 | )
98 | }
99 | }
100 | }
101 |
102 | export default Threadslist;
--------------------------------------------------------------------------------
/user-client/src/views/frontpage/Frontpage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Sectioncard from './Sectioncard';
3 | import all from '../../resources/images/all.svg';
4 | import booksImage from '../../resources/images/books.svg';
5 | import financeImage from '../../resources/images/finance.svg';
6 | import programmingImage from '../../resources/images/programming.svg';
7 | import scienceImage from '../../resources/images/science.svg';
8 | import spaceImage from '../../resources/images/space.svg';
9 | import techImage from '../../resources/images/tech.svg';
10 | import { LoadingOutlined } from '@ant-design/icons';
11 | import { Spin } from 'antd';
12 | import classes from './Frontpage.module.css';
13 |
14 | const loadingIcon = ;
15 |
16 | let sections = ["Books", "Finance", "Programming", "Science", "Space", "Technology"];
17 |
18 | class Frontpage extends Component {
19 |
20 | componentDidMount() {
21 | //Get title of threads with most points.- "All" - three
22 | //Get title of latest threads of each section. - three
23 | this.props.getMetaAction(sections)
24 | }
25 |
26 | render() {
27 | if (this.props.getMetaRequestPending || !this.props.meta) {
28 | return
29 | }
30 | return (
31 |
32 |
33 |
38 |
43 |
48 |
53 |
58 |
63 |
68 |
69 |
70 | )
71 | }
72 | }
73 | export default Frontpage;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Community Forums
7 |
8 |
9 | Start your own community online with natforums.
10 |
11 |
12 | View Demo
13 | ·
14 | Report Bug
15 | ·
16 | Request Feature
17 |
18 |
19 |
20 |
21 |
22 | Table of Contents
23 |
24 |
25 | About
26 |
29 |
30 |
31 | Getting Started
32 |
36 |
37 | License
38 | Acknowledgements
39 |
40 |
41 |
42 |
43 | ## About
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Building a community around your interests is now easy with natforums. It provides you with platform and tools that make it possible to create a growing and productive community. Meanwhile, making it seamless for others to find your community and participate.
52 |
53 | Below are some of the features of natforums:
54 |
55 | * Ranking of theads to present the best of community.
56 | * Readable slugs genrated from title of the thread for better SEO.
57 | * Customize type of file uploads your user can do.
58 | * Sanitizing user genrated content for better security.
59 |
60 | ### Built With
61 |
62 | * [Create React App](https://github.com/facebook/create-react-app)
63 | * [Quill](https://quilljs.com/)
64 | * [GraphQL](https://graphql.org/)
65 | * [Express](https://expressjs.com/)
66 |
67 | ## Getting Started
68 |
69 | Use Dockerfile : [Client](https://hub.docker.com/r/izkshitiz/natclient) | [Server](https://hub.docker.com/r/izkshitiz/natserver)
70 |
71 | OR
72 |
73 | Softwares listed in prerequisites are required to be installed on your system before you can start using it on localhost.
74 |
75 | ### Prerequisites
76 |
77 | [Node.js](https://nodejs.org/en/)
78 |
79 | [mongoDB](https://www.mongodb.com/try/download/community)
80 |
81 | ### Installation
82 |
83 | 1. Download the repo as a zip and extract it or you can clone the repo using
84 | ```sh
85 | git clone https://github.com/izkshitiz/natforums.git
86 | ```
87 | 2. Once you have the repo on your system, open the terminal in root directory of the repo. Now you need to install node modules in both `server` and `user-client` folder as shown in next steps.
88 | 3. Change your directory to `user-client` in terminal and install node modules
89 |
90 | ```sh
91 | npm install
92 | ```
93 | and then run
94 |
95 | ```sh
96 | node app.js
97 | ```
98 | 4. Change the directory to `server` folder and again install node modules
99 |
100 | ```sh
101 | npm install
102 | ```
103 | and then
104 | ```sh
105 | npm start
106 | ```
107 |
108 |
109 |
110 | ## License
111 |
112 | Distributed under the MIT License. See `LICENSE` for more information.
113 |
114 |
115 |
116 | ## Acknowledgements
117 | * [DOMPurify](https://github.com/cure53/DOMPurify)
118 | * [react-quill](https://github.com/zenoamaro/react-quill)
119 | * [ant-design](https://github.com/ant-design/ant-design/)
120 |
--------------------------------------------------------------------------------
/user-client/src/views/landing/Landing.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route, Link, Switch } from 'react-router-dom';
3 | import { Menu, Button } from 'antd';
4 | import { PlusOutlined, UserOutlined } from '@ant-design/icons';
5 | import Signinwrapper from '../signin/Signinwrapper';
6 | import Signupwrapper from '../signup/Signupwrapper';
7 | import Frontpagewrapper from '../frontpage/Frontpagewrapper';
8 | import Publishthreadwrapper from '../publishthread/Publishthreadwrapper';
9 | import Updatethreadwrapper from '../updatethread/Updatethreadwrapper';
10 | import Threadfullviewwrapper from '../threadfullview/Threadfullviewwrapper';
11 | import Threadslistwrapper from '../threadslistview/Threadslistwrapper';
12 | import Dropdownui from '../shared/Dropdownui';
13 | import logo from '../../resources/default-monochrome.svg';
14 | import classes from './Landing.module.css';
15 |
16 | const { SubMenu } = Menu;
17 |
18 | class Landing extends Component {
19 | state = {
20 | current: '',
21 | }
22 |
23 | handleClick = e => {
24 | this.setState({ current: e.key });
25 | };
26 |
27 | Signout = () => {
28 | this.props.signoutUserAction();
29 | }
30 |
31 | render() {
32 | const current = this.props.history.location.pathname;
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 | {!this.props.username ?
41 |
42 | }>{/* Bug, using marginLeft to center icon */}
43 |
44 | Sign in
45 |
46 |
47 | Signup
48 |
49 |
50 | :
51 |
52 |
53 | Logout
54 |
55 | }
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
New Thread)}
68 | icon={current === "/Signin" || current === "/Signup" ? null : }
69 | >
70 |
71 |
72 |
73 |
74 |
75 | } />
76 | } />
77 | } />
78 | } />
79 | } />
80 | } />
81 | } />
82 | } />
83 | } />
84 |
85 |
86 |
87 |
88 | )
89 | }
90 | }
91 |
92 | export default Landing;
--------------------------------------------------------------------------------
/user-client/src/views/publishthread/Publishthread.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Editor from '../shared/editor/Editor';
3 | import { Button, Input, Radio } from 'antd';
4 | import classes from './Publishthread.module.css';
5 |
6 | let now;
7 |
8 | class Publishthread extends Component {
9 |
10 | state = {
11 | title: "",
12 | section: "Books", //Default value
13 | content: ""
14 | }
15 |
16 | componentDidMount() {
17 | now = + new Date();
18 | this.redirectIfNotSignedIn();
19 | }
20 |
21 | componentDidUpdate() {
22 | this.redirectIfNotSignedIn();
23 | this.redirectIfThreadSuccessfullySaved();
24 | }
25 |
26 | redirectIfNotSignedIn() {
27 | if (!this.props.token) this.props.history.push('/Signin');
28 | }
29 |
30 | redirectIfThreadSuccessfullySaved() {
31 | if (this.props.threadPublishedAt && this.props.threadPublishedAt) {
32 | // if creation time of thread in redux store is later than the component mount time, redirect to the new thread page.
33 | let redirect = Boolean(this.props.threadPublishedAt > now);
34 | if (redirect) this.props.history.push('/s/' + this.props.section + '/' + this.props.slug);
35 | }
36 |
37 | }
38 |
39 | onTitleChange = (e) => {
40 | this.setState({ title: e.target.value });
41 | }
42 |
43 | onEditorChange = (content) => {
44 | this.setState({ content });
45 | }
46 |
47 | handleSectionChange = (e) => {
48 | this.setState({ section: e.target.value });
49 | }
50 |
51 | onSubmit = (e) => {
52 | e.preventDefault();
53 | let threadData = {
54 | title: this.state.title,
55 | section: this.state.section,
56 | content: this.state.content,
57 | };
58 | this.props.publishThreadAction(threadData, this.props.token);
59 | }
60 |
61 | render() {
62 | return (
63 |
64 |
65 |
Title
66 |
67 |
Section
68 |
69 | Books
70 | Finance
71 | Programming
72 | Science
73 | Space
74 | Technology
75 |
76 |
Content
77 |
90 |
91 |
92 | Publish
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 | }
101 |
102 | export default Publishthread;
--------------------------------------------------------------------------------
/user-client/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/user-client/src/resources/default-monochrome.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/user-client/src/store/reducers/Threads.js:
--------------------------------------------------------------------------------
1 | import {
2 | PUBLISH_THREAD_REQUEST,
3 | PUBLISH_THREAD_SUCCESS,
4 | PUBLISH_THREAD_FAIL,
5 | UPDATE_THREAD_REQUEST,
6 | UPDATE_THREAD_SUCCESS,
7 | UPDATE_THREAD_FAIL,
8 | GET_THREAD_REQUEST,
9 | GET_THREAD_SUCCESS,
10 | GET_THREAD_FAIL,
11 | GET_THREADS_REQUEST,
12 | GET_THREADS_SUCCESS,
13 | GET_THREADS_FAIL,
14 | PUBLISH_COMMENT_REQUEST,
15 | PUBLISH_COMMENT_SUCCESS,
16 | PUBLISH_COMMENT_FAIL,
17 | CAST_POINT_REQUEST,
18 | CAST_POINT_SUCCESS,
19 | CAST_POINT_FAIL,
20 | DELETE_THREAD_REQUEST,
21 | DELETE_THREAD_SUCCESS,
22 | DELETE_THREAD_FAIL,
23 | DELETE_COMMENT_REQUEST,
24 | DELETE_COMMENT_SUCCESS,
25 | DELETE_COMMENT_FAIL,
26 | GET_META_REQUEST,
27 | GET_META_SUCCESS,
28 | GET_META_FAIL
29 | } from '../actions/Threads';
30 |
31 |
32 | const initialState = { requestPending: false, publishCommentRequestPending: false, threads: [] };
33 |
34 | const updateThreads = (threads, updatedThread) =>
35 | threads.map(thread => (thread._id === updatedThread._id ? updatedThread : thread));
36 |
37 | export default (state = initialState, action) => {
38 |
39 | let threads, threadsList;
40 |
41 | switch (action.type) {
42 |
43 | case GET_META_REQUEST:
44 | return { ...state, getMetaRequestPending: true, error: null };
45 | case GET_META_SUCCESS:
46 | return { ...state, getMetaRequestPending: false, meta: action.meta };
47 | case GET_META_FAIL:
48 | return { ...state, getMetaRequestPending: false, error: action.error };
49 |
50 | case PUBLISH_THREAD_REQUEST:
51 | return { ...state, publishThreadRequestPending: true, error: null };
52 | case PUBLISH_THREAD_SUCCESS:
53 | return { ...state, publishThreadRequestPending: false, publishedThread: action.newThreadData };
54 | case PUBLISH_THREAD_FAIL:
55 | return { ...state, publishThreadRequestPending: false, error: action.error };
56 |
57 | case UPDATE_THREAD_REQUEST:
58 | return { ...state, updateThreadRequestPending: true, error: null };
59 | case UPDATE_THREAD_SUCCESS:
60 | return { ...state, updateThreadRequestPending: false, updatedThread: action.newThreadData };
61 | case UPDATE_THREAD_FAIL:
62 | return { ...state, updateThreadRequestPending: false, error: action.error };
63 |
64 | case GET_THREAD_REQUEST:
65 | return { ...state, getThreadRequestPending: true, error: null };
66 | case GET_THREAD_SUCCESS:
67 | return { ...state, getThreadRequestPending: false, thread: action.thread };
68 | case GET_THREAD_FAIL:
69 | return { ...state, getThreadRequestPending: false, error: action.error };
70 |
71 | case GET_THREADS_REQUEST:
72 | if (action.anchor === 1) {
73 | return { ...state, getThreadsRequestPending: true, initRequestPending: true, threads: [], threadsData: [], error: null }
74 | }
75 | return { ...state, getThreadsRequestPending: true, threads: state.threads.concat([...new Array(5)].map(() => ({ loading: true }))), error: null }
76 | case GET_THREADS_SUCCESS:
77 | threadsList = state.threadsData.concat(action.threads.threads);
78 | return { ...state, getThreadsRequestPending: false, initRequestPending: false, threads: threadsList, threadsData: threadsList, totalThreads: action.threads.totalThreads };
79 | case GET_THREADS_FAIL:
80 | return { ...state, getThreadsRequestPending: false, initRequestPending: false, error: action.error };
81 |
82 | case PUBLISH_COMMENT_REQUEST:
83 | return { ...state, publishCommentRequestPending: true, error: null };
84 | case PUBLISH_COMMENT_SUCCESS:
85 | return { ...state, publishCommentRequestPending: false, thread: action.thread };
86 | case PUBLISH_COMMENT_FAIL:
87 | return { ...state, publishCommentRequestPending: false, error: action.error };
88 |
89 | case CAST_POINT_REQUEST:
90 | let castPointRequestPending = action.charge + action.threadId;
91 | return { ...state, castPointRequestPending, error: null };
92 | case CAST_POINT_SUCCESS:
93 | if (action.compact) {
94 | threads = updateThreads(state.threads, action.thread);
95 | return { ...state, castPointRequestPending: false, threads: threads, threadsData: threads };
96 | } else {
97 | return { ...state, castPointRequestPending: false, thread: action.thread };
98 | }
99 | case CAST_POINT_FAIL:
100 | return { ...state, castPointRequestPending: false, error: action.error };
101 |
102 | case DELETE_THREAD_REQUEST:
103 | return { ...state, deleteThreadRequestPending: action.threadId, error: null };
104 | case DELETE_THREAD_SUCCESS:
105 | threads = state.threads.filter(i => i._id !== action.threadId);
106 | return { ...state, deleteThreadRequestPending: false, threads, threadsData: threads, totalThreads: state.totalThreads - 1, thread: null, deletedThreadId: action.threadId };
107 | case DELETE_THREAD_FAIL:
108 | return { ...state, deleteThreadRequestPending: false, error: action.error };
109 |
110 | case DELETE_COMMENT_REQUEST:
111 | return { ...state, deleteCommentRequestPending: action.commentId, error: null };
112 | case DELETE_COMMENT_SUCCESS:
113 | let thread = { ...state.thread, comments: state.thread.comments.filter(i => i._id !== action.responseCommentId) };
114 | return { ...state, deleteCommentRequestPending: false, thread };
115 | case DELETE_COMMENT_FAIL:
116 | return { ...state, deleteCommentRequestPending: false, error: action.error };
117 |
118 | default:
119 | return state;
120 | }
121 | }
--------------------------------------------------------------------------------
/user-client/src/views/updatethread/Updatethread.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Editor from '../shared/editor/Editor';
3 | import { Button, Spin, Input, Radio } from 'antd';
4 | import { LoadingOutlined } from '@ant-design/icons';
5 | import classes from './Updatethread.module.css';
6 |
7 | const loadingIcon = ;
8 | let now;
9 |
10 | class Updatethread extends Component {
11 |
12 | state = {
13 | id: "",
14 | title: "",
15 | section: "",
16 | content: ""
17 | }
18 |
19 | componentDidMount() {
20 | now = + new Date();
21 | this.redirectIfNotLoggedIn();
22 | this.getThreadToEdit()
23 | }
24 |
25 | componentDidUpdate(prevProp, prevState) {
26 | this.redirectIfNotLoggedIn();
27 | if (this.props.thread) {
28 | if (prevState.id !== this.props.thread._id) { this.loadEditorWithThread() }
29 | }
30 | this.redirectIfThreadSuccessfullySaved();
31 | }
32 |
33 | loadEditorWithThread() {
34 | this.setState({
35 | id: this.props.thread._id,
36 | title: this.props.thread.title,
37 | section: this.props.thread.section,
38 | content: this.props.thread.content
39 | })
40 | }
41 |
42 | getThreadToEdit() {
43 | this.props.getThreadAction(this.props.match.params.threadslug, this.props.token)
44 | }
45 |
46 | redirectIfNotLoggedIn() {
47 | if (!this.props.token) this.props.history.push('/Signin');
48 | }
49 |
50 | redirectIfThreadSuccessfullySaved() {
51 | if (this.props.threadUpdatedAt && this.props.threadUpdatedAt) {
52 | // if thread updation time is after the component mount time, redirect to the new thread page.
53 | let redirect = Boolean(this.props.threadUpdatedAt > now);
54 | if (redirect) this.props.history.push('/s/' + this.props.section + '/' + this.props.slug);
55 | }
56 | }
57 |
58 | onTitleChange = (e) => {
59 | this.setState({ title: e.target.value })
60 | }
61 |
62 | onEditorChange = (value) => {
63 | this.setState({ content: value });
64 | }
65 |
66 | onFilesChange = (files) => {
67 | this.setState({ files });
68 | }
69 |
70 | handleSectionChange = (e) => {
71 | this.setState({ section: e.target.value })
72 | }
73 | onSubmit = (e) => {
74 | e.preventDefault();
75 | let threadData = {
76 | id: this.state.id,
77 | title: this.state.title,
78 | section: this.state.section,
79 | content: this.state.content,
80 | }
81 | this.props.updateThreadAction(threadData, this.props.token)
82 | }
83 |
84 | render() {
85 | if (!this.props.thread || this.props.thread._id !== this.props.match.params.threadslug) {
86 | return
87 | }
88 |
89 | return (
90 |
91 |
92 |
Title
93 |
94 |
Section
95 |
96 | Books
97 | Finance
98 | Programming
99 | Science
100 | Space
101 | Technology
102 |
103 |
Content
104 |
118 |
119 |
120 | Update
121 |
122 |
123 |
124 |
125 | );
126 | }
127 | }
128 |
129 | export default Updatethread;
--------------------------------------------------------------------------------
/user-client/src/serviceWorker.js:
--------------------------------------------------------------------------------
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 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/user-client/src/resources/images/tech.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/individualthread/Individualthread.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Popover, Dropdown, Menu, Spin } from 'antd';
4 | import {
5 | MoreOutlined,
6 | StopOutlined,
7 | EditOutlined,
8 | DeleteOutlined,
9 | LinkedinFilled,
10 | ShareAltOutlined,
11 | TwitterOutlined,
12 | RedditCircleFilled,
13 | FacebookFilled,
14 | CommentOutlined,
15 | UserOutlined,
16 | LoadingOutlined
17 | } from '@ant-design/icons';
18 | import { Point } from './point/Point'
19 | import timeSince from '../../../helper/Timesince';
20 | import './Individualthreadquill.css' //css to customize quill formatting.
21 | import classes from './Individualthread.module.css';
22 |
23 | const loadingIcon = ;
24 |
25 | const socialIcons = (_id, section) => (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 |
42 | const threadOptions = (_id, token, myUsername, username, accessLevel, deleteThreadAction) => (
43 |
44 |
45 |
46 | Report
47 |
48 |
49 |
50 | { myUsername === username || accessLevel === 7 ? (
51 |
52 |
53 |
54 | Edit thread
55 |
56 |
57 |
58 | deleteThreadAction(_id, token)}>
59 | Delete thread
60 |
61 |
62 |
63 | ) :
64 | null
65 | }
66 |
67 |
68 | );
69 |
70 | class Individualthread extends PureComponent {
71 |
72 | render() {
73 | const { _id,
74 | title,
75 | section,
76 | summary,
77 | content,
78 | createdAt,
79 | comments,
80 | compact,
81 | userpoints,
82 | totalpoints,
83 | token,
84 | myUsername,
85 | accessLevel,
86 | deleteThreadRequestPending,
87 | deleteThreadAction } = this.props;
88 |
89 | return (
90 |
91 |
92 | { /* Loading Modal */
93 | deleteThreadRequestPending === _id ?
94 |
95 |
99 |
100 |
101 | :
102 | null
103 | }
104 |
105 |
106 |
107 | {this.props.author.username}
108 | published
109 | {timeSince(createdAt)}
110 | in
111 | {section}
112 |
113 | threadOptions(_id, token, myUsername, this.props.author.username, accessLevel, deleteThreadAction)}>
114 |
115 |
116 | {/*Changing the style for content box depending where the component is rendered. */}
117 |
118 |
119 | {/* Render with link tag if list(compact==true) view */}
120 | {compact ?
121 |
{title}
122 |
123 | : ({title}
124 | )}
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
{" "}{comments.length}
133 |
134 |
{" " + totalpoints}
135 |
136 |
137 |
138 | )
139 |
140 | }
141 |
142 | }
143 |
144 | export default Individualthread;
--------------------------------------------------------------------------------
/user-client/src/store/actions/Threads.js:
--------------------------------------------------------------------------------
1 | import {
2 | castPointHandler,
3 | publishThreadHandler,
4 | updateThreadHandler,
5 | getThreadHandler,
6 | getThreadsHandler,
7 | publishCommentHandler,
8 | deleteThreadHandler,
9 | deleteCommentHandler,
10 | getMetaHandler
11 | } from '../../handler/Queryhandler';
12 |
13 | export const GET_META_REQUEST = 'GET_META_REQUEST';
14 | export const GET_META_SUCCESS = 'GET_META_SUCCESS';
15 | export const GET_META_FAIL = 'GET_META_FAIL';
16 |
17 | export const PUBLISH_THREAD_REQUEST = 'PUBLISH_THREAD_REQUEST';
18 | export const PUBLISH_THREAD_SUCCESS = 'PUBLISH_THREAD_SUCCESS';
19 | export const PUBLISH_THREAD_FAIL = 'PUBLISH_THREAD_FAIL';
20 |
21 | export const UPDATE_THREAD_REQUEST = 'UPDATE_THREAD_REQUEST';
22 | export const UPDATE_THREAD_SUCCESS = 'UPDATE_THREAD_SUCCESS';
23 | export const UPDATE_THREAD_FAIL = 'UPDATE_THREAD_FAIL';
24 |
25 | export const GET_THREAD_REQUEST = 'GET_THREAD_REQUEST';
26 | export const GET_THREAD_SUCCESS = 'GET_THREAD_SUCCESS';
27 | export const GET_THREAD_FAIL = 'GET_THREAD_FAIL';
28 |
29 | export const GET_THREADS_REQUEST = 'GET_THREADS_REQUEST';
30 | export const GET_THREADS_SUCCESS = 'GET_THREADS_SUCCESS';
31 | export const GET_THREADS_FAIL = 'GET_THREADS_FAIL';
32 |
33 | export const PUBLISH_COMMENT_REQUEST = 'PUBLISH_COMMENT_REQUEST';
34 | export const PUBLISH_COMMENT_SUCCESS = 'PUBLISH_COMMENT_SUCCESS';
35 | export const PUBLISH_COMMENT_FAIL = 'PUBLISH_COMMENT_FAIL';
36 |
37 | export const CAST_POINT_REQUEST = 'CAST_POINT_REQUEST';
38 | export const CAST_POINT_SUCCESS = 'CAST_POINT_SUCCESS';
39 | export const CAST_POINT_FAIL = 'CAST_POINT_FAIL';
40 |
41 | export const DELETE_THREAD_REQUEST = 'DELETE_THREAD_REQUEST';
42 | export const DELETE_THREAD_SUCCESS = 'DELETE_THREAD_SUCCESS';
43 | export const DELETE_THREAD_FAIL = 'DELETE_THREAD_FAIL';
44 |
45 | export const DELETE_COMMENT_REQUEST = 'DELETE_COMMENT_REQUEST';
46 | export const DELETE_COMMENT_SUCCESS = 'DELETE_COMMENT_SUCCESS';
47 | export const DELETE_COMMENT_FAIL = 'DELETE_COMMENT_FAIL';
48 |
49 | const getMetaRequest = { type: GET_META_REQUEST };
50 | const getMetaSuccess = meta => ({ type: GET_META_SUCCESS, meta });
51 | const getMetaFail = error => ({ type: GET_META_FAIL, error });
52 | export const getMetaAction = (sections) => async dispatch => {
53 | dispatch(getMetaRequest);
54 | try {
55 | const meta = await getMetaHandler(sections);
56 | dispatch(getMetaSuccess(meta));
57 | } catch (error) {
58 | dispatch(getMetaFail(error));
59 | }
60 | };
61 |
62 | const publishThreadRequest = { type: PUBLISH_THREAD_REQUEST };
63 | const publishThreadSuccess = newThreadData => ({ type: PUBLISH_THREAD_SUCCESS, newThreadData });
64 | const publishThreadFail = error => ({ type: PUBLISH_THREAD_FAIL, error });
65 | export const publishThreadAction = (threadData, token) => async dispatch => {
66 | dispatch(publishThreadRequest);
67 | try {
68 | const newThreadData = await publishThreadHandler(threadData, token);
69 | dispatch(publishThreadSuccess(newThreadData));
70 | } catch (error) {
71 | dispatch(publishThreadFail(error));
72 | }
73 | };
74 |
75 | const updateThreadRequest = { type: UPDATE_THREAD_REQUEST };
76 | const updateThreadSuccess = newThreadData => ({ type: UPDATE_THREAD_SUCCESS, newThreadData });
77 | const updateThreadFail = error => ({ type: UPDATE_THREAD_FAIL, error });
78 | export const updateThreadAction = (threadData, token) => async dispatch => {
79 | dispatch(updateThreadRequest);
80 | try {
81 | const newThreadData = await updateThreadHandler(threadData, token);
82 | dispatch(updateThreadSuccess(newThreadData));
83 | } catch (error) {
84 | dispatch(updateThreadFail(error));
85 | }
86 | };
87 |
88 | const getThreadRequest = { type: GET_THREAD_REQUEST };
89 | const getThreadSuccess = thread => ({ type: GET_THREAD_SUCCESS, thread });
90 | const getThreadFail = error => ({ type: GET_THREAD_FAIL, error });
91 | export const getThreadAction = (slug, token) => async dispatch => {
92 | dispatch(getThreadRequest);
93 | try {
94 | const thread = await getThreadHandler(slug, token);
95 | dispatch(getThreadSuccess(thread));
96 | } catch (error) {
97 | dispatch(getThreadFail(error));
98 | }
99 | };
100 |
101 | const getThreadsRequest = anchor => ({ type: GET_THREADS_REQUEST, anchor });
102 | const getThreadsSuccess = threads => ({ type: GET_THREADS_SUCCESS, threads });
103 | const getThreadsFail = error => ({ type: GET_THREADS_FAIL, error });
104 | export const getThreadsAction = (classifier, parameter, anchor, token) => async dispatch => {
105 | dispatch(getThreadsRequest(anchor));
106 | try {
107 | const threads = await getThreadsHandler(token, classifier, parameter, anchor);
108 | dispatch(getThreadsSuccess(threads));
109 | } catch (error) {
110 | dispatch(getThreadsFail(error));
111 | }
112 | };
113 |
114 | const publishCommentRequest = { type: PUBLISH_COMMENT_REQUEST };
115 | const publishCommentSuccess = thread => ({ type: PUBLISH_COMMENT_SUCCESS, thread });
116 | const publishCommentFail = error => ({ type: PUBLISH_COMMENT_FAIL, error });
117 | export const publishCommentAction = (token, threadId, comment) => async (dispatch) => {
118 | dispatch(publishCommentRequest);
119 | try {
120 | const thread = await publishCommentHandler(token, threadId, comment);
121 | dispatch(publishCommentSuccess(thread));
122 | } catch (error) {
123 | dispatch(publishCommentFail(error));
124 | }
125 | };
126 |
127 | const castPointRequest = (charge, threadId) => ({ type: CAST_POINT_REQUEST, charge, threadId });
128 | const castPointSuccess = (compact, thread) => ({ type: CAST_POINT_SUCCESS, compact, thread });
129 | const castPointFail = error => ({ type: CAST_POINT_FAIL, error });
130 | export const castPointAction = (compact, token, threadId, charge) => async (dispatch) => {
131 | dispatch(castPointRequest(charge, threadId));
132 | try {
133 | const thread = await castPointHandler(compact, token, threadId, charge);
134 | dispatch(castPointSuccess(compact, thread));
135 | } catch (error) {
136 | dispatch(castPointFail(error));
137 | }
138 | };
139 |
140 | const deleteThreadRequest = threadId => ({ type: DELETE_THREAD_REQUEST, threadId });
141 | const deleteThreadSuccess = threadId => ({ type: DELETE_THREAD_SUCCESS, threadId });
142 | const deleteThreadFail = error => ({ type: DELETE_THREAD_FAIL, error });
143 | export const deleteThreadAction = (threadId, token) => async (dispatch) => {
144 | dispatch(deleteThreadRequest(threadId));
145 | try {
146 | await deleteThreadHandler(threadId, token);
147 | dispatch(deleteThreadSuccess(threadId));
148 | } catch (error) {
149 | dispatch(deleteThreadFail(error));
150 | }
151 | };
152 |
153 | const deleteCommentRequest = commentId => ({ type: DELETE_COMMENT_REQUEST, commentId });
154 | const deleteCommentSuccess = responseCommentId => ({ type: DELETE_COMMENT_SUCCESS, responseCommentId });
155 | const deleteCommentFail = error => ({ type: DELETE_COMMENT_FAIL, error });
156 | export const deleteCommentAction = (token, threadId, commentId) => async (dispatch) => {
157 | dispatch(deleteCommentRequest(commentId));
158 | try {
159 | const responseCommentId = await deleteCommentHandler(token, threadId, commentId);
160 | dispatch(deleteCommentSuccess(responseCommentId));
161 | } catch (error) {
162 | dispatch(deleteCommentFail(error));
163 | }
164 | };
165 |
166 |
--------------------------------------------------------------------------------
/user-client/src/resources/images/space.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV !== 'production') {
2 | require('dotenv').config();
3 | }
4 |
5 | const express = require('express');
6 | const aws = require('aws-sdk');
7 | const app = express();
8 | const Auth = require('./auth/Auth');
9 | let fs = require('fs');
10 | const graphqlHttp = require('express-graphql').graphqlHTTP;
11 | const graphqlSchema = require('./graphql/Schema');
12 | const graphqlResolver = require('./graphql/Resolvers');
13 | const mongoose = require('mongoose');
14 | const multer = require('multer');
15 | const path = require('path');
16 | const { UPLOAD_SUPPORT } = require('./helper/Const');
17 |
18 | const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY;
19 | const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
20 | const S3_BUCKET = process.env.S3_BUCKET_NAME;
21 |
22 | const mediaMaxSize = 500000 //in Bytes ~ 500 KB
23 | const fileMaxSize = 1000000 //in Bytes ~ 1 MB
24 |
25 | app.use((req, res, next) => {
26 | res.setHeader('Access-Control-Allow-Origin', '*');
27 | res.setHeader(
28 | 'Access-Control-Allow-Methods',
29 | 'OPTIONS, GET, POST, PUT, PATCH, DELETE'
30 | );
31 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
32 |
33 | if (req.method === 'OPTIONS') {
34 | return res.sendStatus(200);
35 | }
36 |
37 | next();
38 | });
39 |
40 | //Media storage location depending on extension of file
41 | const mediaStorage = multer.diskStorage({
42 | destination: (req, file, cb) => {
43 | if (file.mimetype === 'image/png' ||
44 | file.mimetype === 'image/jpg' ||
45 | file.mimetype === 'image/jpeg' ||
46 | file.mimetype === 'image/bmp') {
47 | if (!fs.existsSync('public/images')) {
48 | fs.mkdirSync('public/images', { recursive: true });
49 | }
50 | cb(null, 'public/images')
51 | } else if (file.mimetype === 'video/mp4' ||
52 | file.mimetype === 'video/x-flv' ||
53 | file.mimetype === 'video/mpeg' ||
54 | file.mimetype === 'video/webm') {
55 | if (!fs.existsSync('public/videos')) {
56 | fs.mkdirSync('public/videos', { recursive: true });
57 | }
58 | cb(null, 'public/videos')
59 | } else {
60 | cb({ error: 'Mime type not supported' })
61 | }
62 | },
63 | filename: (req, file, cb) => {
64 | cb(null, new Date().toISOString().replace(/:/g, '-') + '-' + file.originalname);
65 | }
66 | });
67 |
68 | //Types of media files you want to accept on the server
69 | const mediaFilter = (req, file, cb) => {
70 | if (
71 | file.mimetype === 'image/png' ||
72 | file.mimetype === 'image/jpg' ||
73 | file.mimetype === 'image/jpeg' ||
74 | file.mimetype === 'image/bmp' ||
75 | file.mimetype === 'video/mp4' ||
76 | file.mimetype === 'video/x-flv' ||
77 | file.mimetype === 'video/mpeg' ||
78 | file.mimetype === 'video/webm'
79 | ) {
80 | cb(null, true);
81 | } else {
82 | cb(null, false);
83 | }
84 | };
85 |
86 | //File storage location depending on extension of file
87 | const fileStorage = multer.diskStorage({
88 | destination: (req, file, cb) => {
89 | if (file.mimetype === 'application/msword' ||
90 | file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
91 | file.mimetype === 'application/pdf' ||
92 | file.mimetype === 'application/vnd.ms-powerpoint') {
93 | if (!fs.existsSync('public/documents')) {
94 | fs.mkdirSync('public/documents', { recursive: true });
95 | }
96 | cb(null, 'public/documents')
97 | } else if (file.mimetype === 'application/java-archive' ||
98 | file.mimetype === 'application/vnd.rar' ||
99 | file.mimetype === 'application/x-7z-compressed' ||
100 | file.mimetype === 'application/vnd.android.package-archive') {
101 | if (!fs.existsSync('public/others')) {
102 | fs.mkdirSync('public/others', { recursive: true });
103 | }
104 | cb(null, 'public/others')
105 | } else {
106 | cb(null, false);
107 | }
108 | },
109 | filename: (req, file, cb) => {
110 | cb(null, new Date().toISOString().replace(/:/g, '-') + '-' + file.originalname);
111 | }
112 | });
113 |
114 | //Types of files you want to accept on the server
115 | const fileFilter = (req, file, cb) => {
116 | if (
117 | file.mimetype === 'application/msword' ||
118 | file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
119 | file.mimetype === 'application/pdf' ||
120 | file.mimetype === 'application/vnd.ms-powerpoint' ||
121 | file.mimetype === 'application/java-archive' ||
122 | file.mimetype === 'application/vnd.rar' ||
123 | file.mimetype === 'application/x-7z-compressed' ||
124 | file.mimetype === 'application/vnd.android.package-archive'
125 | ) {
126 | cb(null, true);
127 | } else {
128 | cb({ error: 'Mime type not supported' });
129 | }
130 | };
131 |
132 | let uploadMedia = multer({ storage: mediaStorage, limits: { fileSize: mediaMaxSize }, fileFilter: mediaFilter });
133 |
134 | let uploadFile = multer({ storage: fileStorage, limits: { fileSize: fileMaxSize }, fileFilter: fileFilter });
135 |
136 | app.use(Auth);
137 |
138 | const authCheck = (req, res, next) => {
139 |
140 | if (!req.isAuth) {
141 | throw new Error('Not authenticated!');
142 | }
143 |
144 | next();
145 | }
146 |
147 | const featureCheck = (req, res, next) => {
148 |
149 | if (UPLOAD_SUPPORT !== "enabled") {
150 | throw new Error('Uploads are disabled');
151 | }
152 |
153 | next();
154 | }
155 |
156 | app.use(express.static(path.join(__dirname, '/public')));
157 |
158 | app.get('/sign-s3', authCheck, featureCheck, (req, res) => {
159 | aws.config.update({
160 | accessKeyId: AWS_ACCESS_KEY,
161 | secretAccessKey: AWS_SECRET_ACCESS_KEY,
162 | })
163 |
164 | const s3 = new aws.S3();
165 | const fileName = new Date().toISOString().replace(/:/g, '-') + '-' + req.query['file-name'];
166 | const fileType = req.query['file-type'];
167 | const s3Params = {
168 | Bucket: S3_BUCKET,
169 | Key: fileName,
170 | Expires: 120,
171 | ContentType: fileType,
172 | ACL: 'public-read'
173 | };
174 |
175 | s3.getSignedUrl('putObject', s3Params, (err, data) => {
176 | if (err) {
177 | console.log(err);
178 | return res.end();
179 | }
180 | const returnData = {
181 | signedRequest: data,
182 | url: `https://${S3_BUCKET}.s3.amazonaws.com/${fileName}`
183 | };
184 | res.write(JSON.stringify(returnData));
185 | res.end();
186 | });
187 | });
188 |
189 | app.post('/mediaUpload', authCheck, featureCheck, uploadMedia.single("media"), (req, res, next) => {
190 |
191 | if (!req.file) {
192 | return res.status(200).json({ message: 'file not uploaded!' });
193 | }
194 |
195 | return res.status(201).json({ message: 'Media uploaded.', filePath: req.file.path, alt: req.file.originalname });
196 | });
197 |
198 | app.post('/fileUpload', authCheck, featureCheck, uploadFile.single("file"), (req, res, next) => {
199 |
200 | if (!req.file) {
201 | return res.status(200).json({ message: 'file not uploaded!' });
202 | }
203 |
204 | return res
205 | .status(201)
206 | .json({ message: 'Media uploaded.', filePath: req.file.path, name: req.file.originalname });
207 | });
208 |
209 | app.use('/graphql', graphqlHttp({
210 | schema: graphqlSchema,
211 | rootValue: graphqlResolver,
212 | customFormatErrorFn(error) {
213 | if (!error.originalError) {
214 | return error;
215 | }
216 | const data = error.originalError.data;
217 | const message = error.message || 'An error occurred.';
218 | const code = error.originalError.code || 500;
219 | return { message: message, status: code, data: data };
220 | }
221 | })
222 | );
223 |
224 | app.use((error, req, res, next) => {
225 | const status = error.statusCode || 500;
226 | const message = error.message;
227 | const data = error.data;
228 | res.status(status).json({ message: message, data: data });
229 | });
230 |
231 | mongoose
232 | .connect(process.env.MONGO_DB_URI || 'mongodb://localhost/nat?retryWrites=true',
233 | { useUnifiedTopology: true, useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false })
234 | .then(result => {
235 | app.listen(process.env.PORT || 8080);
236 | })
237 | .catch(error => console.log(error));
238 |
239 |
240 |
--------------------------------------------------------------------------------
/user-client/src/resources/images/finance.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/user-client/src/views/shared/editor/Editor.js:
--------------------------------------------------------------------------------
1 | //https://quilljs.com/docs/modules/toolbar/
2 | import React from 'react';
3 | import ReactDOMServer from 'react-dom/server';
4 | import { FileAddOutlined, PictureOutlined, VideoCameraAddOutlined, YoutubeOutlined, LinkOutlined, LoadingOutlined } from '@ant-design/icons';
5 | import ReactQuill, { Quill } from 'react-quill';
6 | import { message, Spin } from 'antd';
7 | import { HOST_URL, UPLOAD_TYPE, UPLOAD_SUPPORT } from '../../../helper/Const';
8 | import ImageBlot from '../elements/Imageblot';
9 | import VideoBlot from '../elements/Videoblot';
10 | import FileBlot from '../elements/Fileblot';
11 | import "react-quill/dist/quill.snow.css";
12 | import classes from './Editor.module.css';
13 |
14 | const maxMediaSize = 500000 //in Bytes ~ 500 KB
15 | const maxFileSize = 1000000 //in Bytes ~ 1 MB
16 | const loadingIcon = ;
17 | // Level of debug log
18 | Quill.debug('error');
19 |
20 | // Register custom elements with quill
21 | Quill.register(ImageBlot);
22 | Quill.register(VideoBlot);
23 | Quill.register(FileBlot);
24 |
25 | // Add or modify icons of toolbar
26 | const icons = Quill.import('ui/icons');
27 | const fileIcon = ReactDOMServer.renderToString( );
28 | const imageIcon = ReactDOMServer.renderToString( );
29 | const videoIcon = ReactDOMServer.renderToString( );
30 | const ytIcon = ReactDOMServer.renderToString( );
31 | const linkIcon = ReactDOMServer.renderToString( );
32 | icons.uploadFile = fileIcon;
33 | icons.uploadImage = imageIcon;
34 | icons.uploadVideo = videoIcon;
35 | icons.video = ytIcon;
36 | icons.link = linkIcon;
37 |
38 | // Formating and toolbar options
39 | const options = [
40 | ['bold', 'italic', 'underline', 'strike'],
41 | ['link', 'video', 'uploadImage', 'uploadVideo', 'uploadFile'],
42 |
43 | ['blockquote', 'code-block'],
44 |
45 | [{ 'list': 'ordered' }, { 'list': 'bullet' }],
46 | [{ 'indent': '-1' }, { 'indent': '+1' }],
47 | [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
48 |
49 | [{ 'color': [] }, { 'background': [] }],
50 |
51 | [{ 'align': [] }],
52 |
53 | ['clean']
54 | ];
55 |
56 |
57 |
58 | class Editor extends React.Component {
59 |
60 | constructor(props) {
61 | super(props);
62 | this.reactQuillRef = null;
63 | this.ImageRef = React.createRef();
64 | this.VideoRef = React.createRef();
65 | this.FileRef = React.createRef();
66 | }
67 |
68 | state = {
69 | editorHtml: (this.props.thread && this.props.thread.content) || ""
70 | };
71 |
72 | componentDidUpdate(prevProps) {
73 | if (this.props.uploadedImage && (prevProps.uploadedImage !== this.props.uploadedImage)) {
74 | this.addImage(this.props.uploadedImage);
75 | }
76 | if (this.props.uploadedVideo && (prevProps.uploadedVideo !== this.props.uploadedVideo)) {
77 | this.addVideo(this.props.uploadedVideo);
78 | }
79 | if (this.props.uploadedFile && (prevProps.uploadedFile !== this.props.uploadedFile)) {
80 | this.addFile(this.props.uploadedFile);
81 | }
82 | }
83 |
84 | handleChange = async (html) => {
85 | this.setState({
86 | editorHtml: html
87 | }, () => {
88 | this.props.onEditorChange(this.state.editorHtml);
89 | });
90 | };
91 |
92 | imageHandler = () => {
93 | this.ImageRef.current.click();
94 | };
95 |
96 | videoHandler = () => {
97 | this.VideoRef.current.click();
98 | };
99 |
100 | fileHandler = () => {
101 | this.FileRef.current.click();
102 | };
103 |
104 | // Media and files are uploaded to the server first and then added to the editor
105 |
106 | uploadImage = (e) => {
107 | if (e.currentTarget.files.length > 0) {
108 | if (UPLOAD_SUPPORT !== "enabled") {
109 | message.warn("Feature has been disabled temporarily in production environment. Read the docs for more info.");
110 | return;
111 | }
112 | if (e.currentTarget.files[0].size > maxMediaSize) {
113 | message.warn("Image exceeds max file size of " + maxMediaSize / 1000 + " KB");
114 | return;
115 | }
116 | const file = e.currentTarget.files[0];
117 | const token = this.props.token;
118 |
119 | this.props.uploadImageAction(token, UPLOAD_TYPE, file);
120 | } else {
121 | message.error("No image selected.");
122 | }
123 | }
124 |
125 | addImage = (uploadedImage) => {
126 | const quill = this.reactQuillRef.getEditor();
127 | quill.focus();
128 | let range = quill.getSelection();
129 | //Get the position of text cursor
130 | let position = range ? range.index : 0;
131 | if (UPLOAD_TYPE === "cloud") {
132 | quill.insertEmbed(position, "image", { src: uploadedImage.url, alt: uploadedImage.filename });
133 | } else {
134 | quill.insertEmbed(position, "image", { src: HOST_URL + "/" + uploadedImage.filePath.substring(7), alt: uploadedImage.alt.replace(/\.[^/.]+$/, "") });
135 | }
136 | quill.setSelection(position + 1);
137 | }
138 |
139 | uploadVideo = (e) => {
140 | if (e.currentTarget.files.length > 0) {
141 | if (UPLOAD_SUPPORT !== "enabled") {
142 | message.warn("Feature has been disabled temporarily in production environment. Read the docs for more info.");
143 | return;
144 | }
145 | if (e.currentTarget.files[0].size > maxMediaSize) {
146 | message.warn("Video exceeds max file size of " + maxMediaSize / 1000 + " KB");
147 | return;
148 | }
149 | const file = e.currentTarget.files[0];
150 | const token = this.props.token;
151 |
152 | this.props.uploadVideoAction(token, UPLOAD_TYPE, file)
153 | }
154 | else {
155 | message.error("No video selected");
156 | }
157 | }
158 |
159 | addVideo = (uploadedVideo) => {
160 | const quill = this.reactQuillRef.getEditor();
161 | quill.focus();
162 | let range = quill.getSelection();
163 | //Get the position of text cursor
164 | let position = range ? range.index : 0;
165 | if (UPLOAD_TYPE === "cloud") {
166 | quill.insertEmbed(position, "video", { src: uploadedVideo.url, title: uploadedVideo.filename });
167 | } else {
168 | quill.insertEmbed(position, "video", { src: HOST_URL + "/" + uploadedVideo.filePath.substring(7), title: uploadedVideo.alt.replace(/\.[^/.]+$/, "") });
169 | }
170 |
171 | quill.setSelection(position + 1);
172 | }
173 |
174 | uploadFile = (e) => {
175 | if (e.currentTarget.files.length > 0) {
176 | if (UPLOAD_SUPPORT !== "enabled") {
177 | message.warn("Feature has been disabled temporarily in production environment. Read the docs for more info.");
178 | return;
179 | }
180 | if (e.currentTarget.files[0].size > maxFileSize) {
181 | message.warn("File exceeds max file size of " + maxFileSize / 1000000 + " MB");
182 | return;
183 | }
184 | const file = e.currentTarget.files[0];
185 | const token = this.props.token;
186 |
187 | this.props.uploadFileAction(token, UPLOAD_TYPE, file);
188 | }
189 | else {
190 | message.error("No File Selected");
191 | }
192 | }
193 |
194 | addFile = (uploadedFile) => {
195 | const quill = this.reactQuillRef.getEditor();
196 | quill.focus();
197 | let range = quill.getSelection();
198 | //Get the position of text cursor
199 | let position = range ? range.index : 0;
200 | if (UPLOAD_TYPE === "cloud") {
201 | quill.insertEmbed(position, "file", { href: uploadedFile.url, name: uploadedFile.filename });
202 | } else {
203 | quill.insertEmbed(position, "file", { href: HOST_URL + "/" + uploadedFile.filePath.substring(7), name: uploadedFile.name.replace(/\.[^/.]+$/, "") });
204 | }
205 | quill.setSelection(position + 1);
206 | };
207 |
208 |
209 | modules = {
210 | syntax: true,
211 | toolbar: {
212 | container: options,
213 | handlers: {
214 | 'uploadImage': this.imageHandler,
215 | 'uploadVideo': this.videoHandler,
216 | 'uploadFile': this.fileHandler,
217 | }
218 | },
219 |
220 | };
221 |
222 | render() {
223 |
224 | return (
225 |
253 | )
254 | }
255 |
256 |
257 | }
258 |
259 | export default Editor;
--------------------------------------------------------------------------------
/user-client/src/resources/images/all.svg:
--------------------------------------------------------------------------------
1 | welcoming
--------------------------------------------------------------------------------
/user-client/src/resources/images/programming.svg:
--------------------------------------------------------------------------------
1 | code typing
--------------------------------------------------------------------------------
/user-client/src/handler/Queryhandler.js:
--------------------------------------------------------------------------------
1 | import { message } from 'antd';
2 | import { HOST_URL } from '../helper/Const';
3 |
4 | // AUTHENTICATE
5 | export const signupUserHandler = async (username, email, password) => {
6 |
7 | let response, graphqlQuery;
8 |
9 | try {
10 | graphqlQuery = {
11 | query: `
12 | mutation toCreateUser( $username: String!, $email: String!, $password: String!) {
13 | createUser(userInput:{username: $username, email: $email, password: $password}) {
14 | token,
15 | userId
16 | }
17 | }
18 | `,
19 | variables: {
20 | username: username,
21 | email: email,
22 | password: password
23 | }
24 | };
25 |
26 | response = await fetch(`${HOST_URL}/graphql`, {
27 | method: 'POST',
28 | headers: {
29 | 'Content-Type': 'application/json'
30 | },
31 | body: JSON.stringify(graphqlQuery)
32 | });
33 | } catch (error) {
34 | message.error(error);
35 | }
36 |
37 | if (response.ok) {
38 | const responseJsonData = await response.json();
39 | return responseJsonData.data.createUser.token;
40 | }
41 | else {
42 | const responseJsonData = await response.json();
43 | message.error(responseJsonData.errors[0].message);
44 | throw Error;
45 | }
46 |
47 | };
48 |
49 | export const signinUserHandler = async (email, password) => {
50 |
51 | let response, graphqlQuery;
52 |
53 | try {
54 | graphqlQuery = {
55 | query: `
56 | query toSignin($email: String!, $password: String!) {
57 | signin(email: $email, password: $password) {
58 | token,
59 | userId
60 | }
61 | }
62 | `,
63 | variables: {
64 | email: email,
65 | password: password
66 | }
67 | };
68 |
69 | response = await fetch(`${HOST_URL}/graphql`, {
70 | method: 'POST',
71 | headers: {
72 | 'Content-Type': 'application/json'
73 | },
74 | body: JSON.stringify(graphqlQuery)
75 | });
76 | } catch (error) {
77 | message.error(error);
78 | }
79 |
80 | if (response.ok) {
81 | const responseJsonData = await response.json();
82 | return responseJsonData.data.signin.token;
83 | } else {
84 | const responseJsonData = await response.json();
85 | message.error(responseJsonData.errors[0].message);
86 | throw Error;
87 | }
88 |
89 |
90 | }
91 |
92 | // THREADS
93 | export const publishThreadHandler = async (threadData, token) => {
94 |
95 | let response, graphqlQuery;
96 |
97 | try {
98 | graphqlQuery = {
99 | query: `
100 | mutation toPublishThread($title: String!, $content: String!,$section: String!){
101 | publishThread(threadInput:{title:$title,content:$content,section:$section})
102 | {
103 | _id,
104 | section,
105 | createdAt
106 | }
107 | }
108 | `,
109 | variables: {
110 | title: threadData.title,
111 | content: threadData.content,
112 | section: threadData.section
113 | }
114 | };
115 |
116 | response = await fetch(`${HOST_URL}/graphql`, {
117 | method: 'POST',
118 | headers: {
119 | Authorization: 'Bearer ' + token,
120 | 'Content-Type': 'application/json'
121 | },
122 | body: JSON.stringify(graphqlQuery)
123 | });
124 |
125 | }
126 | catch (error) {
127 | message.error(error);
128 | }
129 |
130 | if (response.ok) {
131 | const responseJsonData = await response.json();
132 | message.success('Post Created. Redirecting Now...');
133 | return responseJsonData.data.publishThread;
134 | } else {
135 | const responseJsonData = await response.json();
136 | message.error(responseJsonData.errors[0].data[0].message);
137 | throw Error;
138 | }
139 |
140 | };
141 |
142 | export const updateThreadHandler = async (threadData, token) => {
143 |
144 | let response, graphqlQuery;
145 |
146 | try {
147 | graphqlQuery = {
148 | query: `
149 | mutation toUpdateThread($id: String,$title: String!, $content: String!,$section: String!){
150 | updateThread(threadInput:{id:$id,title:$title,content:$content,section:$section})
151 | {
152 | _id,
153 | title,
154 | section,
155 | content,
156 | author{
157 | username
158 | },
159 | userpoints,
160 | createdAt,
161 | updatedAt
162 | }
163 | }
164 | `,
165 | variables: {
166 | id: threadData.id,
167 | title: threadData.title,
168 | content: threadData.content,
169 | section: threadData.section
170 | }
171 | };
172 |
173 | response = await fetch(`${HOST_URL}/graphql`, {
174 | method: 'POST',
175 | headers: {
176 | Authorization: 'Bearer ' + token,
177 | 'Content-Type': 'application/json'
178 | },
179 | body: JSON.stringify(graphqlQuery)
180 | });
181 |
182 | }
183 | catch (error) {
184 | message.error(error)
185 | }
186 |
187 | if (response.ok) {
188 | const responseJsonData = await response.json();
189 | message.success('Post Updated. Redirecting Now...');
190 | return responseJsonData.data.updateThread
191 | } else {
192 | const responseJsonData = await response.json();
193 | message.error(responseJsonData.errors[0].data[0].message);
194 | throw Error;
195 | }
196 | };
197 |
198 | export const getThreadHandler = async (slug, token) => {
199 |
200 | let response, config, graphqlQuery;
201 |
202 | if (token) {
203 | config = {
204 | Authorization: 'Bearer ' + token,
205 | 'Content-Type': 'application/json'
206 | }
207 | } else {
208 | config = {
209 | 'Content-Type': 'application/json'
210 | }
211 | }
212 |
213 | try {
214 | graphqlQuery = {
215 | query: `
216 | query toGetThread ($slug:String!) {
217 | getThread(slug:$slug){
218 | _id,
219 | title,
220 | section,
221 | content,
222 | author{
223 | username
224 | },
225 | comments{
226 | _id,
227 | commentauthor{
228 | username
229 | },
230 | content,
231 | createdAt
232 | }
233 | totalpoints,
234 | userpoints,
235 | createdAt,
236 | updatedAt
237 | }
238 | }
239 | `,
240 | variables: {
241 | slug
242 | }
243 | };
244 |
245 | response = await fetch(`${HOST_URL}/graphql`, {
246 | method: 'POST',
247 | headers: config,
248 | body: JSON.stringify(graphqlQuery)
249 | });
250 |
251 | }
252 | catch (error) {
253 | message.error(error)
254 | }
255 |
256 | if (response.ok) {
257 | const responseJsonData = await response.json();
258 | return responseJsonData.data.getThread;
259 | } else {
260 | const responseJsonData = await response.json();
261 | message.error(responseJsonData.errors[0].message);
262 | throw Error;
263 | }
264 |
265 | };
266 |
267 | export const getThreadsHandler = async (token, classifier, parameter, anchor) => {
268 |
269 | let response, config, graphqlQuery;
270 | // anchor at times, exceeds 32 bit Int limit of GraphQl so parse it as Int at server side.
271 | let anchorStr = anchor.toString();
272 |
273 | if (token) {
274 | config = {
275 | Authorization: 'Bearer ' + token,
276 | 'Content-Type': 'application/json'
277 | }
278 | } else {
279 | config = {
280 | 'Content-Type': 'application/json'
281 | }
282 | }
283 |
284 | try {
285 | graphqlQuery = {
286 | query: `
287 | query toGetThreads ($classifier:String!,$parameter:String!,$anchor:String!) {
288 | getThreads(classifier:$classifier,parameter:$parameter,anchor:$anchor){
289 | threads{
290 | _id,
291 | title,
292 | section,
293 | summary,
294 | content,
295 | comments{
296 | _id
297 | },
298 | author{
299 | username
300 | },
301 | totalpoints,
302 | userpoints,
303 | createdAt
304 | },
305 | totalThreads
306 | }
307 | }
308 | `,
309 | variables: {
310 | classifier: classifier,
311 | parameter: parameter,
312 | anchor: anchorStr
313 | }
314 | };
315 |
316 | response = await fetch(`${HOST_URL}/graphql`, {
317 | method: 'POST',
318 | headers: config,
319 | body: JSON.stringify(graphqlQuery)
320 | });
321 |
322 | }
323 | catch (error) {
324 | message.error(error)
325 | }
326 |
327 | if (response.ok) {
328 | const responseJsonData = await response.json();
329 | return responseJsonData.data.getThreads;
330 | } else {
331 | const responseJsonData = await response.json();
332 | message.error(responseJsonData.errors[0].message);
333 | throw Error;
334 | }
335 |
336 | };
337 |
338 | export const publishCommentHandler = async (token, threadId, comment) => {
339 |
340 | let response, graphqlQuery;
341 |
342 | try {
343 | graphqlQuery = {
344 | query: `
345 | mutation toPublishComment($threadId: String!, $comment: String!){
346 | publishComment(commentInput:{threadId:$threadId,comment:$comment})
347 | {
348 | _id,
349 | title,
350 | section,
351 | content,
352 | author{
353 | username
354 | },
355 | comments{
356 | _id,
357 | commentauthor{
358 | username
359 | },
360 | content,
361 | createdAt
362 | },
363 | totalpoints,
364 | userpoints,
365 | createdAt,
366 | updatedAt
367 | }
368 | }
369 | `,
370 | variables: {
371 | threadId,
372 | comment,
373 | }
374 | };
375 |
376 | response = await fetch(`${HOST_URL}/graphql`, {
377 | method: 'POST',
378 | headers: {
379 | Authorization: 'Bearer ' + token,
380 | 'Content-Type': 'application/json'
381 | },
382 | body: JSON.stringify(graphqlQuery)
383 | });
384 |
385 | }
386 | catch (error) {
387 | message.error(error);
388 | }
389 |
390 | if (response.ok) {
391 | const responseJsonData = await response.json();
392 | return responseJsonData.data.publishComment;
393 | } else {
394 | const responseJsonData = await response.json();
395 | message.error(responseJsonData.errors[0].message);
396 | throw Error;
397 | }
398 |
399 | };
400 |
401 | export const castPointHandler = async (compact, token, threadId, charge) => {
402 |
403 | let response, graphqlQuery;
404 |
405 | try {
406 | graphqlQuery = {
407 | query: `
408 | mutation toCastPoint($compact:Boolean,$threadId: String!, $charge: String!){
409 | castPoint(pointInput:{compact:$compact,threadId:$threadId,charge:$charge})
410 | {
411 | _id,
412 | title,
413 | section,
414 | ${compact ? "summary" : "content"},
415 | author{
416 | username
417 | },
418 | comments{
419 | ${compact ? `_id` :
420 | `_id,
421 | commentauthor{
422 | username
423 | },
424 | content,
425 | createdAt
426 | `}
427 | }
428 | totalpoints,
429 | userpoints,
430 | createdAt,
431 | updatedAt
432 | }
433 | }
434 | `,
435 | variables: {
436 | compact,
437 | threadId,
438 | charge
439 | }
440 | };
441 |
442 | response = await fetch(`${HOST_URL}/graphql`, {
443 | method: 'POST',
444 | headers: {
445 | Authorization: 'Bearer ' + token,
446 | 'Content-Type': 'application/json'
447 | },
448 | body: JSON.stringify(graphqlQuery)
449 | });
450 | }
451 | catch (error) {
452 | message.error(error);
453 | }
454 |
455 | if (response.ok) {
456 | const responseJsonData = await response.json();
457 | return responseJsonData.data.castPoint
458 | } else {
459 | const responseJsonData = await response.json();
460 | message.error(responseJsonData.errors[0].message);
461 | throw Error;
462 | }
463 |
464 | };
465 |
466 | // DELETE
467 | export const deleteThreadHandler = async (threadId, token) => {
468 |
469 | let response, graphqlQuery;
470 |
471 | try {
472 | graphqlQuery = {
473 | query: `
474 | mutation {
475 | deleteThread(threadId: "${threadId}")
476 | }
477 | `
478 | };
479 |
480 | response = await fetch(`${HOST_URL}/graphql`, {
481 | method: 'POST',
482 | headers: {
483 | Authorization: 'Bearer ' + token,
484 | 'Content-Type': 'application/json'
485 | },
486 | body: JSON.stringify(graphqlQuery)
487 | });
488 | }
489 | catch (error) {
490 | message.error(error);
491 | }
492 |
493 | if (response.ok) {
494 | const responseJsonData = await response.json();
495 | message.success('Thread Deleted Successfully');
496 | return responseJsonData.data.deleteThread;
497 | } else {
498 | const responseJsonData = await response.json();
499 | message.error(responseJsonData.errors[0].message);
500 | throw Error;
501 | }
502 |
503 | };
504 |
505 | export const deleteCommentHandler = async (token, threadId, commentId) => {
506 |
507 | let response, graphqlQuery;
508 |
509 | try {
510 | graphqlQuery = {
511 | query: `
512 | mutation {
513 | deleteComment(threadId: "${threadId}",commentId: "${commentId}")
514 | }
515 | `
516 | };
517 |
518 | response = await fetch(`${HOST_URL}/graphql`, {
519 | method: 'POST',
520 | headers: {
521 | Authorization: 'Bearer ' + token,
522 | 'Content-Type': 'application/json'
523 | },
524 | body: JSON.stringify(graphqlQuery)
525 | });
526 | }
527 | catch (error) {
528 | message.error(error);
529 | }
530 |
531 | if (response.ok) {
532 | const responseJsonData = await response.json();
533 | message.success('Comment Deleted Successfully');
534 | return responseJsonData.data.deleteComment
535 | } else {
536 | const responseJsonData = await response.json();
537 | message.error(responseJsonData.errors[0].message);
538 | throw Error;
539 | }
540 | };
541 |
542 | // META
543 | export const getMetaHandler = async (sections) => {
544 |
545 | let sectionsString = sections.toString();
546 | let response, graphqlQuery;
547 |
548 | try {
549 | graphqlQuery = {
550 | query: `
551 | query toGetMeta ($sections:String!) {
552 | getMeta(sections:$sections){
553 | All{
554 | _id,
555 | title,
556 | section,
557 | createdAt
558 | },
559 | Books{
560 | _id,
561 | title,
562 | section,
563 | createdAt
564 | },
565 | Finance{
566 | _id,
567 | title,
568 | section,
569 | createdAt
570 | },
571 | Programming{
572 | _id,
573 | title,
574 | section,
575 | createdAt
576 | }
577 | Science{
578 | _id,
579 | title,
580 | section,
581 | createdAt
582 | },
583 | Space{
584 | _id,
585 | title,
586 | section,
587 | createdAt
588 | },
589 | Technology{
590 | _id,
591 | title,
592 | section,
593 | createdAt
594 | }
595 | }
596 | }
597 | `,
598 | variables: {
599 | sections: sectionsString
600 | }
601 | };
602 |
603 | response = await fetch(`${HOST_URL}/graphql`, {
604 | method: 'POST',
605 | headers: {
606 | 'Content-Type': 'application/json'
607 | },
608 | body: JSON.stringify(graphqlQuery)
609 | });
610 | }
611 | catch (error) {
612 | message.error(error);
613 | }
614 |
615 | if (response.ok) {
616 | const responseJsonData = await response.json();
617 | return responseJsonData.data.getMeta;
618 | } else {
619 | const responseJsonData = await response.json();
620 | message.error(responseJsonData.errors[0].message);
621 | throw Error;
622 | }
623 |
624 | };
625 |
--------------------------------------------------------------------------------