├── 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 |
34 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | Remember me 52 | 53 | 54 | 55 | 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 |