├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_report.md
├── pull_request_template.md
└── workflows
│ └── node.js.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Project_Board.md
├── README.md
├── REQUIREMENTS.md
├── Reviewers.md
├── SECURITY.md
├── STYLE_GUIDE.md
├── TASKS-UPDATES.md
├── frontend
├── .env.example
├── .gitignore
├── .prettierrc
├── .stylelintrc
├── README.md
├── cypress.json
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── integration
│ │ └── sample_spec.js
│ ├── plugins
│ │ └── index.js
│ └── support
│ │ ├── commands.js
│ │ └── index.js
├── package-lock.json
├── package.json
├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ ├── mstile-150x150.png
│ ├── robots.txt
│ └── safari-pinned-tab.svg
└── src
│ ├── App.js
│ ├── __tests__
│ └── components
│ │ └── CreatePost.test.js
│ ├── actions
│ ├── appActions.js
│ ├── articleActions.js
│ ├── authActions.js
│ ├── local.js
│ ├── notificationsActions.js
│ ├── postActions.js
│ └── userActions.js
│ ├── assets
│ ├── images
│ │ ├── Dennis.jpg
│ │ ├── forgotpwd-svg.svg
│ │ ├── hero-dark.svg
│ │ ├── hero-light.svg
│ │ ├── mailbox.svg
│ │ └── screenshot.PNG
│ └── logo
│ │ ├── dark-logo-login.png
│ │ ├── dark-logo.png
│ │ └── light-logo.png
│ ├── common
│ ├── AuthorBox.js
│ ├── Avatar.js
│ ├── Button.js
│ ├── Card.js
│ ├── Input.js
│ ├── InputChoice.js
│ ├── Loading.js
│ ├── Message.js
│ ├── Modal.js
│ ├── ModalContentAction.js
│ ├── Tag.js
│ ├── TagInput.js
│ ├── TextArea.js
│ ├── VotingWidget.js
│ └── index.js
│ ├── components
│ ├── ArticlesCard.js
│ ├── Contributors.js
│ ├── CreateMessageModal.js
│ ├── CreatePost.js
│ ├── DiscussionsCard.js
│ ├── FeedCard.js
│ ├── FollowButton.js
│ ├── Header.js
│ ├── LoginForm.js
│ ├── Notification.js
│ ├── Page.js
│ ├── PostAction.js
│ ├── PostCard.js
│ ├── PostCardOptions.js
│ ├── PostCardPlaceholder.js
│ ├── ProfilePicCropperModal.js
│ ├── RestoreScroll.js
│ ├── SearchBox.js
│ ├── SearchByArticlesList.js
│ ├── SearchByInterestsList.js
│ ├── SearchByPanel.js
│ ├── SearchByPostsList.js
│ ├── SearchBySkillsList.js
│ ├── SearchByUsersList.js
│ ├── SidebarNav.js
│ ├── SignupForm.js
│ ├── SkillList.js
│ ├── SkillTags.js
│ ├── TopicTags.js
│ ├── UserCard.js
│ ├── UserCardPlaceholder.js
│ ├── UserSettingModalContent.js
│ ├── UserSettingUpdateModal.js
│ └── index.js
│ ├── constants
│ ├── appConstants.js
│ ├── articleConstants.js
│ ├── authConstants.js
│ ├── localConstants.js
│ ├── notificationsConstants.js
│ ├── postConstants.js
│ └── userConstants.js
│ ├── data
│ ├── articles.js
│ ├── discussions.js
│ ├── images
│ │ ├── abhijit.png
│ │ ├── cody.png
│ │ ├── default.png
│ │ ├── dennis.jpg
│ │ ├── mani.png
│ │ ├── mehdi.png
│ │ ├── mohammad.png
│ │ ├── peng.png
│ │ ├── samthefam.png
│ │ ├── shahriar.png
│ │ ├── sulamita.png
│ │ ├── ujjawal.png
│ │ └── zach.png
│ ├── index.js
│ ├── interests.js
│ ├── notifications.js
│ ├── posts.js
│ ├── skills.js
│ └── users.js
│ ├── hooks
│ ├── index.js
│ ├── useForm.js
│ ├── useLocationBlocker.js
│ └── useValidationForm.js
│ ├── index.js
│ ├── pages
│ ├── ArticlePage.js
│ ├── ArticlesPage.js
│ ├── CreateArticlePage.js
│ ├── CreateDiscussionPage.js
│ ├── DeleteAccountPage.js
│ ├── DiscussionPage.js
│ ├── Error404Page.js
│ ├── Error500Page.js
│ ├── ForgotPasswordPage.js
│ ├── HomePage.js
│ ├── InboxPage.js
│ ├── LoginSignupPage.js
│ ├── LogoutConfirmation.js
│ ├── NotificationsPage.js
│ ├── ProfilePage.js
│ ├── SearchPage.js
│ ├── TagsPage.js
│ ├── UserSettingsPage.js
│ └── index.js
│ ├── reducers
│ ├── articleReducer.js
│ ├── auth.js
│ ├── index.js
│ ├── local.js
│ ├── notificationsReducer.js
│ ├── postReducers.js
│ ├── searchBarReducer.js
│ └── userReducers.js
│ ├── reportWebVitals.js
│ ├── services
│ ├── articlesService.js
│ ├── authService.js
│ ├── config.js
│ ├── index.js
│ ├── messageService.js
│ ├── notificationsService.js
│ ├── postsService.js
│ └── usersService.js
│ ├── setupTests.js
│ ├── store.js
│ ├── styles
│ ├── App.css
│ ├── common
│ │ ├── _animation.css
│ │ ├── _author-box.css
│ │ ├── _avatar.css
│ │ ├── _base.css
│ │ ├── _button.css
│ │ ├── _card.css
│ │ ├── _form.css
│ │ ├── _loading.css
│ │ ├── _message.css
│ │ ├── _page.css
│ │ ├── _tag-input.css
│ │ ├── _tag.css
│ │ ├── _toastify.css
│ │ ├── _typography.css
│ │ ├── _utilities.css
│ │ └── _variables.css
│ ├── components
│ │ ├── ArticlePage.css
│ │ ├── CreateArticlePage.css
│ │ ├── CreateDiscussionPage.css
│ │ ├── CreatePost.css
│ │ ├── DeleteAccountPage.css
│ │ ├── DiscussionPage.css
│ │ ├── Error404Page.css
│ │ ├── Error500Page.css
│ │ ├── Feed.css
│ │ ├── ForgotPasswordPage.css
│ │ ├── HeaderBar.css
│ │ ├── HomePage.css
│ │ ├── InboxPage.css
│ │ ├── LoginOrSignUp.css
│ │ ├── LogoutConfirmation.css
│ │ ├── Modal.css
│ │ ├── NotificationPage.css
│ │ ├── NotificationTitle.css
│ │ ├── PostCardOptions.css
│ │ ├── PostCardPlaceholder.css
│ │ ├── ProfilePage.css
│ │ ├── SearchBox.css
│ │ ├── SearchByPanel.css
│ │ ├── SearchByUsersAndPostList.css
│ │ ├── SearchPage.css
│ │ ├── SidebarNav.css
│ │ ├── UserCard.css
│ │ └── UserSettingsPage.css
│ └── index.css
│ ├── uikit
│ ├── index.html
│ ├── scripts
│ │ └── app.js
│ └── styles
│ │ ├── app.css
│ │ └── modules
│ │ ├── _codeblock.css
│ │ └── _page.css
│ └── utilities
│ ├── PrivateRoute.js
│ ├── RouteHandler.js
│ ├── formatDate.js
│ ├── getImageUrl.js
│ └── index.js
└── images
├── activate-project.gif
├── dark-logo.png
├── discussion-page-lightmode.PNG
├── login-page-darkmode.png
├── login-page-lightmode.PNG
├── mumble-ui-kit.png
├── profile-page-lightmode.PNG
├── project-board.gif
├── projects-icon.PNG
├── user-feed-darkmode.png
└── user-feed-lightmode.png
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: "Use this template to report a bug you found in Mumble"
4 | title: ''
5 | labels: "bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Preflight Checklist
11 |
12 |
13 | * [ ] I have searched the issue tracker for an issue that matches the one I want to file, without success.
14 |
15 | #
16 |
17 | ### Describe the bug
18 |
19 | A clear and concise description of what the bug is.
20 |
21 | #
22 |
23 | ### To Reproduce
24 |
25 | Steps to reproduce the behavior:
26 |
27 | 1. Go to '...'
28 | 2. Click on '....'
29 | 3. Scroll down to '....'
30 | 4. See error
31 |
32 | #
33 |
34 | ### Expected behavior
35 |
36 | A clear and concise description of what you expected to happen.
37 |
38 | #
39 |
40 | ### Add additional instructions and considerations for the assignee
41 |
42 | - [ ] Jupiter
43 |
44 | - [ ] Saturn
45 |
46 | - [ ] Neptune
47 |
48 | #
49 |
50 | ### Screenshots
51 |
52 | If applicable, add screenshots to help explain your problem.
53 |
54 | #
55 |
56 | ### Additional context
57 |
58 | Add any other context about the problem here (include commit numbers and branches if relevant)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature/Enhancement request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: feature, enhancement
6 | assignees: ""
7 | ---
8 |
9 | ### Is your feature request related to a problem? Please describe.
10 |
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | #
14 |
15 | ### Describe the solution you'd like
16 |
17 | A clear and concise description of what you want to happen.
18 |
19 | #
20 |
21 | ### Describe alternatives you've considered
22 |
23 | A clear and concise description of any alternative solutions or features you've considered.
24 |
25 | #
26 |
27 | ### Add additional instructions and considerations for the assignee
28 |
29 | - [ ] Jupiter
30 |
31 | - [ ] Saturn
32 |
33 | - [ ] Neptune
34 |
35 | #
36 |
37 | ### Additional context
38 |
39 | Add any other context such as screenshots, schematics, about the feature request here.
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ### Well detailed description of the change :
10 |
11 |
12 |
13 | I worked on the .....
14 |
15 | #
16 |
17 | ### Context of the change :
18 |
19 |
20 |
21 | - Why is this change required ?
22 |
23 |
24 |
25 | - Does it solve a problem ? (please link the issue)
26 |
27 | #
28 |
29 | ### Type of change :
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | - [ ] Bug fix
39 |
40 | - [ ] New feature
41 |
42 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
43 |
44 | #
45 |
46 | ### Preview (Screenshots) :
47 |
48 |
49 |
50 |
51 |
52 |
If it is possible, please link screenshots of your changes preview !
53 |
54 |
55 | #
56 |
57 | ### Checklist:
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | - [ ] I have read the **CONTRIBUTING** document.
66 | - [ ] My code follows the **STYLE_GUIDE** of this project
67 | - [ ] I have performed a self-review of my own code
68 | - [ ] I have commented my code, particularly in hard-to-understand areas
69 | - [ ] My changes generate no new warnings
70 | - [ ] I have added tests that prove my fix is effective or that my feature works
71 | - [ ] All new and existing tests passed.
72 |
73 | #
74 |
75 | ### Reviewers
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [master]
9 | pull_request:
10 | branches: [master]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [14.x, 15.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 |
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v1
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 |
29 | - name: Installing node dependencies
30 | run: npm ci
31 | working-directory: ./frontend
32 |
33 | - name: Running ESLint Linter
34 | run: npm run lint --if-present
35 | working-directory: ./frontend
36 |
37 | - name: Running Stylelint
38 | run: npm run stylelint --if-present
39 | working-directory: ./frontend
40 |
41 | - name: Running Prettier Code Formatter
42 | run: npm run format:check --if-present
43 | working-directory: ./frontend
44 |
45 | - name: Performing React build
46 | run: npm run build --if-present
47 | working-directory: ./frontend
48 |
49 | - name: Performing React tests if any defined
50 | run: npm test
51 | working-directory: ./frontend
52 |
53 | # - name: Cypress run
54 | # uses: cypress-io/github-action@v2
55 | # with:
56 | # working-directory: ./frontend
57 | # start: npm start
58 |
--------------------------------------------------------------------------------
/Project_Board.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |
15 |
16 |
17 | ### Project Board
18 |
19 | In our repository, there is a project board named Tasks - Mumble, it helps moderators to see how is the work going.
20 |
21 |
22 | *Preview :*
23 |
24 |
25 |
26 |
27 | #
28 |
29 | ### So please, while submitting a PR or Issue, make sure to :
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Reviewers.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 |
12 |
13 |
14 |
15 | After submitting your PR, please tag reviewer(s) in your PR message. You can tag anyone below for the following.
16 |
17 |
18 |
19 | - **Markdown, Documentation, Email templates:**
20 |
21 | [@Mehdi - MidouWebDev](https://github.com/MidouWebDev)
22 |
23 | #
24 |
25 | - **Frontend, Design :**
26 |
27 | --> *Choose two reviewers :*
28 |
29 | [@Dennis Ivy](https://github.com/divanov11)
30 |
31 | [@Shahriar Parvez](https://github.com/Mr-spShuvo)
32 |
33 | [@Cody Seibert](https://github.com/codyseibert)
34 |
35 | [@Mehdi - MidouWebDev](https://github.com/MidouWebDev)
36 |
37 | #
38 |
39 | ### Need Help ?
40 |
41 | Join us in **[the Discord Server](https://discord.gg/9Du4KUY3dE)** and tag the Mumble Repo-Managers in the correct channel.
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | #
2 |
11 |
12 | ### Reporting a Vulnerability :
13 |
14 | To report a security vulnerability, please :
15 |
16 |
17 | - DM the Mumble Bot in our Discord Server
18 | - Or tell us the problem at #security-vulnerabilities
19 |
20 |
21 |
22 | *You will receive a response from us *(Moderators and Git Repo Managers)* within 24 hours*
23 |
24 | #
25 |
26 | ### Join the Mumble Community :
27 |
28 | Join our Discord Server :
29 |
--------------------------------------------------------------------------------
/TASKS-UPDATES.md:
--------------------------------------------------------------------------------
1 | # Status
2 |
3 | Note: I am uploading the code before I feel ready but I will be traveling all day tommarrow (4/1) and want everyone to have a chance to see it. Will be back at it in a day or two. Here are the area's that are incomplete at the moment.
4 |
5 | * Account settings page (src/pages/UserSettings.js) - I slapped this page together but I am open to a complete re-build. I think it looks terrible at the moment but if someone wants to keep the layout I am up for a complete re-build.
6 | * Responsiveness - I have not added any Responsiveness to the site yet but will get to that once all page layouts are complete -Asigned to:DENNIS
7 | * Discussions Page (src/pages/Discussion.js) - Page design is mostly complete but the code is clunky. Needed a re-build to clean things up and keep format
8 | * Post Comments - Need to create a comment component for posts and discussions. Layout is 30% of the way there but needs work
9 | * CSS - I have been working with a few people on the UI kit along with adding styling to the website. At some point we will need to go back through and separate all the CSS code and clean up the formatting
10 |
11 |
12 | # Pages & Components Needed:
13 | * Users Search page - Find all users on platform by name, username & interests
14 | * Topic search page - Search topics and add them to your list of interests
15 | * Article page
16 | * Header Bar - Asigned to:DENNIS
17 | * Notifications dropdown option
18 |
19 | # Action Plan
20 |
21 | At this point it looks like I'll need another day or two to to get the front end finished up. Once thats done I will spend a few days building out the database and API so we can start implementing both sides.
22 |
--------------------------------------------------------------------------------
/frontend/.env.example:
--------------------------------------------------------------------------------
1 | # Rename this file to .env.example to .env and add corresponding values 👇
2 |
3 | REACT_APP_API_ENDPOINT= # 👈 set api url here
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | /cypress/videos
9 | # testing
10 | /coverage
11 |
12 | # production
13 | #/build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | yarn.lock
27 |
--------------------------------------------------------------------------------
/frontend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "endOfLine": "lf",
5 | "jsxBracketSameLine": false,
6 | "printWidth": 100,
7 | "semi": true,
8 | "singleQuote": true,
9 | "tabWidth": 2,
10 | "trailingComma": "all",
11 | "useTabs": false
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard"
3 | }
--------------------------------------------------------------------------------
/frontend/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000"
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/cypress/integration/sample_spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | describe(
4 | 'Basic Navigation',
5 | {
6 | viewportWidth: 1400,
7 | viewportHeight: 700,
8 | },
9 | () => {
10 | it('can navigate to the login page', () => {
11 | cy.visit('/login');
12 | });
13 |
14 | it('can load the dashboard', () => {
15 | cy.visit('/');
16 | });
17 |
18 | it('can navigate to the settings page via the profile dropdown', () => {
19 | cy.get('#nav-toggle-icon').click();
20 | cy.get('#user-settings').click();
21 | cy.url().should('include', '/settings');
22 | });
23 | },
24 | );
25 |
--------------------------------------------------------------------------------
/frontend/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/frontend/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "proxy": "https://mumbleapi.herokuapp.com/",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@ckeditor/ckeditor5-build-classic": "^27.0.0",
8 | "@ckeditor/ckeditor5-react": "^3.0.2",
9 | "@testing-library/jest-dom": "^5.11.10",
10 | "@testing-library/react": "^11.2.5",
11 | "@testing-library/user-event": "^12.8.3",
12 | "axios": "^0.21.1",
13 | "classnames": "^2.3.1",
14 | "date-fns": "^2.19.0",
15 | "jwt-decode": "^3.1.2",
16 | "lodash": "^4.17.21",
17 | "prop-types": "^15.7.2",
18 | "react": "^17.0.2",
19 | "react-detect-click-outside": "^1.0.7",
20 | "react-dom": "^17.0.2",
21 | "react-error-boundary": "^3.1.1",
22 | "react-image-crop": "^8.6.6",
23 | "react-linkify": "^1.0.0-alpha",
24 | "react-placeholder": "^4.1.0",
25 | "react-redux": "^7.2.3",
26 | "react-router-dom": "^5.2.0",
27 | "react-scripts": "4.0.3",
28 | "react-toastify": "^7.0.3",
29 | "redux-devtools-extension": "^2.13.9",
30 | "redux-thunk": "^2.3.0",
31 | "web-vitals": "^1.1.1"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "start:local": "REACT_APP_API_ENDPOINT=http://127.0.0.1:8000 react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject",
39 | "lint": "eslint ./src",
40 | "cypress": "cypress run",
41 | "cypress:open": "cypress open",
42 | "format": "prettier --write \"src/**/*.(js|jsx|ts|tsx|css|scss)\"",
43 | "format:check": "prettier -c \"src/**/*.(js|jsx|ts|tsx|css|scss)\"",
44 | "stylelint": "stylelint \"src/**/*.css\"",
45 | "stylelint:fix": "stylelint \"src/**/*.css\" --fix"
46 | },
47 | "eslintConfig": {
48 | "extends": [
49 | "react-app",
50 | "react-app/jest",
51 | "plugin:react/recommended"
52 | ],
53 | "rules": {
54 | "react/prop-types": [
55 | "off"
56 | ],
57 | "react/react-in-jsx-scope": [
58 | "off"
59 | ]
60 | }
61 | },
62 | "browserslist": {
63 | "production": [
64 | ">0.2%",
65 | "not dead",
66 | "not op_mini all"
67 | ],
68 | "development": [
69 | "last 1 chrome version",
70 | "last 1 firefox version",
71 | "last 1 safari version"
72 | ]
73 | },
74 | "devDependencies": {
75 | "cypress": "^6.8.0",
76 | "eslint-plugin-react": "^7.23.1",
77 | "prettier": "^2.2.1",
78 | "stylelint": "^13.12.0",
79 | "stylelint-config-standard": "^21.0.0",
80 | "typescript": "^4.2.3"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/frontend/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/frontend/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/frontend/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/frontend/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/frontend/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/favicon-32x32.png
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 | Mumble
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
43 |
44 |
45 |
46 | You need to enable JavaScript to run this app !
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Mumble",
3 | "name": "Mumble",
4 | "icons": [
5 | {
6 | "src": "favicon-16x16.png",
7 | "sizes": "16x16",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "favicon-32x32.png",
12 | "sizes": "32x32",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "android-chrome-192x192.png",
17 | "type": "image/png",
18 | "sizes": "192x192"
19 | },
20 | {
21 | "src": "android-chrome-512x512.png",
22 | "type": "image/png",
23 | "sizes": "512x512"
24 | }
25 | ],
26 | "start_url": ".",
27 | "display": "standalone",
28 | "theme_color": "#000000",
29 | "background_color": "#ffffff"
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/public/mstile-150x150.png
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
25 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/frontend/src/__tests__/components/CreatePost.test.js:
--------------------------------------------------------------------------------
1 | import { fireEvent, render, screen } from '@testing-library/react';
2 | import { Provider } from 'react-redux';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import CreatePost from '../../components/CreatePost';
5 | import store from '../../store';
6 |
7 | test('renders MUMBLE link', () => {
8 | render(
9 |
10 |
11 |
12 |
13 | ,
14 | ,
15 | );
16 | const buttonElement = screen.getByText(/Mumble Now/);
17 | fireEvent(
18 | buttonElement,
19 | new MouseEvent('click', {
20 | bubbles: true,
21 | cancelable: true,
22 | }),
23 | );
24 | const errorWarning = screen.getByText(/Post cannot be empty!/);
25 | expect(errorWarning).toBeInTheDocument();
26 | });
27 |
--------------------------------------------------------------------------------
/frontend/src/actions/appActions.js:
--------------------------------------------------------------------------------
1 | import { SEARCH_BAR_TYPED } from '../constants/appConstants';
2 |
3 | export const searchBarTyped = (keyword = '') => async (dispatch) => {
4 | dispatch({ type: SEARCH_BAR_TYPED, payload: keyword });
5 | };
6 |
--------------------------------------------------------------------------------
/frontend/src/actions/articleActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | ARTICLE_CREATE_REQUEST,
3 | ARTICLE_CREATE_SUCCESS,
4 | ARTICLE_CREATE_FAIL,
5 | ARTICLE_GET_FAIL,
6 | ARTICLE_GET_SUCCESS,
7 | ARTICLE_GET_REQUEST,
8 | ARTICLE_SEARCH_REQUEST,
9 | ARTICLE_SEARCH_SUCCESS,
10 | ARTICLE_SEARCH_FAIL,
11 | ARTICLE_SEARCH_RESET,
12 | } from '../constants/articleConstants';
13 | import { ArticlesService } from '../services';
14 | import { createActionPayload } from './postActions';
15 |
16 | export const searchArticles = (keyword = '') => async (dispatch) => {
17 | try {
18 | dispatch({ type: ARTICLE_SEARCH_REQUEST });
19 |
20 | const data = await ArticlesService.getArticlesByKeyword(keyword);
21 |
22 | dispatch({
23 | type: ARTICLE_SEARCH_SUCCESS,
24 | payload: data,
25 | });
26 | } catch (error) {
27 | dispatch(createActionPayload(ARTICLE_SEARCH_FAIL, error));
28 | }
29 | };
30 |
31 | export const createArticle = (articleData, history) => async (dispatch, getState) => {
32 | try {
33 | dispatch({ type: ARTICLE_CREATE_REQUEST });
34 |
35 | // hard coding tags for now because backend api requires it
36 | const article = await ArticlesService.createArticle({ ...articleData, tags: '' });
37 |
38 | dispatch({
39 | type: ARTICLE_CREATE_SUCCESS,
40 | payload: article,
41 | });
42 |
43 | history.push(`/article/${article.id}`);
44 | } catch (error) {
45 | dispatch(createActionPayload(ARTICLE_CREATE_FAIL, error));
46 | }
47 | };
48 |
49 | export const resetSearchArticles = () => async (dispatch) => {
50 | dispatch({
51 | type: ARTICLE_SEARCH_RESET,
52 | });
53 | };
54 |
55 | export const getArticle = (articleId) => async (dispatch, getState) => {
56 | try {
57 | dispatch({ type: ARTICLE_GET_REQUEST });
58 |
59 | const article = await ArticlesService.getArticle(articleId);
60 |
61 | dispatch({
62 | type: ARTICLE_GET_SUCCESS,
63 | payload: article,
64 | });
65 | } catch (error) {
66 | dispatch(createActionPayload(ARTICLE_GET_FAIL, error));
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/frontend/src/actions/authActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOGIN_REQUEST,
3 | LOGIN_SUCCESS,
4 | LOGIN_FAIL,
5 | LOGOUT_REQUEST,
6 | LOGOUT_SUCCESS,
7 | LOGOUT_FAIL,
8 | REGISTER_REQUEST,
9 | REGISTER_SUCCESS,
10 | REGISTER_FAIL,
11 | REFRESH_TOKEN_SUCCESS,
12 | } from '../constants/authConstants';
13 | import authService from '../services/authService';
14 | import { createActionPayload } from './postActions';
15 |
16 | export const login = (loginCredentials) => async (dispatch) => {
17 | try {
18 | dispatch({ type: LOGIN_REQUEST });
19 |
20 | const tokens = await authService.login(loginCredentials);
21 |
22 | dispatch({
23 | type: LOGIN_SUCCESS,
24 | payload: tokens,
25 | });
26 | } catch (error) {
27 | dispatch(createActionPayload(LOGIN_FAIL, error));
28 | }
29 | };
30 |
31 | export const logout = () => async (dispatch) => {
32 | try {
33 | dispatch({ type: LOGOUT_REQUEST });
34 | dispatch({ type: LOGOUT_SUCCESS });
35 | } catch (error) {
36 | dispatch(createActionPayload(LOGOUT_FAIL, error));
37 | }
38 | };
39 |
40 | export const register = (inputs) => async (dispatch) => {
41 | try {
42 | dispatch({
43 | type: REGISTER_REQUEST,
44 | });
45 |
46 | const data = await authService.register(inputs);
47 |
48 | dispatch({
49 | type: REGISTER_SUCCESS,
50 | payload: data,
51 | });
52 |
53 | dispatch({
54 | type: LOGIN_SUCCESS,
55 | payload: data,
56 | });
57 | } catch (error) {
58 | dispatch(createActionPayload(REGISTER_FAIL, error));
59 | }
60 | };
61 |
62 | export const refreshToken = () => async (dispatch, getState) => {
63 | try {
64 | const { auth } = getState();
65 |
66 | let { access } = await authService.refreshToken(auth.refresh);
67 |
68 | dispatch({
69 | type: REFRESH_TOKEN_SUCCESS,
70 | payload: { access },
71 | });
72 | } catch (error) {
73 | dispatch(logout());
74 | }
75 | };
76 |
77 | export const deleteAccount = () => async (dispatch, getState) => {
78 | try {
79 | await authService.deleteAccount();
80 | dispatch(logout());
81 | } catch (error) {
82 | dispatch(logout());
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/frontend/src/actions/local.js:
--------------------------------------------------------------------------------
1 | import { MUMBLETHEME } from '../constants/localConstants';
2 |
3 | // Toggle Sidebar
4 | export const toggleTheme = () => async (dispatch) => {
5 | dispatch({
6 | type: MUMBLETHEME,
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/frontend/src/actions/notificationsActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | NOTIFICATIONS_REQUEST,
3 | NOTIFICATIONS_SUCCESS,
4 | NOTIFICATIONS_FAIL,
5 | READ_REQUEST,
6 | READ_SUCCESS,
7 | READ_FAIL,
8 | NOTIFICATIONS_UNREAD_REQUEST,
9 | NOTIFICATIONS_UNREAD_SUCCESS,
10 | NOTIFICATIONS_UNREAD_FAIL,
11 | } from '../constants/notificationsConstants';
12 | import { NotificationsService } from '../services';
13 |
14 | export const createActionPayload = (type, error) => ({
15 | type: type,
16 | payload:
17 | error.response && error.response.data.detail ? error.response.data.detail : error.message,
18 | });
19 |
20 | export const getNotifications = () => async (dispatch) => {
21 | try {
22 | dispatch({ type: NOTIFICATIONS_REQUEST });
23 |
24 | const notifications = await NotificationsService.getNotifications();
25 | dispatch({
26 | type: NOTIFICATIONS_SUCCESS,
27 | payload: notifications,
28 | });
29 | } catch (error) {
30 | dispatch(createActionPayload(NOTIFICATIONS_FAIL, error));
31 | }
32 | };
33 |
34 | export const getUnreadNotifications = () => async (dispatch) => {
35 | try {
36 | dispatch({ type: NOTIFICATIONS_UNREAD_REQUEST });
37 |
38 | const unreadNotifications = await NotificationsService.getUnreadNotifications();
39 | dispatch({
40 | type: NOTIFICATIONS_UNREAD_SUCCESS,
41 | payload: unreadNotifications,
42 | });
43 | } catch (error) {
44 | dispatch(createActionPayload(NOTIFICATIONS_UNREAD_FAIL, error));
45 | }
46 | };
47 |
48 | export const markAsRead = (notificationId) => async (dispatch) => {
49 | try {
50 | dispatch({ type: READ_REQUEST });
51 |
52 | await NotificationsService.markAsRead(notificationId);
53 | dispatch({
54 | type: READ_SUCCESS,
55 | });
56 |
57 | dispatch(getUnreadNotifications());
58 | } catch (error) {
59 | dispatch(createActionPayload(READ_FAIL, error));
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/Dennis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/assets/images/Dennis.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/images/screenshot.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/assets/images/screenshot.PNG
--------------------------------------------------------------------------------
/frontend/src/assets/logo/dark-logo-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/assets/logo/dark-logo-login.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo/dark-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/assets/logo/dark-logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo/light-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/assets/logo/light-logo.png
--------------------------------------------------------------------------------
/frontend/src/common/AuthorBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import PropTypes from 'prop-types';
4 | import classNames from 'classnames';
5 |
6 | import Avatar from './Avatar';
7 |
8 | const AuthorBox = ({
9 | avatarSrc = '',
10 | className = '',
11 | handle = '',
12 | name = '',
13 | size = 'sm',
14 | url = '',
15 | ...others
16 | }) => {
17 | return (
18 |
23 |
24 |
25 |
{name}
26 | {handle &&
@{handle} }
27 |
28 |
29 | );
30 | };
31 |
32 | AuthorBox.propTypes = {
33 | avatarSrc: PropTypes.string,
34 | className: PropTypes.string,
35 | handle: PropTypes.string,
36 | name: PropTypes.string.isRequired,
37 | size: PropTypes.oneOf(['sm', 'md', 'lg']),
38 | url: PropTypes.string.isRequired,
39 | };
40 |
41 | export default AuthorBox;
42 |
--------------------------------------------------------------------------------
/frontend/src/common/Avatar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Avatar = ({
6 | alt = 'User Avatar',
7 | className = '',
8 | size = 'md',
9 | src = 'https://randomuser.me/api/portraits/men/52.jpg',
10 | ...others
11 | }) => {
12 | return (
13 |
19 | );
20 | };
21 |
22 | Avatar.propTypes = {
23 | alt: PropTypes.string,
24 | className: PropTypes.string,
25 | size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
26 | src: PropTypes.string,
27 | };
28 |
29 | export default Avatar;
30 |
--------------------------------------------------------------------------------
/frontend/src/common/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Button = ({
6 | children,
7 | className = '',
8 | color = '',
9 | iconName = '',
10 | iconStyle = 'fas',
11 | link = false,
12 | outline = false,
13 | size = '',
14 | text = '',
15 | loading = false,
16 | ...others
17 | }) => {
18 | return (
19 |
33 | {iconName && }
34 | {loading ? (
35 |
36 | ) : (
37 | <>
38 | {text} {children}
39 | >
40 | )}
41 |
42 | );
43 | };
44 |
45 | Button.propTypes = {
46 | className: PropTypes.string,
47 | color: PropTypes.oneOf(['main', 'sub', 'warning']),
48 | iconName: PropTypes.string,
49 | iconStyle: PropTypes.string,
50 | link: PropTypes.bool,
51 | outline: PropTypes.bool,
52 | size: PropTypes.oneOf(['sm', 'md', 'lg']),
53 | text: PropTypes.string,
54 | };
55 |
56 | export default Button;
57 |
--------------------------------------------------------------------------------
/frontend/src/common/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Card = ({ cardStyle = '', className = '', children, ...others }) => {
6 | return (
7 |
15 | );
16 | };
17 |
18 | Card.propTypes = {
19 | cardStyle: PropTypes.string,
20 | className: PropTypes.string,
21 | };
22 |
23 | export default Card;
24 |
--------------------------------------------------------------------------------
/frontend/src/common/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Input = ({
6 | className = '',
7 | error = '',
8 | hideLabel = false,
9 | label = '',
10 | name,
11 | placeholder = '',
12 | type = 'text',
13 | value = '',
14 | ...others
15 | }) => {
16 | return (
17 |
18 |
22 | {label}
23 |
24 |
33 | {error && {error} }
34 |
35 | );
36 | };
37 |
38 | Input.propTypes = {
39 | className: PropTypes.string,
40 | error: PropTypes.string,
41 | hideLabel: PropTypes.bool,
42 | label: PropTypes.string.isRequired,
43 | name: PropTypes.string.isRequired,
44 | placeholder: PropTypes.string,
45 | type: PropTypes.oneOf(['text', 'email', 'password', 'number', 'date', 'time']),
46 | value: PropTypes.string,
47 | };
48 |
49 | export default Input;
50 |
--------------------------------------------------------------------------------
/frontend/src/common/InputChoice.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Input = ({
6 | className = '',
7 | error,
8 | hideLabel = false,
9 | label = '',
10 | name,
11 | options = [],
12 | type = 'radio',
13 | ...others
14 | }) => {
15 | return (
16 |
17 |
{label}:
18 | {options.map((option) => (
19 |
20 |
28 | {option?.title}
29 |
30 | ))}
31 | {error &&
{error} }
32 |
33 | );
34 | };
35 |
36 | Input.propTypes = {
37 | className: PropTypes.string,
38 | error: PropTypes.string,
39 | hideLabel: PropTypes.bool,
40 | label: PropTypes.string.isRequired,
41 | name: PropTypes.string.isRequired,
42 | options: PropTypes.arrayOf(
43 | PropTypes.shape({
44 | title: PropTypes.string.isRequired,
45 | value: PropTypes.string.isRequired,
46 | }),
47 | ).isRequired,
48 | type: PropTypes.oneOf(['checkbox', 'radio']),
49 | };
50 |
51 | export default Input;
52 |
--------------------------------------------------------------------------------
/frontend/src/common/Loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import logo from '../assets/logo/dark-logo-login.png';
4 |
5 | const Loading = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export default Loading;
21 |
--------------------------------------------------------------------------------
/frontend/src/common/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Message = ({ onClose, variant, dismissible, children }) => {
4 | return (
5 |
6 | {children}
7 | {dismissible &&
✕
}
8 |
9 | );
10 | };
11 |
12 | export default Message;
13 |
--------------------------------------------------------------------------------
/frontend/src/common/Modal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import { useEffect, useRef } from 'react';
4 |
5 | import '../styles/components/Modal.css';
6 |
7 | // TODO: Make it universal and modular
8 |
9 | const Modal = ({ heading, children, active, setActive }) => {
10 | const modalRef = useRef();
11 |
12 | const closeModal = () => {
13 | setActive(false);
14 | };
15 |
16 | const MODAL_TRANSITION_TIME = 200;
17 |
18 | const fadeIn = (el) => {
19 | el.style.display = 'flex';
20 | setTimeout(() => {
21 | el.style.opacity = '1';
22 | }, 30);
23 | };
24 |
25 | const fadeOut = (el) => {
26 | el.style.opacity = '0';
27 | setTimeout(() => {
28 | el.style.display = 'none';
29 | }, MODAL_TRANSITION_TIME);
30 | };
31 |
32 | useEffect(() => {
33 | const modalEl = modalRef.current;
34 | active ? fadeIn(modalEl) : fadeOut(modalEl);
35 | }, [active]);
36 |
37 | return ReactDom.createPortal(
38 |
39 |
40 |
41 |
{heading}
42 |
43 |
44 |
45 |
46 |
47 |
{children}
48 |
49 |
,
50 | document.getElementById('modal'),
51 | );
52 | };
53 |
54 | export default Modal;
55 |
--------------------------------------------------------------------------------
/frontend/src/common/ModalContentAction.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ModalContentAction = ({ confirmLabel = 'Update', setActive, successAction }) => {
4 | const handleCancel = (e) => {
5 | e.preventDefault();
6 | setActive(false);
7 | };
8 |
9 | const handleSuccess = () => {
10 | if (typeof successAction === 'function') {
11 | successAction();
12 | }
13 | };
14 |
15 | return (
16 |
17 | handleSuccess()}
21 | >
22 | {confirmLabel}
23 |
24 |
25 | Cancel
26 |
27 |
28 | );
29 | };
30 |
31 | export default ModalContentAction;
32 |
--------------------------------------------------------------------------------
/frontend/src/common/Tag.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | const Tag = ({ className = '', text, outline = false, ...others }) => {
6 | return (
7 |
8 | {text}
9 |
10 | );
11 | };
12 |
13 | Tag.propTypes = {
14 | className: PropTypes.string,
15 | outline: PropTypes.bool,
16 | text: PropTypes.string.isRequired,
17 | };
18 |
19 | export default Tag;
20 |
--------------------------------------------------------------------------------
/frontend/src/common/TagInput.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const TagInput = ({ tags, setTags }) => {
5 | const [input, setInput] = useState('');
6 |
7 | const addTags = (e) => {
8 | e.stopPropagation();
9 | const value = e.key === ',' ? input.substring(0, input.length - 1) : input;
10 | if (value && (e.key === 'Enter' || e.key === ',')) {
11 | setInput('');
12 | setTags([...tags, { name: value }]);
13 | }
14 | };
15 |
16 | const removeTag = (tagToRemove) => {
17 | setTags((tags) => {
18 | const newState = [...tags.filter((tag) => tag !== tagToRemove)];
19 | return newState;
20 | });
21 | };
22 |
23 | const renderTags = tags.map((tag) => {
24 | return (
25 |
26 | {tag.name}
27 | removeTag(tag)} />
28 |
29 | );
30 | });
31 |
32 | return (
33 | document.querySelector('.tag-input').focus()}>
34 |
{renderTags}
35 |
setInput(e.target.value)}
40 | onKeyUp={addTags}
41 | placeholder="Press enter to add a tag"
42 | />
43 |
44 | );
45 | };
46 |
47 | TagInput.propTypes = {
48 | tags: PropTypes.arrayOf(PropTypes.array).isRequired,
49 | setTags: PropTypes.arrayOf(PropTypes.func).isRequired,
50 | };
51 |
52 | export default TagInput;
53 |
--------------------------------------------------------------------------------
/frontend/src/common/TextArea.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import PropTypes from 'prop-types';
4 |
5 | const TextArea = ({
6 | className = '',
7 | defaultValue = '',
8 | error = '',
9 | hideLabel = false,
10 | label = '',
11 | name,
12 | placeholder = 'Share you mumble thoughts...',
13 | value = '',
14 | ...others
15 | }) => {
16 | return (
17 |
18 |
22 | {label}
23 |
24 |
35 | {error && {error} }
36 |
37 | );
38 | };
39 |
40 | TextArea.propTypes = {
41 | className: PropTypes.string,
42 | defaultValue: PropTypes.string,
43 | error: PropTypes.string,
44 | hideLabel: PropTypes.bool,
45 | label: PropTypes.string.isRequired,
46 | name: PropTypes.string.isRequired,
47 | placeholder: PropTypes.string,
48 | value: PropTypes.string,
49 | };
50 |
51 | export default TextArea;
52 |
--------------------------------------------------------------------------------
/frontend/src/common/VotingWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useDispatch } from 'react-redux';
4 |
5 | import { modifyVote } from '../actions/postActions';
6 |
7 | const VotingWidget = ({
8 | votes,
9 | postId,
10 | postUsername,
11 | upVoters,
12 | downVoters,
13 | authUserId,
14 | remumbledPost,
15 | }) => {
16 | const dispatch = useDispatch();
17 |
18 | const isUpVoted = upVoters.find((vote) => vote.id === authUserId);
19 | const isDownVoted = downVoters.find((vote) => vote.id === authUserId);
20 |
21 | return (
22 |
23 |
25 | dispatch(
26 | modifyVote({
27 | post_id: remumbledPost?.original_mumble.id || postId,
28 | value: 'upvote',
29 | post_username: postUsername,
30 | remumbled_post: remumbledPost,
31 | }),
32 | )
33 | }
34 | className={`${isUpVoted ? 'fas' : 'far'} fa-caret-up vote-icon up-arrow`}
35 | >
36 |
37 | {votes === 0 ? (
38 |
{votes}
39 | ) : votes > 0 ? (
40 |
+{votes}
41 | ) : (
42 |
{votes}
43 | )}
44 |
45 |
47 | dispatch(
48 | modifyVote({
49 | post_id: remumbledPost?.original_mumble.id || postId,
50 | value: `${isUpVoted ? 'upvote' : 'downvote'}`,
51 | post_username: postUsername,
52 | remumbled_post: remumbledPost,
53 | }),
54 | )
55 | }
56 | className={`${isDownVoted ? 'fas' : 'far'} fa-caret-down vote-icon down-arrow`}
57 | >
58 |
59 | );
60 | };
61 |
62 | VotingWidget.propTypes = {
63 | authUserId: PropTypes.number,
64 | downVoters: PropTypes.array,
65 | postId: PropTypes.string,
66 | postUsername: PropTypes.string,
67 | upVoters: PropTypes.array,
68 | votes: PropTypes.number.isRequired,
69 | };
70 |
71 | export default VotingWidget;
72 |
--------------------------------------------------------------------------------
/frontend/src/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as AuthorBox } from './AuthorBox';
2 | export { default as Avatar } from './Avatar.js';
3 | export { default as Button } from './Button';
4 | export { default as Card } from './Card';
5 | export { default as Input } from './Input';
6 | export { default as InputChoice } from './InputChoice';
7 | export { default as Loading } from './Loading';
8 | export { default as Message } from './Message';
9 | export { default as Modal } from './Modal';
10 | export { default as ModalContentAction } from './ModalContentAction';
11 | export { default as Tag } from './Tag';
12 | export { default as TagInput } from './TagInput';
13 | export { default as TextArea } from './TextArea';
14 | export { default as VotingWidget } from './VotingWidget';
15 |
--------------------------------------------------------------------------------
/frontend/src/components/ArticlesCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { Card } from '../common';
5 |
6 | const ArticlesCard = ({ articles }) => {
7 | return (
8 |
9 | Popular Articles
10 |
11 | Write a Post
12 |
13 |
14 |
15 | {articles.map((article) => (
16 |
17 |
18 |
19 |
{article.vote_rank}
20 |
21 |
22 |
23 |
24 |
{article.title}
25 |
26 |
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default ArticlesCard;
34 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributors.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 |
5 | import FollowButton from './FollowButton';
6 | import { AuthorBox } from '../common';
7 | import { listRecommenedUsers } from '../actions/userActions';
8 | import { getImageUrl } from '../utilities/getImageUrl';
9 |
10 | const Contributors = () => {
11 | const dispatch = useDispatch();
12 |
13 | const userList = useSelector((state) => state.userListRecommended);
14 | const { users } = userList;
15 |
16 | useEffect(() => {
17 | dispatch(listRecommenedUsers());
18 | }, [dispatch]);
19 |
20 | return (
21 |
22 |
23 |
Top Contributors
24 |
25 | View More
26 |
27 |
28 |
29 | {users.map((user) => (
30 |
41 | ))}
42 |
43 |
44 | );
45 | };
46 |
47 | export default Contributors;
48 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateMessageModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Modal, ModalContentAction, TextArea } from '../common';
3 | import { MessageService } from '../services';
4 |
5 | const CreateMessageModal = ({ active, setActive, toUser, onMessageCreated }) => {
6 | const [content, setContent] = useState('');
7 |
8 | const createMessage = async () => {
9 | await MessageService.createMessage({
10 | content,
11 | to_user: toUser,
12 | });
13 | setContent('');
14 | setActive(false);
15 | onMessageCreated();
16 | };
17 |
18 | return (
19 |
20 |
28 |
29 |
30 | );
31 | };
32 | export default CreateMessageModal;
33 |
--------------------------------------------------------------------------------
/frontend/src/components/CreatePost.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Prompt } from 'react-router-dom';
3 | import { useDispatch } from 'react-redux';
4 | import { createPost } from '../actions/postActions';
5 | import '../styles/components/CreatePost.css';
6 |
7 | import { Button, TextArea } from '../common';
8 |
9 | const CreatePost = () => {
10 | let dispatch = useDispatch();
11 |
12 | const [message, setMessage] = useState('');
13 | const [error, setError] = useState(null);
14 |
15 | const onFormSubmit = (e) => {
16 | e.preventDefault();
17 |
18 | if (!message.trim()) {
19 | return setError('Post cannot be empty!');
20 | } else {
21 | dispatch(createPost({ content: message, isComment: false }));
22 | }
23 | setMessage('');
24 | };
25 |
26 | const handleMessageChange = (e) => {
27 | setMessage(e.target.value);
28 | if (error && e.target.value) {
29 | setError(null);
30 | }
31 | };
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
48 |
49 |
50 |
0}
52 | message="Are you sure you want to leave without posting?"
53 | />
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default CreatePost;
61 |
--------------------------------------------------------------------------------
/frontend/src/components/DiscussionsCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | function DiscussionsCard({ discussions }) {
5 | return (
6 |
7 |
8 |
Discussions
9 |
10 | Start a Discussion
11 |
12 |
13 |
14 | {discussions.map((discussion, index) => (
15 |
16 |
17 |
18 |
{discussion.vote_ratio}
19 |
20 |
21 |
22 |
23 |
{discussion.headline}
24 |
25 |
26 |
27 | ))}
28 |
29 |
30 | );
31 | }
32 |
33 | export default DiscussionsCard;
34 |
--------------------------------------------------------------------------------
/frontend/src/components/FeedCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import '../styles/components/Feed.css';
4 |
5 | import { Card } from '../common';
6 | import PostCard from './PostCard';
7 |
8 | function Feed({ posts }) {
9 | return (
10 |
11 | {posts.map((post) => (
12 |
13 |
16 |
17 | ))}
18 |
19 | );
20 | }
21 |
22 | export default Feed;
23 |
--------------------------------------------------------------------------------
/frontend/src/components/FollowButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { followUser } from '../actions/userActions';
4 | import Button from '../common/Button';
5 |
6 | const FollowButton = ({ userProfile }) => {
7 | const isFollowingThisUser = useSelector(
8 | (state) => !!state.following.following.find((userIFollow) => userIFollow.id === userProfile.id),
9 | );
10 |
11 | const isFollowLoading = useSelector((state) => state.follow.loading[userProfile.username]);
12 | const isLoading = isFollowLoading;
13 |
14 | const dispatch = useDispatch();
15 |
16 | const toggleFollow = (username) => {
17 | dispatch(followUser(username));
18 | };
19 |
20 | return (
21 |
22 | {isFollowingThisUser ? (
23 | toggleFollow(userProfile.username)}
25 | color="main"
26 | size="sm"
27 | loading={isLoading}
28 | text="Following"
29 | />
30 | ) : (
31 | toggleFollow(userProfile.username)}
33 | color="main"
34 | loading={isLoading}
35 | size="sm"
36 | text="Follow"
37 | outline
38 | />
39 | )}
40 |
41 | );
42 | };
43 |
44 | export default FollowButton;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import { login } from '../actions/authActions';
5 | import { useForm } from '../hooks';
6 | import { Button, Input, Message } from '../common';
7 |
8 | const LoginForm = () => {
9 | let dispatch = useDispatch();
10 |
11 | let auth = useSelector((state) => state.auth);
12 | let { error } = auth;
13 |
14 | const [formValues, fieldChanges] = useForm({ username: '', password: '' });
15 | const onSubmit = (e) => {
16 | e.preventDefault();
17 | dispatch(login(formValues));
18 | };
19 | return (
20 | <>
21 | {error && {error} }
22 |
23 |
24 |
31 |
39 |
40 |
41 | New here? Sign up
42 |
43 |
44 |
45 |
46 | Forgot Password?
47 |
48 | >
49 | );
50 | };
51 |
52 | export default LoginForm;
53 |
--------------------------------------------------------------------------------
/frontend/src/components/Notification.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import classNames from 'classnames';
4 |
5 | import '../styles/components/NotificationTitle.css';
6 |
7 | import { Avatar } from '../common';
8 | import { formatDate } from '../utilities';
9 | import { getNotificationLink } from './Header';
10 | import { markAsRead } from '../actions/notificationsActions';
11 | import { useDispatch } from 'react-redux';
12 | import { getImageUrl } from '../utilities/getImageUrl';
13 |
14 | const Notification = ({ notification }) => {
15 | const dispatch = useDispatch();
16 |
17 | return (
18 | dispatch(markAsRead(notification.id))}
21 | to={getNotificationLink(notification)}
22 | >
23 |
24 |
31 |
32 |
33 |
38 | {notification.content}
39 |
40 |
{formatDate.distanceDate(notification.created)}
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export const NotificationTitle = ({ notification }) => {
48 | return (
49 |
50 | {notification.created_by.username}
51 | {notification.content}
52 |
53 | );
54 | };
55 |
56 | export default Notification;
57 |
--------------------------------------------------------------------------------
/frontend/src/components/Page.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import Header from './Header';
4 | import SidebarNav from './SidebarNav';
5 |
6 | const Page = ({ children, header = true, sidebarNav = true, singleContent = false }) => {
7 | const { isAuthenticated } = useSelector((state) => state.auth);
8 | const [isSidebarNav, setIsSidebarNav] = useState(false);
9 | const [isResponsiveSidebarNav, setIsResponsiveSidebarNav] = useState(true);
10 |
11 | const toggleSidebarNav = () => setIsSidebarNav(!isSidebarNav);
12 |
13 | useEffect(() => {
14 | const resizeListener = window.addEventListener('resize', (e) => {
15 | setIsSidebarNav(false);
16 | if (e.target.outerWidth < 660) return setIsResponsiveSidebarNav(true);
17 | setIsResponsiveSidebarNav(false);
18 | });
19 |
20 | return () => window.removeEventListener('resize', resizeListener);
21 | }, []);
22 |
23 | return (
24 | <>
25 | {isAuthenticated && header && (
26 |
27 | )}
28 |
29 | {sidebarNav && (
30 |
31 | )}
32 |
37 | {children}
38 |
39 |
40 | >
41 | );
42 | };
43 |
44 | export default Page;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/PostAction.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import classNames from 'classnames';
3 | import { Prompt } from 'react-router-dom';
4 | import { useDispatch } from 'react-redux';
5 |
6 | import { createComment, createRemumble } from '../actions/postActions';
7 |
8 | import Button from '../common/Button';
9 |
10 | const PostAction = ({ onMessageIconClick, comments, shares, postId, setComments, ancestors }) => {
11 | let dispatch = useDispatch();
12 |
13 | const [showCommentBox, setShowCommentBox] = useState(false);
14 | const [comment, setComment] = useState('');
15 |
16 | const toggleCommentBox = () => {
17 | setShowCommentBox((prev) => !prev);
18 | };
19 | const handleCommentChange = (e) => setComment(e.target.value);
20 |
21 | const handleCommentSubmit = (e) => {
22 | e.preventDefault();
23 | dispatch(
24 | createComment(setComments, postId, { content: comment, isComment: true, postId: postId }),
25 | );
26 | let newComment = true;
27 | onMessageIconClick(newComment);
28 | for (let ancestor of ancestors) {
29 | ancestor((count) => count + 1);
30 | }
31 | setComment('');
32 | toggleCommentBox();
33 | };
34 |
35 | let toggleRemumble = () => {
36 | dispatch(createRemumble(postId));
37 | };
38 |
39 | return (
40 |
41 |
42 |
48 |
49 | {comments}
50 |
51 |
52 |
53 | {/* */}
54 |
55 | Comment
56 | {/* */}
57 |
58 |
59 |
60 |
61 |
62 | {shares}
63 |
64 |
65 |
66 | {/* comment Textarea */}
67 | {showCommentBox && (
68 |
69 |
76 |
77 | Submit
78 |
79 |
80 | )}
81 |
0}
83 | message="Do you want to leave without finishing your comment?"
84 | />
85 |
86 | );
87 | };
88 |
89 | export default PostAction;
90 |
--------------------------------------------------------------------------------
/frontend/src/components/PostCardPlaceholder.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RoundShape, TextBlock } from 'react-placeholder/lib/placeholders';
3 |
4 | import '../styles/components/PostCardPlaceholder.css';
5 |
6 | const PostCardPlaceholder = () => {
7 | const cards = [1, 2, 3];
8 | return (
9 |
10 | {cards.map((value) => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default PostCardPlaceholder;
37 |
--------------------------------------------------------------------------------
/frontend/src/components/RestoreScroll.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | const RestoreScroll = () => {
5 | const pathname = useLocation().pathname;
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [pathname]);
10 |
11 | return null;
12 | };
13 |
14 | export default RestoreScroll;
15 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useEffect, useRef } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useHistory } from 'react-router-dom';
4 | import debounce from 'lodash/debounce';
5 | import { searchBarTyped } from '../actions/appActions';
6 | import '../styles/components/SearchBox.css';
7 |
8 | const SearchBox = () => {
9 | const keyword = useSelector((state) => state.searchBar.input);
10 | const dispatch = useDispatch();
11 | let history = useHistory();
12 |
13 | /* useMemo() makes sures that debounce function is not recreated everytime the component re-renders */
14 | const pushKeyword = useMemo(() => {
15 | return debounce((searchTerm) => {
16 | if (searchTerm) {
17 | history.push(`/search?q=${searchTerm}`);
18 | } else {
19 | history.push(history.push(history.location.pathname));
20 | }
21 | }, 500); // end of debounce
22 | }, [history]);
23 |
24 | const submitHandler = (e) => {
25 | e.preventDefault();
26 | pushKeyword(keyword);
27 | };
28 |
29 | // defining the function of the shortcut for the searh bar
30 | const ShortcutKey = (key, callback) => {
31 | const callbackRef = useRef(callback);
32 | useEffect(() => {
33 | callbackRef.current = callback;
34 | });
35 |
36 | useEffect(() => {
37 | function handle(event) {
38 | if (event.code === key) callbackRef.current(event);
39 | }
40 | document.addEventListener('keyup', handle);
41 | return () => document.removeEventListener('keyup', handle);
42 | }, [key]);
43 | };
44 |
45 | // Applying the functionality of the shortcut to the search bar
46 | ShortcutKey('Slash', (event) => {
47 | // if event occured in an input or textarea do nothing
48 | if (event.target.closest('input, textarea')) return;
49 | // else focus on the searchbox
50 | else {
51 | document.getElementById('search-input').focus();
52 | }
53 | });
54 |
55 | return (
56 |
63 |
64 | dispatch(searchBarTyped(e.target.value))}
69 | placeholder="Search Mumble"
70 | />
71 |
72 | /
73 |
74 |
75 | );
76 | };
77 |
78 | export default SearchBox;
79 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchByArticlesList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useLocation } from 'react-router';
4 | import { Link } from 'react-router-dom';
5 | import { resetSearchArticles, searchArticles } from '../actions/articleActions';
6 |
7 | import '../styles/components/SearchBox.css';
8 |
9 | const SearchByArticlesList = () => {
10 | const dispatch = useDispatch();
11 | const location = useLocation();
12 | const keyword = location.search;
13 |
14 | const { data } = useSelector((state) => state.articleSearchList);
15 | const { results: articles } = data;
16 |
17 | useEffect(() => {
18 | dispatch(searchArticles(keyword));
19 | return () => {
20 | dispatch(resetSearchArticles());
21 | };
22 | }, [dispatch, keyword]);
23 |
24 | return (
25 |
26 | {articles.map((article, index) => (
27 |
28 |
29 |
30 |
31 |
32 |
33 | {article.title}
34 |
35 |
36 |
37 |
38 |
39 | ))}
40 |
41 | );
42 | };
43 |
44 | export default SearchByArticlesList;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchByInterestsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import '../styles/components/SearchBox.css';
4 |
5 | const SearchByInterestsList = ({ interests }) => {
6 | return (
7 |
8 | {interests.map((interest, index) => (
9 |
20 | ))}
21 |
22 | );
23 | };
24 |
25 | export default SearchByInterestsList;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchByPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import '../styles/components/SearchByPanel.css';
5 |
6 | export const CATEGORY_USERS = 'CATEGORY_USERS';
7 | export const CATEGORY_POSTS = 'CATEGORY_POSTS';
8 | export const CATEGORY_ARTICLES = 'CATEGORY_ARTICLES';
9 | export const CATEGORY_SKILLS = 'CATEGORY_SKILLS';
10 | export const CATEGORY_INTERESTS = 'CATEGORY_INTERESTS';
11 |
12 | const SearchByPanel = ({ category, setCategory }) => {
13 | return (
14 |
15 |
16 |
Search by:
17 |
setCategory(CATEGORY_USERS)}
22 | >
23 | Users
24 |
25 |
setCategory(CATEGORY_POSTS)}
30 | >
31 | Posts
32 |
33 |
setCategory(CATEGORY_ARTICLES)}
38 | >
39 | Articles
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default SearchByPanel;
47 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchByPostsList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useLocation } from 'react-router';
4 | import { Link } from 'react-router-dom';
5 |
6 | import '../styles/components/SearchBox.css';
7 | import '../styles/components/SearchByUsersAndPostList.css';
8 | import logo from '../assets/logo/dark-logo.png';
9 |
10 | import { searchPosts } from '../actions/postActions';
11 | import { Card } from '../common';
12 | import PostCard from './PostCard';
13 |
14 | const SearchByPostsList = () => {
15 | const dispatch = useDispatch();
16 | const location = useLocation();
17 | const keyword = location.search;
18 | const { posts } = useSelector((state) => state.postSearchList);
19 |
20 | useEffect(() => {
21 | dispatch(searchPosts(keyword));
22 | }, [dispatch, keyword]);
23 |
24 | const showResultsNotFound = posts.length === 0;
25 |
26 | return (
27 |
28 | {showResultsNotFound && (
29 |
30 |
31 |
32 |
33 |
34 | 4
35 |
36 |
37 |
38 | 4
39 |
40 |
Mumble post not found
41 |
42 |
Looks like the post title was spelled wrong or the post has been deleted !
43 |
44 |
← Go Home
45 |
46 |
47 |
48 |
49 | )}
50 | {!showResultsNotFound &&
51 | posts.map((post) => (
52 |
53 |
56 |
57 | ))}
58 |
59 | );
60 | };
61 |
62 | export default SearchByPostsList;
63 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBySkillsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import '../styles/components/SearchBox.css';
4 |
5 | const SearchBySkillsList = ({ skills }) => {
6 | return (
7 |
8 | {skills.map((skill, index) => (
9 |
20 | ))}
21 |
22 | );
23 | };
24 |
25 | export default SearchBySkillsList;
26 |
--------------------------------------------------------------------------------
/frontend/src/components/SidebarNav.js:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom';
2 | import '../styles/components/SidebarNav.css';
3 | import { useEffect } from 'react';
4 | import { MessageService } from '../services';
5 | import { useState } from 'react';
6 |
7 | const SidebarNav = ({ isSidebarNav, isResponsiveSidebarNav }) => {
8 | const [count, setCount] = useState(0);
9 |
10 | useEffect(() => {
11 | MessageService.getUnreadMessagesCount().then(({ count }) => setCount(count));
12 | }, []);
13 |
14 | return (
15 |
19 |
20 |
21 |
22 |
23 | Home
24 |
25 |
26 |
27 |
28 |
29 |
30 | {count > 0 &&
}
31 |
32 | Inbox
33 |
34 |
35 |
36 |
37 |
38 |
39 | Contributors
40 |
41 |
42 |
43 |
44 |
45 |
46 | Settings
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default SidebarNav;
55 |
--------------------------------------------------------------------------------
/frontend/src/components/SignupForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { useForm } from '../hooks';
5 | import Message from '../common/Message';
6 | import { register } from '../actions/authActions';
7 | import { Button, Input } from '../common';
8 |
9 | const SignupForm = () => {
10 | let dispatch = useDispatch();
11 |
12 | const [message, setMessage] = useState('');
13 |
14 | let auth = useSelector((state) => state.auth);
15 | let { error } = auth;
16 |
17 | const [inputs, fieldChanges] = useForm({
18 | email: '',
19 | username: '',
20 | password: '',
21 | password1: '',
22 | });
23 |
24 | const onSubmit = (e) => {
25 | e.preventDefault();
26 |
27 | if (inputs.password !== inputs.password1) {
28 | setMessage('Passwords do not match');
29 | } else {
30 | setMessage('');
31 | dispatch(register(inputs));
32 | }
33 | };
34 |
35 | return (
36 | <>
37 | {error && {error} }
38 | {message && {message} }
39 |
40 |
41 |
49 |
56 |
64 |
65 |
73 |
74 |
75 |
76 | Have an account? Login
77 |
78 |
79 | >
80 | );
81 | };
82 | export default SignupForm;
83 |
--------------------------------------------------------------------------------
/frontend/src/components/SkillList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | const SkillList = ({ tags }) => {
5 | return (
6 |
7 | {tags.map((tag) => (
8 |
9 |
10 | {tag.name}
11 |
12 |
13 | ))}
14 | {tags.length === 0 &&
They are a skilless developer...
}
15 |
16 | );
17 | };
18 |
19 | export default SkillList;
20 |
--------------------------------------------------------------------------------
/frontend/src/components/SkillTags.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SkillList from './SkillList';
3 |
4 | const SkillTags = ({ tags }) => {
5 | return (
6 |
13 | );
14 | };
15 |
16 | export default SkillTags;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/TopicTags.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import { Tag } from '../common';
5 |
6 | const TopicTags = ({ tags }) => {
7 | return (
8 |
9 |
10 |
Topics you follow
11 |
12 | More Topics
13 |
14 |
15 |
16 |
17 | {tags.map((tag, index) => (
18 |
19 | ))}
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default TopicTags;
27 |
--------------------------------------------------------------------------------
/frontend/src/components/UserCard.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import '../styles/components/UserCard.css';
3 |
4 | import { Avatar, Button } from '../common';
5 | import FollowButton from './FollowButton';
6 | import { useSelector } from 'react-redux';
7 | import { getImageUrl } from '../utilities/getImageUrl';
8 | import CreateMessageModal from './CreateMessageModal';
9 |
10 | const UserCard = ({ userProfile, onMessageCreated }) => {
11 | const { isAuthenticated, user } = useSelector((state) => state.auth);
12 | const [showCreateModal, setShowCreateModal] = useState(false);
13 |
14 | return (
15 |
16 |
17 |
18 |
24 |
{userProfile.name}
25 |
@{userProfile.username}
26 |
27 |
{userProfile.bio}
28 |
29 |
30 | {Math.sign(userProfile.vote_ratio) === -1 ? (
31 |
{userProfile.vote_ratio}
32 | ) : (
33 | +{userProfile.vote_ratio}
34 | )}
35 |
36 | Vote Ratio
37 |
38 |
39 |
{userProfile.followers_count}
40 | Followers
41 |
42 |
43 |
44 | {isAuthenticated && userProfile?.user !== user?.id && (
45 |
46 |
47 | setShowCreateModal(true)}
49 | color="main"
50 | size="sm"
51 | text="Send Message"
52 | outline
53 | />
54 |
55 | )}
56 |
57 |
58 |
59 |
65 |
66 | );
67 | };
68 |
69 | export default UserCard;
70 |
--------------------------------------------------------------------------------
/frontend/src/components/UserCardPlaceholder.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RoundShape, TextBlock } from 'react-placeholder/lib/placeholders';
3 | import '../styles/components/UserCard.css';
4 |
5 | const UserCardPlaceholder = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default UserCardPlaceholder;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/UserSettingModalContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ModalContentAction } from '../common';
4 |
5 | const UserSettingModalContent = ({ closeModal, children, handleFormSubmit }) => {
6 | const submit = (e) => {
7 | e.preventDefault();
8 | handleFormSubmit(e);
9 | closeModal(false);
10 | };
11 | return (
12 |
13 | {children}
14 |
15 |
16 | );
17 | };
18 |
19 | export default UserSettingModalContent;
20 |
--------------------------------------------------------------------------------
/frontend/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as ArticlesCard } from './ArticlesCard';
2 | export { default as Contributors } from './Contributors.js';
3 | export { default as CreatePost } from './CreatePost';
4 | export { default as DiscussionsCard } from './DiscussionsCard';
5 | export { default as FeedCard } from './FeedCard';
6 | export { default as Header } from './Header';
7 | export { default as LoginForm } from './LoginForm';
8 | export { default as Notification } from './Notification';
9 | export { default as Page } from './Page';
10 | export { default as PostCardPlaceholder } from './PostCardPlaceholder';
11 | export { default as ProfilePicCropperModal } from './ProfilePicCropperModal';
12 | export { default as RestoreScroll } from './RestoreScroll';
13 | export { default as SearchBox } from './SearchBox';
14 | export { default as SignupForm } from './SignupForm';
15 | export { default as SkillTags } from './SkillTags';
16 | export { default as SkillList } from './SkillList';
17 | export { default as TopicTags } from './TopicTags';
18 | export { default as UserCard } from './UserCard';
19 | export { default as UserCardPlaceholder } from './UserCardPlaceholder';
20 | export { default as UserSettingModalContent } from './UserSettingModalContent';
21 | export { default as UserSettingUpdateModal } from './UserSettingUpdateModal';
22 | export { default as SearchByPanel } from './SearchByPanel';
23 | export { default as SearchByUsersList } from './SearchByUsersList';
24 | export { default as SearchByInterestsList } from './SearchByInterestsList';
25 | export { default as SearchByArticlesList } from './SearchByArticlesList';
26 | export { default as SearchBySkillsList } from './SearchBySkillsList';
27 | export { default as SearchByPostsList } from './SearchByPostsList';
28 | export { default as SidebarNav } from './SidebarNav';
29 |
--------------------------------------------------------------------------------
/frontend/src/constants/appConstants.js:
--------------------------------------------------------------------------------
1 | export const SEARCH_BAR_TYPED = 'SEARCH_BAR_TYPED';
2 |
--------------------------------------------------------------------------------
/frontend/src/constants/articleConstants.js:
--------------------------------------------------------------------------------
1 | export const ARTICLE_CREATE_REQUEST = 'ARTICLE_CREATE_REQUEST';
2 | export const ARTICLE_CREATE_SUCCESS = 'ARTICLE_CREATE_SUCCESS';
3 | export const ARTICLE_CREATE_FAIL = 'ARTICLE_CREATE_FAIL';
4 |
5 | export const ARTICLE_GET_REQUEST = 'ARTICLE_GET_REQUEST';
6 | export const ARTICLE_GET_SUCCESS = 'ARTICLE_GET_SUCCESS';
7 | export const ARTICLE_GET_FAIL = 'ARTICLE_GET_FAIL';
8 |
9 | export const ARTICLE_SEARCH_REQUEST = 'ARTICLE_SEARCH_REQUEST';
10 | export const ARTICLE_SEARCH_SUCCESS = 'ARTICLE_SEARCH_SUCCESS';
11 | export const ARTICLE_SEARCH_FAIL = 'ARTICLE_SEARCH_FAIL';
12 | export const ARTICLE_SEARCH_RESET = 'ARTICLE_SEARCH_RESET';
13 |
--------------------------------------------------------------------------------
/frontend/src/constants/authConstants.js:
--------------------------------------------------------------------------------
1 | export const LOGIN_REQUEST = 'LOGIN_REQUEST';
2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
3 | export const LOGIN_FAIL = 'LOGIN_FAIL';
4 | export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
5 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
6 | export const LOGOUT_FAIL = 'LOGOUT_FAIL';
7 | export const REGISTER_REQUEST = 'REGISTER_REQUEST';
8 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
9 | export const REGISTER_FAIL = 'REGISTER_FAIL';
10 | export const REFRESH_TOKEN_SUCCESS = 'REFRESH_TOKEN_SUCCESS';
11 | export const REFRESH_TOKEN_FAIL = 'REFRESH_TOKEN_FAIL';
12 |
--------------------------------------------------------------------------------
/frontend/src/constants/localConstants.js:
--------------------------------------------------------------------------------
1 | export const MUMBLETHEME = 'MUMBLETHEME';
2 |
--------------------------------------------------------------------------------
/frontend/src/constants/notificationsConstants.js:
--------------------------------------------------------------------------------
1 | export const NOTIFICATIONS_REQUEST = 'NOTIFICATIONS_REQUEST';
2 | export const NOTIFICATIONS_SUCCESS = 'NOTIFICATIONS_SUCCESS';
3 | export const NOTIFICATIONS_FAIL = 'NOTIFICATIONS_FAIL';
4 |
5 | export const NOTIFICATIONS_UNREAD_REQUEST = 'NOTIFICATIONS_UNREAD_REQUEST';
6 | export const NOTIFICATIONS_UNREAD_SUCCESS = 'NOTIFICATIONS_UNREAD_SUCCESS';
7 | export const NOTIFICATIONS_UNREAD_FAIL = 'NOTIFICATIONS_UNREAD_FAIL';
8 |
9 | export const READ_REQUEST = 'READ_REQUEST';
10 | export const READ_SUCCESS = 'READ_SUCCESS';
11 | export const READ_FAIL = 'READ_FAIL';
12 |
--------------------------------------------------------------------------------
/frontend/src/constants/postConstants.js:
--------------------------------------------------------------------------------
1 | export const POST_SEARCH_LIST_REQUEST = 'POST_SEARCH_LIST_REQUEST';
2 | export const POST_SEARCH_LIST_SUCCESS = 'POST_SEARCH_LIST_SUCCESS';
3 | export const POST_SEARCH_LIST_FAIL = 'POST_SEARCH_LIST_FAIL';
4 |
5 | export const POST_DASHBOARD_REQUEST = 'POST_DASHBOARD_REQUEST';
6 | export const POST_DASHBOARD_SUCCESS = 'POST_DASHBOARD_SUCCESS';
7 | export const POST_DASHBOARD_FAIL = 'POST_DASHBOARD_FAIL';
8 | export const POST_DASHBOARD_RESET = 'POST_DASHBOARD_RESET';
9 |
10 | export const POST_CREATE_REQUEST = 'POST_CREATE_REQUEST';
11 | export const POST_CREATE_SUCCESS = 'POST_CREATE_SUCCESS';
12 | export const POST_CREATE_FAIL = 'POST_CREATE_FAIL';
13 |
14 | export const COMMENT_CREATE_REQUEST = 'COMMENT_CREATE_REQUEST';
15 | export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
16 | export const COMMENT_CREATE_FAIL = 'COMMENT_CREATE_FAIL';
17 |
18 | export const POST_COMMENTS_REQUEST = 'POST_COMMENTS_REQUEST';
19 | export const POST_COMMENTS_SUCCESS = 'POST_COMMENTS_SUCCESS';
20 | export const POST_COMMENTS_FAIL = 'POST_COMMENTS_FAIL';
21 |
22 | export const POST_VOTE_REQUEST = 'POST_VOTE_REQUEST';
23 | export const POST_VOTE_SUCCESS = 'POST_VOTE_SUCCESS';
24 | export const POST_VOTE_FAIL = 'POST_VOTE_FAIL';
25 |
26 | export const POST_DELETE_REQUEST = 'POST_DELETE_REQUEST';
27 | export const POST_DELETE_SUCCESS = 'POST_DELETE_SUCCESS';
28 | export const POST_DELETE_FAIL = 'POST_DELETE_FAIL';
29 |
--------------------------------------------------------------------------------
/frontend/src/constants/userConstants.js:
--------------------------------------------------------------------------------
1 | export const USER_LIST_REQUEST = 'USER_LIST_REQUEST';
2 | export const USER_LIST_SUCCESS = 'USER_LIST_SUCCESS';
3 | export const USER_LIST_RESET = 'USER_LIST_RESET';
4 | export const USER_LIST_FAIL = 'USER_LIST_FAIL';
5 |
6 | export const USER_LIST_RECOMMENDED_REQUEST = 'USER_LIST_RECOMMENDED_REQUEST';
7 | export const USER_LIST_RECOMMENDED_SUCCESS = 'USER_LIST_RECOMMENDED_SUCCESS';
8 | export const USER_LIST_RECOMMENDED_FAIL = 'USER_LIST_RECOMMENDED_FAIL';
9 |
10 | export const USER_DETAIL_REQUEST = 'USER_DETAIL_REQUEST';
11 | export const USER_DETAIL_SUCCESS = 'USER_DETAIL_SUCCESS';
12 | export const USER_DETAIL_RESET = 'USER_DETAIL_RESET';
13 | export const USER_DETAIL_FAIL = 'USER_DETAIL_FAIL';
14 |
15 | export const LIST_FOLLOWING_SUCCESS = 'LIST_FOLLOWING_SUCCESS';
16 | export const LIST_FOLLOWING_FAIL = 'LIST_FOLLOWING_FAIL';
17 | export const LIST_FOLLOWING_REQUEST = 'LIST_FOLLOWING_REQUEST';
18 |
19 | export const USER_POSTS_LIST_REQUEST = 'USER_POSTS_LIST_REQUEST';
20 | export const USER_POSTS_LIST_SUCCESS = 'USER_POSTS_LIST_SUCCESS';
21 | export const USER_POSTS_LIST_FAIL = 'USER_POSTS_LIST_FAIL';
22 |
23 | export const USER_POST_DELETE_REQUEST = 'USER_POST_DELETE_REQUEST';
24 | export const USER_POST_DELETE_SUCCESS = 'USER_POST_DELETE_SUCCESS';
25 | export const USER_POST_DELETE_FAIL = 'USER_POST_DELETE_FAIL';
26 |
27 | export const USER_ARTICLES_LIST_REQUEST = 'USER_ARTICLES_LIST_REQUEST';
28 | export const USER_ARTICLES_LIST_SUCCESS = 'USER_ARTICLES_LIST_SUCCESS';
29 | export const USER_ARTICLES_LIST_FAIL = 'USER_ARTICLES_LIST_FAIL';
30 |
31 | export const FOLLOW_USER_REQUEST = 'FOLLOW_USER_REQUEST';
32 | export const FOLLOW_USER_SUCCESS = 'FOLLOW_USER_SUCCESS';
33 | export const FOLLOW_USER_FAIL = 'FOLLOW_USER_FAIL';
34 |
35 | export const LOAD_PROFILE_SUCCESS = 'LOAD_PROFILE_SUCCESS';
36 | export const LOAD_PROFILE_FAIL = 'LOAD_PROFILE_FAIL';
37 |
38 | export const UPDATE_USER_REQUEST = 'UPDATE_USER_REQUEST';
39 | export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS';
40 | export const UPDATE_USER_FAIL = 'UPDATE_USER_FAIL';
41 |
42 | export const UPDATE_USER_PHOTO_REQUEST = 'UPDATE_USER_PHOTO_REQUEST';
43 | export const UPDATE_USER_PHOTO_SUCCESS = 'UPDATE_USER_PHOTO_SUCCESS';
44 | export const UPDATE_USER_PHOTO_FAIL = 'UPDATE_USER_PHOTO_FAIL';
45 |
46 | export const LOAD_MORE_USER_REQUEST = 'LOAD_MORE_USER_REQUEST';
47 | export const LOAD_MORE_USER_SUCCESS = 'LOAD_MORE_USER_SUCCESS';
48 | export const LOAD_MORE_USER_FAIL = 'LOAD_MORE_USER_FAIL';
49 |
50 | export const UPDATE_USER_TAGS_FAIL = 'UPDATE_USER_TAGS_FAIL';
51 |
--------------------------------------------------------------------------------
/frontend/src/data/articles.js:
--------------------------------------------------------------------------------
1 | const articles = [
2 | {
3 | id: '1',
4 | title: 'What are Django class based views & should you use them?',
5 | thumbnail: 'https://fakeimg.pl/640x360',
6 | vote_rank: '132',
7 | body:
8 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
9 | slug: 'article1',
10 | tags: ['Python', 'JavaScript'],
11 | },
12 | {
13 | id: '2',
14 | title: 'WordPress Vs Hand Coding',
15 | thumbnail: 'https://fakeimg.pl/640x360',
16 | vote_rank: '116',
17 | body:
18 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
19 | slug: 'article2',
20 | tags: ['Python', 'C#', 'Google Maps'],
21 | },
22 | {
23 | id: '3',
24 | title: '5 things that ranked my website fast',
25 | thumbnail: 'https://fakeimg.pl/640x360',
26 | vote_rank: '54',
27 | body:
28 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
29 | slug: 'article3',
30 | tags: ['React JS', 'CSS3'],
31 | },
32 | ];
33 |
34 | export default articles;
35 |
--------------------------------------------------------------------------------
/frontend/src/data/images/abhijit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/abhijit.png
--------------------------------------------------------------------------------
/frontend/src/data/images/cody.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/cody.png
--------------------------------------------------------------------------------
/frontend/src/data/images/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/default.png
--------------------------------------------------------------------------------
/frontend/src/data/images/dennis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/dennis.jpg
--------------------------------------------------------------------------------
/frontend/src/data/images/mani.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/mani.png
--------------------------------------------------------------------------------
/frontend/src/data/images/mehdi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/mehdi.png
--------------------------------------------------------------------------------
/frontend/src/data/images/mohammad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/mohammad.png
--------------------------------------------------------------------------------
/frontend/src/data/images/peng.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/peng.png
--------------------------------------------------------------------------------
/frontend/src/data/images/samthefam.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/samthefam.png
--------------------------------------------------------------------------------
/frontend/src/data/images/shahriar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/shahriar.png
--------------------------------------------------------------------------------
/frontend/src/data/images/sulamita.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/sulamita.png
--------------------------------------------------------------------------------
/frontend/src/data/images/ujjawal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/ujjawal.png
--------------------------------------------------------------------------------
/frontend/src/data/images/zach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/frontend/src/data/images/zach.png
--------------------------------------------------------------------------------
/frontend/src/data/index.js:
--------------------------------------------------------------------------------
1 | export { default as articles } from './articles';
2 | export { default as discussions } from './discussions.js';
3 | export { default as interests } from './interests';
4 | export { default as notifications } from './notifications';
5 | export { default as postsData } from './posts';
6 | export { default as skills } from './skills';
7 | export { default as usersData } from './users';
8 |
--------------------------------------------------------------------------------
/frontend/src/data/interests.js:
--------------------------------------------------------------------------------
1 | const interests = [
2 | 'JavasScript',
3 | 'Excel',
4 | 'C#',
5 | 'Ruby',
6 | 'Python',
7 | 'Google Maps API',
8 | 'React Native',
9 | 'React JS',
10 | 'Blazor',
11 | ];
12 |
13 | export default interests;
14 |
--------------------------------------------------------------------------------
/frontend/src/data/notifications.js:
--------------------------------------------------------------------------------
1 | // import defaultImg from './images/default.png';
2 | import sulamita from './images/sulamita.png';
3 | // import dennis from './images/dennis.jpg';
4 | import shahriar from './images/shahriar.png';
5 | // import cody from './images/cody.png';
6 | // import mani from './images/mani.png';
7 | // import mohammad from './images/mohammad.png';
8 | // import abhijit from './images/abhijit.png';
9 | // import mehdi from './images/mehdi.png';
10 | // import samthefam from './images/samthefam.png';
11 | import peng from './images/peng.png';
12 | // import zach from './images/zach.png';
13 | // import ujjawal from './images/ujjawal.png';
14 |
15 | const notifications = [
16 | {
17 | id: '1',
18 | content: 'started discusion How to code better',
19 | notification_type: 'discussion',
20 | content_slug: 'How-do-you-configure-HttpOnly-cookies',
21 | description:
22 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
23 | user: {
24 | id: '11',
25 | name: 'Peng Boris Akebuon',
26 | username: 'itzomen',
27 | profile_pic: peng,
28 | },
29 | created: '2021-04-02 T19:04:25+00:00',
30 | is_read: false,
31 | },
32 | {
33 | id: '2',
34 | content: 'wrote a new article',
35 | notification_type: 'article',
36 | content_slug: 'article2',
37 | description:
38 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
39 | user: {
40 | id: '2',
41 | name: 'Shahriar Parvez',
42 | username: 'mrspShuvo',
43 | profile_pic: shahriar,
44 | },
45 | created: '2021-04-02 T19:04:25+00:00',
46 | is_read: false,
47 | },
48 | {
49 | id: '3',
50 | content: 'Started following you',
51 | notification_type: 'follow',
52 | content_slug: 'realsamwick',
53 | description:
54 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
55 | user: {
56 | id: '3',
57 | name: 'Sulamita Ivanov',
58 | username: 'sulamtiaiva',
59 | profile_pic: sulamita,
60 | },
61 | created: '2021-03-29 T19:04:25+00:00',
62 | is_read: false,
63 | },
64 | ];
65 |
66 | export default notifications;
67 |
--------------------------------------------------------------------------------
/frontend/src/data/skills.js:
--------------------------------------------------------------------------------
1 | const skills = ['JavasScript', 'Python', 'Google Maps API', 'React Native', 'React JS'];
2 |
3 | export default skills;
4 |
--------------------------------------------------------------------------------
/frontend/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useForm } from './useForm';
2 | export { default as useValidationForm } from './useValidationForm';
3 | export { default as useLocationBlocker } from './useLocationBlocker';
4 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useForm = (initialState) => {
4 | const [fieldValue, setFieldValue] = useState(initialState);
5 | const handleChange = (e) => {
6 | setFieldValue((state) => {
7 | return { ...state, [e.target.name]: e.target.value };
8 | });
9 | };
10 | return [fieldValue, handleChange];
11 | };
12 |
13 | export default useForm;
14 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLocationBlocker.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | /**
3 | * React Router v5 will keep pushing history when you try to click a link that navigates
4 | * the user to the same page they are currently on. This will cause the user to have to
5 | * press the browser back button multiple times to leave the page. This hook checks
6 | * if the user is navigating to the same page, and if so, do nothing.
7 | *
8 | * See https://github.com/divanov11/Mumble/issues/315 for more issue
9 | */
10 | const useLocationBlocker = (history) => {
11 | const getLocationId = ({ pathname, search, hash }) => {
12 | return pathname + (search ? '?' + search : '') + (hash ? '#' + hash : '');
13 | };
14 |
15 | useEffect(
16 | () =>
17 | history.block(
18 | (location, action) =>
19 | action !== 'PUSH' || getLocationId(location) !== getLocationId(history.location),
20 | ),
21 | [history],
22 | );
23 | };
24 |
25 | export default useLocationBlocker;
26 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useValidationForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const useValidationForm = ({ validation, onSubmit }) => {
4 | const [form, setForm] = useState({});
5 | const [errors, setErrors] = useState({});
6 | const [isFormSubmitted, setIsFormSubmitted] = useState(false);
7 |
8 | const onInputChange = (e) => {
9 | const key = e.target.name;
10 | const value = e.target.value;
11 | setForm({
12 | ...form,
13 | [key]: value,
14 | });
15 | if (isFormSubmitted) {
16 | setErrors({
17 | ...errors,
18 | [key]: !validation[key]?.(value),
19 | });
20 | }
21 | };
22 |
23 | const hasErrors = () => {
24 | const newErrors = {};
25 | Object.keys(validation).forEach((key) => (newErrors[key] = !validation[key](form[key] || '')));
26 | setErrors(newErrors);
27 | return Object.keys(validation).some((key) => newErrors[key]);
28 | };
29 |
30 | const _onSubmit = (e) => {
31 | e.preventDefault();
32 | setIsFormSubmitted(true);
33 | if (!hasErrors()) {
34 | onSubmit();
35 | }
36 | };
37 |
38 | return [form, errors, _onSubmit, onInputChange];
39 | };
40 |
41 | export default useValidationForm;
42 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import './styles/index.css';
6 |
7 | import store from './store';
8 | import App from './App';
9 | import reportWebVitals from './reportWebVitals';
10 | import { BrowserRouter as Router } from 'react-router-dom';
11 | import { RouteHandler } from './utilities';
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | document.getElementById('root'),
22 | );
23 |
24 | reportWebVitals();
25 |
--------------------------------------------------------------------------------
/frontend/src/pages/ArticlePage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import '../styles/components/ArticlePage.css';
4 |
5 | import { ArticlesCard, Page } from '../components';
6 | import { articles } from '../data';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { getArticle } from '../actions/articleActions';
9 |
10 | const ArticlePage = ({ match }) => {
11 | let article = useSelector((state) => state.articlePage.article);
12 | let relatedArticles = articles.filter((d) => d.slug !== match.params.slug);
13 | const dispatch = useDispatch();
14 |
15 | useEffect(() => {
16 | dispatch(getArticle(match.params.slug));
17 | }, [dispatch, match.params.slug]);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
{article.title}
25 |
26 | {/* {article.tags.map((tag, index) => (
27 |
28 | {tag}
29 |
30 | ))} */}
31 |
32 |
33 |
{article.content}
34 |
35 |
36 |
37 |
38 |
41 |
42 | );
43 | };
44 |
45 | export default ArticlePage;
46 |
--------------------------------------------------------------------------------
/frontend/src/pages/ArticlesPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import { Page } from '../components';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { resetSearchArticles, searchArticles } from '../actions/articleActions';
6 | import { Avatar, Button } from '../common';
7 | import { getApiUrl } from '../services/config';
8 | import { Link } from 'react-router-dom';
9 |
10 | const ArticlesPage = () => {
11 | const { data, loading } = useSelector((state) => state.articleSearchList);
12 | const dispatch = useDispatch();
13 |
14 | const { results: articles } = data;
15 |
16 | useEffect(() => {
17 | dispatch(searchArticles());
18 | return () => {
19 | dispatch(resetSearchArticles());
20 | };
21 | }, [dispatch]);
22 |
23 | const handleLoadMore = () => {
24 | if (!data.next) return;
25 | const keywordWithPageNo = new URL(data.next).search;
26 | dispatch(searchArticles(keywordWithPageNo));
27 | };
28 |
29 | return (
30 |
31 |
32 | {articles.map((article) => (
33 |
34 |
35 |
36 |
37 |
38 |
39 | {article.title}
40 |
41 |
42 |
43 |
44 |
45 | {article.user.username}
46 |
47 |
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 | {data.next && (
55 |
62 | )}
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default ArticlesPage;
72 |
--------------------------------------------------------------------------------
/frontend/src/pages/CreateArticlePage.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 |
4 | import { CKEditor } from '@ckeditor/ckeditor5-react';
5 | import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
6 |
7 | import '../styles/components/CreateArticlePage.css';
8 |
9 | import { Button, Input } from '../common';
10 | import { ArticlesCard, Page } from '../components';
11 | import { articles } from '../data';
12 | import { useValidationForm } from '../hooks';
13 | import { useDispatch } from 'react-redux';
14 | import { createArticle } from '../actions/articleActions';
15 |
16 | const CreateArticlePage = () => {
17 | const history = useHistory();
18 | const dispatch = useDispatch();
19 |
20 | const handleFormSubmit = () => {
21 | dispatch(createArticle(form, history));
22 | };
23 |
24 | const [form, errors, onSubmit, onChange] = useValidationForm({
25 | validation: {
26 | title: (value) => value,
27 | content: (value) => value,
28 | },
29 | onSubmit: handleFormSubmit,
30 | });
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
Create Article
39 |
40 |
41 |
51 | {
56 | onChange({
57 | target: {
58 | name: 'content',
59 | value: editor.getData(),
60 | },
61 | });
62 | }}
63 | />
64 | {errors.content && {'Body is required'} }
65 |
66 |
67 | Submit
68 |
69 |
70 |
71 |
72 |
73 |
76 |
77 | );
78 | };
79 |
80 | export default CreateArticlePage;
81 |
--------------------------------------------------------------------------------
/frontend/src/pages/CreateDiscussionPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useHistory, Prompt } from 'react-router-dom';
3 |
4 | import '../styles/components/CreateDiscussionPage.css';
5 |
6 | import { Button } from '../common';
7 | import { Page } from '../components';
8 |
9 | const CreateDiscussionPage = () => {
10 | const history = useHistory();
11 | const [title, setTitle] = useState('');
12 | const [body, setBody] = useState('');
13 | const [isDiscussionSubmitted, setIsDiscussionSubmitted] = useState(false);
14 |
15 | const handleTitleChange = (e) => setTitle(e.target.value);
16 | const handleBodyChange = (e) => setBody(e.target.value);
17 |
18 | const handleFormSubmit = (e) => {
19 | e.preventDefault();
20 | window.onbeforeunload = null;
21 | // use the form data and make a request to API
22 | alert('Discussion Created!! \n Now you will be directed to the Discussion Page');
23 | // set isArticleSubmitted variables
24 | setIsDiscussionSubmitted(true);
25 | // reset title and body variables
26 | setTitle('');
27 | setBody('');
28 | };
29 |
30 | window.onbeforeunload = function (e) {
31 | e.preventDefault();
32 | if (title.trim() || body.trim()) {
33 | return 'Discard changes?';
34 | }
35 | };
36 |
37 | useEffect(() => {
38 | if (isDiscussionSubmitted) {
39 | // redirect to the articles page, in the real request slug should be changed to created article's slug
40 | history.push(`/article/article1`);
41 | }
42 | }, [history, isDiscussionSubmitted]);
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
Create Discussion
51 |
52 |
53 |
54 | Title
55 |
62 |
63 |
64 | Body
65 |
71 |
72 |
73 | Submit
74 |
75 |
76 |
0 || body.length > 0}
78 | message="Are you sure you want to leave without finishing your discussion?"
79 | />
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default CreateDiscussionPage;
89 |
--------------------------------------------------------------------------------
/frontend/src/pages/DeleteAccountPage.js:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { deleteAccount } from '../actions/authActions';
4 |
5 | import { toggleTheme as DarkLightTheme } from '../actions/local';
6 | import classNames from 'classnames';
7 |
8 | import '../styles/components/DeleteAccountPage.css';
9 |
10 | const DeleteAccountPage = () => {
11 | const dispatch = useDispatch();
12 | const isDarkTheme = useSelector((state) => state.local.darkTheme);
13 | const toggleTheme = useDispatch();
14 |
15 | const handleDeleteAccount = () => {
16 | dispatch(deleteAccount());
17 | };
18 |
19 | return (
20 |
21 | {/* Theme changer */}
22 |
23 | {
30 | toggleTheme(DarkLightTheme());
31 | }}
32 | >
33 |
34 |
35 |
Are you sure to Delete your Account?
36 |
37 | {/* Links back to home page */}
38 |
39 |
43 | ← Cancel
44 |
45 | {/* */}
46 |
51 | Delete
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default DeleteAccountPage;
60 |
--------------------------------------------------------------------------------
/frontend/src/pages/Error404Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { useSelector, useDispatch } from 'react-redux';
4 |
5 | import { toggleTheme as DarkLightTheme } from '../actions/local';
6 | import classNames from 'classnames';
7 |
8 | import '../styles/components/Error404Page.css';
9 |
10 | const Error404Page = () => {
11 | const isDarkTheme = useSelector((state) => state.local.darkTheme);
12 | const toggleTheme = useDispatch();
13 |
14 | return (
15 |
16 |
17 | {
24 | toggleTheme(DarkLightTheme());
25 | }}
26 | >
27 |
28 |
29 | 4
30 |
35 | 4
36 |
37 |
Page Not Found !
38 |
Looks like you got lost
39 |
40 |
41 | ← Go Home
42 |
43 |
44 | );
45 | };
46 |
47 | export default Error404Page;
48 |
--------------------------------------------------------------------------------
/frontend/src/pages/Error500Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import '../styles/components/Error500Page.css';
5 |
6 | const Error500Page = ({ error }) => (
7 |
8 |
9 | 5
10 |
15 |
20 |
21 |
🚨 Internal Server Error !
22 |
Something went wrong. 🤷♂️
23 |
24 |
25 | ← Go Home
26 |
27 |
28 |
29 | You can 🙌 raise an issue
30 |
31 | here
32 |
33 | mentioning these issue below.
34 |
35 | {error.message}
36 |
37 |
38 |
39 | );
40 |
41 | export default Error500Page;
42 |
--------------------------------------------------------------------------------
/frontend/src/pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import ReactPlaceholder from 'react-placeholder';
3 |
4 | import '../styles/components/HomePage.css';
5 |
6 | import {
7 | Contributors,
8 | FeedCard,
9 | CreatePost,
10 | TopicTags,
11 | PostCardPlaceholder,
12 | Page,
13 | } from '../components';
14 | import { usersData } from '../data';
15 | import { getPostsForDashboard, resetPostDashboard } from '../actions/postActions';
16 | import { useDispatch, useSelector } from 'react-redux';
17 | import { Button } from '../common';
18 |
19 | const HomePage = () => {
20 | const dispatch = useDispatch();
21 | const user = usersData.find((u) => Number(u.id) === 1);
22 | const { posts, next } = useSelector((state) => state.dashboard);
23 | const isPostsLoading = useSelector((state) => state.dashboard.loading);
24 |
25 | useEffect(() => {
26 | dispatch(getPostsForDashboard());
27 | return () => dispatch(resetPostDashboard()); // when the componenets un-mounts clear the posts from the store
28 | }, [dispatch]);
29 |
30 | const handleLoadMore = () => {
31 | if (!next || isPostsLoading) return;
32 |
33 | const searchString = new URL(next).search;
34 | const params = new URLSearchParams(searchString);
35 | const page = params.get('page');
36 | dispatch(getPostsForDashboard(page));
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 | }
46 | showLoadingAnimation
47 | // show the posts if the loading is false or if there are posts
48 | ready={!isPostsLoading || posts.length > 0}
49 | >
50 |
51 |
52 | {next && ( // Load More Button
53 |
59 | )}
60 |
61 |
62 |
66 |
67 | );
68 | };
69 |
70 | export default HomePage;
71 |
--------------------------------------------------------------------------------
/frontend/src/pages/InboxPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useState } from 'react';
3 | import { MessageService } from '../services';
4 | import { Page } from '../components';
5 | import { AuthorBox, Button } from '../common';
6 | import mailbox from '../assets/images/mailbox.svg';
7 | import { getImageUrl } from '../utilities/getImageUrl';
8 | import '../styles/components/InboxPage.css';
9 |
10 | const InboxPage = () => {
11 | const [messages, setMessages] = useState([]);
12 |
13 | const [count, setCount] = useState(0);
14 |
15 | useEffect(() => {
16 | MessageService.getUnreadMessagesCount().then(({ count }) => setCount(count));
17 | }, []);
18 |
19 | const markAsRead = async (message) => {
20 | await MessageService.markAsRead(message.id);
21 | await MessageService.getUnreadMessagesCount().then(({ count }) => setCount(count));
22 | await MessageService.getMessages().then(setMessages);
23 | };
24 |
25 | useEffect(() => {
26 | MessageService.getMessages().then(setMessages);
27 | }, []);
28 |
29 | return (
30 |
31 |
32 | New Messages ({count})
33 | {messages.length === 0 && (
34 |
35 |
36 |
You have no messages
37 |
38 | )}
39 | {messages.map((message) => (
40 |
41 |
42 |
43 |
52 |
{message.content}
53 | {!message.is_read && (
54 |
markAsRead(message)} text="Mark as Read" />
55 | )}
56 |
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default InboxPage;
68 |
--------------------------------------------------------------------------------
/frontend/src/pages/LogoutConfirmation.js:
--------------------------------------------------------------------------------
1 | import { Link, useHistory } from 'react-router-dom';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { logout } from '../actions/authActions';
4 |
5 | import { toggleTheme as DarkLightTheme } from '../actions/local';
6 | import classNames from 'classnames';
7 |
8 | import '../styles/components/LogoutConfirmation.css';
9 |
10 | const LogoutConfirmation = () => {
11 | const dispatch = useDispatch();
12 | const history = useHistory();
13 |
14 | const isDarkTheme = useSelector((state) => state.local.darkTheme);
15 | const toggleTheme = useDispatch();
16 |
17 | // Logs out the user
18 | const logoutUser = () => {
19 | dispatch(logout());
20 | history.push('/login');
21 | };
22 |
23 | return (
24 |
25 | {/* Theme changer */}
26 |
27 | {
34 | toggleTheme(DarkLightTheme());
35 | }}
36 | >
37 |
38 |
39 |
Do you really want to logout?
40 |
41 | {/* Links back to home page */}
42 |
43 |
47 | ← Back
48 |
49 | {/* Logs out the user */}
50 |
55 | Logout
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default LogoutConfirmation;
64 |
--------------------------------------------------------------------------------
/frontend/src/pages/NotificationsPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import '../styles/components/NotificationPage.css';
3 |
4 | import { Card } from '../common';
5 | import { Notification, Page } from '../components';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { getNotifications } from '../actions/notificationsActions';
8 |
9 | function NotificationsPage() {
10 | const { notifications } = useSelector((state) => state.notifications);
11 | const dispatch = useDispatch();
12 |
13 | useEffect(() => {
14 | dispatch(getNotifications());
15 | }, [dispatch]);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
All Notifications
23 |
24 | {notifications.map((notification) => (
25 |
26 | ))}
27 | {notifications.length === 0 && (
28 | You have no notifications! Start following some Mumblers
29 | )}
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default NotificationsPage;
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/ProfilePage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 |
4 | import '../styles/components/ProfilePage.css';
5 |
6 | import {
7 | FeedCard,
8 | Page,
9 | PostCardPlaceholder,
10 | SkillTags,
11 | UserCard,
12 | UserCardPlaceholder,
13 | } from '../components';
14 |
15 | import {
16 | listUserArticles,
17 | listUserDetails,
18 | listUserPosts,
19 | resetUserDetails,
20 | } from '../actions/userActions';
21 | import { Message } from '../common';
22 | import { useState } from 'react';
23 |
24 | const Profile = ({ match }) => {
25 | const username = match.params.username;
26 |
27 | const dispatch = useDispatch();
28 |
29 | const userProfileDetail = useSelector((state) => state.userProfileDetail);
30 | const userPostsList = useSelector((state) => state.userPostsList);
31 |
32 | const { user: userProfile, loading: isUserLoading } = userProfileDetail;
33 | const { posts, loading: isPostsLoading } = userPostsList;
34 |
35 | useEffect(() => {
36 | dispatch(listUserDetails(username));
37 | dispatch(listUserPosts(username));
38 | dispatch(listUserArticles(username));
39 |
40 | return () => dispatch(resetUserDetails());
41 | }, [dispatch, username]);
42 |
43 | const [message, setMessage] = useState('');
44 |
45 | const onMessageCreated = () => {
46 | setMessage('Succesfully created a message');
47 | };
48 |
49 | return (
50 |
51 |
52 | {message && (
53 | setMessage('')} dismissible variant="success">
54 | {message}
55 |
56 | )}
57 | {!isPostsLoading ? : }
58 |
59 |
60 |
61 | {!isUserLoading && userProfile ? (
62 |
63 |
64 |
65 |
66 | ) : (
67 |
68 | )}
69 |
70 |
71 | );
72 | };
73 |
74 | export default Profile;
75 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import '../styles/components/SearchPage.css';
4 |
5 | import { articles, interests, skills } from '../data';
6 | import {
7 | SearchByPanel,
8 | SearchBySkillsList,
9 | SearchByInterestsList,
10 | SearchByUsersList,
11 | SearchByArticlesList,
12 | SearchByPostsList,
13 | Page,
14 | } from '../components';
15 | import {
16 | CATEGORY_ARTICLES,
17 | CATEGORY_INTERESTS,
18 | CATEGORY_POSTS,
19 | CATEGORY_SKILLS,
20 | CATEGORY_USERS,
21 | } from '../components/SearchByPanel';
22 |
23 | const SearchPage = () => {
24 | // refactor this into redux
25 | let [category, setCategory] = useState(CATEGORY_USERS);
26 |
27 | return (
28 |
29 |
30 | {category === CATEGORY_USERS && }
31 | {category === CATEGORY_POSTS && }
32 | {category === CATEGORY_ARTICLES && }
33 | {category === CATEGORY_SKILLS && }
34 | {category === CATEGORY_INTERESTS && }
35 |
36 |
37 |
40 |
41 | );
42 | };
43 |
44 | export default SearchPage;
45 |
--------------------------------------------------------------------------------
/frontend/src/pages/TagsPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import '../styles/components/SearchPage.css';
4 |
5 | import { Page, SkillList } from '../components';
6 | import { useLocation } from 'react-router';
7 | import { useEffect } from 'react';
8 | import { UsersService } from '../services';
9 | import { AuthorBox, Button } from '../common';
10 | import FollowButton from '../components/FollowButton';
11 | import { getImageUrl } from '../utilities/getImageUrl';
12 |
13 | const TagsPage = () => {
14 | const { search } = useLocation();
15 | const [skill, setSkill] = useState();
16 | const [results, setResults] = useState({
17 | results: [],
18 | });
19 | const [hasMore, setHasMore] = useState(true);
20 | const [page, setPage] = useState(1);
21 | const [loading, setLoading] = useState(false);
22 |
23 | useEffect(() => {
24 | const params = new URLSearchParams(search);
25 | setSkill(params.get('skill'));
26 | setResults({
27 | results: [],
28 | });
29 | }, [search]);
30 |
31 | useEffect(() => {
32 | if (!skill) return;
33 | setLoading(true);
34 | UsersService.getUsersBySkill(skill, page).then((res) => {
35 | setResults((r) => ({
36 | ...r,
37 | results: [...r.results, ...res.results],
38 | }));
39 | setHasMore(!!res.next);
40 | setLoading(false);
41 | });
42 | }, [skill, page]);
43 |
44 | const handleLoadMore = () => {
45 | setPage(page + 1);
46 | };
47 |
48 | return (
49 |
50 |
51 |
52 | Users who know {skill}
53 |
54 | {results.results.map((user) => (
55 |
56 |
57 |
58 |
68 |
{user.profile.bio}
69 |
70 |
71 |
72 |
73 | ))}
74 |
75 | {hasMore && (
76 |
83 | )}
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default TagsPage;
91 |
--------------------------------------------------------------------------------
/frontend/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const ArticlePage = React.lazy(() => import('./ArticlePage'));
4 | export const CreateArticlePage = React.lazy(() => import('./CreateArticlePage'));
5 | export const CreateDiscussionPage = React.lazy(() => import('./CreateDiscussionPage'));
6 | export const DiscussionPage = React.lazy(() => import('./DiscussionPage'));
7 | export const Error404Page = React.lazy(() => import('./Error404Page'));
8 | export const Error500Page = React.lazy(() => import('./Error500Page'));
9 | export const ForgotPasswordPage = React.lazy(() => import('./ForgotPasswordPage'));
10 | export const HomePage = React.lazy(() => import('./HomePage'));
11 | export const LoginSignupPage = React.lazy(() => import('./LoginSignupPage'));
12 | export const NotificationsPage = React.lazy(() => import('./NotificationsPage'));
13 | export const ProfilePage = React.lazy(() => import('./ProfilePage'));
14 | export const UserSettingsPage = React.lazy(() => import('./UserSettingsPage'));
15 | export const SearchPage = React.lazy(() => import('./SearchPage'));
16 | export const LogoutConfirmation = React.lazy(() => import('./LogoutConfirmation'));
17 | export const DeleteAccountPage = React.lazy(() => import('./DeleteAccountPage'));
18 | export const TagsPage = React.lazy(() => import('./TagsPage'));
19 |
--------------------------------------------------------------------------------
/frontend/src/reducers/articleReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ARTICLE_CREATE_FAIL,
3 | ARTICLE_CREATE_SUCCESS,
4 | ARTICLE_CREATE_REQUEST,
5 | ARTICLE_GET_REQUEST,
6 | ARTICLE_GET_SUCCESS,
7 | ARTICLE_GET_FAIL,
8 | ARTICLE_SEARCH_REQUEST,
9 | ARTICLE_SEARCH_SUCCESS,
10 | ARTICLE_SEARCH_FAIL,
11 | ARTICLE_SEARCH_RESET,
12 | } from '../constants/articleConstants';
13 |
14 | export const createArticleReducer = (state = { articles: [] }, action) => {
15 | switch (action.type) {
16 | case ARTICLE_CREATE_REQUEST:
17 | return { loading: true, articles: [] };
18 |
19 | case ARTICLE_CREATE_SUCCESS:
20 | return { loading: false, articles: action.payload };
21 |
22 | case ARTICLE_CREATE_FAIL:
23 | return { loading: false, articles: [], error: action.payload };
24 |
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | export const articlePageReducer = (state = { article: { tags: '' } }, action) => {
31 | switch (action.type) {
32 | case ARTICLE_GET_REQUEST:
33 | return { ...state, loading: true };
34 |
35 | case ARTICLE_GET_SUCCESS:
36 | return { ...state, loading: false, article: action.payload };
37 |
38 | case ARTICLE_GET_FAIL:
39 | return { ...state, loading: false, article: {}, error: action.payload };
40 |
41 | default:
42 | return state;
43 | }
44 | };
45 |
46 | export const articleSearchListReducer = (
47 | state = { data: { results: [], next: null, previous: null, count: 0 } },
48 | action,
49 | ) => {
50 | switch (action.type) {
51 | case ARTICLE_SEARCH_REQUEST:
52 | return { ...state, loading: true };
53 |
54 | case ARTICLE_SEARCH_SUCCESS:
55 | return {
56 | ...state,
57 | loading: false,
58 | data: { ...action.payload, results: [...state.data.results, ...action.payload.results] },
59 | };
60 |
61 | case ARTICLE_SEARCH_FAIL:
62 | return { ...state, loading: false, error: action.payload };
63 |
64 | case ARTICLE_SEARCH_RESET:
65 | return { data: { results: [], next: null, previous: null, count: 0 } };
66 |
67 | default:
68 | return state;
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/frontend/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | LOGIN_REQUEST,
3 | LOGIN_SUCCESS,
4 | LOGIN_FAIL,
5 | LOGOUT_REQUEST,
6 | LOGOUT_SUCCESS,
7 | REGISTER_FAIL,
8 | REGISTER_REQUEST,
9 | REFRESH_TOKEN_SUCCESS,
10 | } from '../constants/authConstants';
11 |
12 | import { LOAD_PROFILE_SUCCESS, UPDATE_USER_PHOTO_SUCCESS } from '../constants/userConstants';
13 |
14 | export default function authReducer(state = {}, action) {
15 | const { type, payload } = action;
16 |
17 | switch (type) {
18 | case REGISTER_REQUEST: {
19 | return { isLoading: true };
20 | }
21 |
22 | case LOGIN_REQUEST: {
23 | return { isLoading: true };
24 | }
25 |
26 | case LOGIN_SUCCESS: {
27 | localStorage.setItem('access', payload.access);
28 | localStorage.setItem('refresh', payload.refresh);
29 | return {
30 | ...state,
31 | access: payload.access,
32 | refresh: payload.refresh,
33 | isAuthenticated: true,
34 | isLoading: false,
35 | user: payload,
36 | };
37 | }
38 |
39 | case LOAD_PROFILE_SUCCESS: {
40 | return {
41 | ...state,
42 | user: {
43 | ...payload,
44 | ...state.user,
45 | },
46 | };
47 | }
48 |
49 | case REFRESH_TOKEN_SUCCESS: {
50 | localStorage.setItem('access', payload.access);
51 | return {
52 | ...state,
53 | access: payload.access,
54 | };
55 | }
56 |
57 | case UPDATE_USER_PHOTO_SUCCESS:
58 | const newState = {
59 | ...state,
60 | user: {
61 | ...state.user,
62 | profile_pic: payload.profile_pic,
63 | },
64 | };
65 | return newState;
66 |
67 | case LOGIN_FAIL:
68 | return { isLoading: false, error: payload };
69 |
70 | case LOGOUT_REQUEST: {
71 | return {
72 | ...state,
73 | isLoading: true,
74 | };
75 | }
76 |
77 | case LOGOUT_SUCCESS: {
78 | localStorage.removeItem('access');
79 | localStorage.removeItem('refresh');
80 | return {
81 | ...state,
82 | isAuthenticated: false,
83 | access: null,
84 | refresh: null,
85 | isLoading: false,
86 | user: null,
87 | };
88 | }
89 |
90 | case REGISTER_FAIL:
91 | return { isLoading: false, error: payload };
92 |
93 | default:
94 | return state;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/frontend/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import auth from './auth';
3 | import local from './local';
4 | import {
5 | userListReducer,
6 | userListRecommendedReducer,
7 | userProfileDetailReducer,
8 | userPostsListReducer,
9 | userArticleListReducer,
10 | followingReducer,
11 | followReducer,
12 | } from './userReducers';
13 |
14 | import { postDashboardReducer, postSearchListReducer } from './postReducers';
15 | import {
16 | articlePageReducer,
17 | createArticleReducer,
18 | articleSearchListReducer,
19 | } from './articleReducer';
20 | import { notificationsReducer, unreadNotificationsReducer } from './notificationsReducer';
21 | import { searchBarReducer } from './searchBarReducer';
22 |
23 | export default combineReducers({
24 | local,
25 | auth,
26 | searchBar: searchBarReducer,
27 | userList: userListReducer,
28 | following: followingReducer,
29 | follow: followReducer,
30 | userListRecommended: userListRecommendedReducer,
31 | userProfileDetail: userProfileDetailReducer,
32 | userPostsList: userPostsListReducer,
33 | createArticle: createArticleReducer,
34 | postSearchList: postSearchListReducer,
35 | articleSearchList: articleSearchListReducer,
36 | articlePage: articlePageReducer,
37 | dashboard: postDashboardReducer,
38 | userArticleList: userArticleListReducer,
39 | notifications: notificationsReducer,
40 | unreadNotifications: unreadNotificationsReducer,
41 | });
42 |
43 | export const replaceItem = (collection, item) => {
44 | const index = collection.findIndex((entry) => entry.id === item.id);
45 | return [...collection.slice(0, index), item, ...collection.slice(index + 1)];
46 | };
47 |
--------------------------------------------------------------------------------
/frontend/src/reducers/local.js:
--------------------------------------------------------------------------------
1 | import { MUMBLETHEME } from '../constants/localConstants';
2 |
3 | // localStorage store as a string, So this will convert into boolean
4 | const isDarkTheme = localStorage.getItem('mumble-theme') === 'true' ? true : false;
5 | const isNull = localStorage.getItem('mumble-theme') === null;
6 |
7 | const initialState = {
8 | darkTheme: isNull ? false : isDarkTheme,
9 | };
10 |
11 | export default function localReducer(state = initialState, action) {
12 | const { type } = action;
13 | switch (type) {
14 | case MUMBLETHEME:
15 | localStorage.setItem('mumble-theme', !state.darkTheme);
16 | return {
17 | ...state,
18 | darkTheme: !state.darkTheme,
19 | };
20 | default:
21 | return state;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/reducers/notificationsReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | NOTIFICATIONS_REQUEST,
3 | NOTIFICATIONS_SUCCESS,
4 | NOTIFICATIONS_FAIL,
5 | NOTIFICATIONS_UNREAD_REQUEST,
6 | NOTIFICATIONS_UNREAD_SUCCESS,
7 | NOTIFICATIONS_UNREAD_FAIL,
8 | } from '../constants/notificationsConstants';
9 |
10 | export const notificationsReducer = (state = { notifications: [] }, action) => {
11 | switch (action.type) {
12 | case NOTIFICATIONS_REQUEST:
13 | return { ...state, loading: true };
14 |
15 | case NOTIFICATIONS_SUCCESS:
16 | return { ...state, loading: false, notifications: action.payload };
17 |
18 | case NOTIFICATIONS_FAIL:
19 | return { ...state, loading: false, error: action.payload };
20 |
21 | default:
22 | return state;
23 | }
24 | };
25 |
26 | export const unreadNotificationsReducer = (state = { notifications: [] }, action) => {
27 | switch (action.type) {
28 | case NOTIFICATIONS_UNREAD_REQUEST:
29 | return { ...state, loading: true };
30 |
31 | case NOTIFICATIONS_UNREAD_SUCCESS:
32 | return { ...state, loading: false, notifications: action.payload };
33 |
34 | case NOTIFICATIONS_UNREAD_FAIL:
35 | return { ...state, loading: false, error: action.payload };
36 |
37 | default:
38 | return state;
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/frontend/src/reducers/postReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | POST_CREATE_SUCCESS,
3 | POST_DASHBOARD_FAIL,
4 | POST_DASHBOARD_REQUEST,
5 | POST_DASHBOARD_RESET,
6 | POST_DASHBOARD_SUCCESS,
7 | POST_DELETE_SUCCESS,
8 | POST_SEARCH_LIST_FAIL,
9 | POST_SEARCH_LIST_REQUEST,
10 | POST_SEARCH_LIST_SUCCESS,
11 | POST_VOTE_SUCCESS,
12 | } from '../constants/postConstants';
13 | import { replaceItem } from './index';
14 |
15 | export const postSearchListReducer = (state = { posts: [] }, action) => {
16 | switch (action.type) {
17 | case POST_SEARCH_LIST_REQUEST:
18 | return { ...state, loading: true, posts: [] };
19 |
20 | case POST_SEARCH_LIST_SUCCESS:
21 | return { ...state, loading: false, posts: action.payload };
22 |
23 | case POST_SEARCH_LIST_FAIL:
24 | return { ...state, loading: false, error: action.payload };
25 |
26 | case POST_VOTE_SUCCESS:
27 | return {
28 | ...state,
29 | posts: replaceItem(state.posts, action.payload),
30 | };
31 |
32 | default:
33 | return state;
34 | }
35 | };
36 |
37 | export const postDashboardReducer = (
38 | state = { loading: false, posts: [], prev: null, next: null },
39 | action,
40 | ) => {
41 | switch (action.type) {
42 | case POST_DASHBOARD_REQUEST:
43 | return { ...state, loading: true };
44 |
45 | case POST_DASHBOARD_SUCCESS:
46 | return {
47 | ...state,
48 | loading: false,
49 | posts: [...state.posts, ...action.payload.results],
50 | prev: action.payload.prev,
51 | next: action.payload.next,
52 | };
53 |
54 | case POST_CREATE_SUCCESS:
55 | return {
56 | ...state,
57 | posts: [action.payload, ...state.posts],
58 | };
59 |
60 | case POST_DELETE_SUCCESS:
61 | return {
62 | ...state,
63 | posts: state.posts.filter((p) => p.id !== action.payload),
64 | };
65 |
66 | case POST_DASHBOARD_RESET:
67 | return { loading: false, posts: [], prev: null, next: null };
68 |
69 | case POST_DASHBOARD_FAIL:
70 | return { ...state, loading: false, error: action.payload };
71 |
72 | case POST_VOTE_SUCCESS:
73 | return {
74 | ...state,
75 | posts: replaceItem(state.posts, action.payload),
76 | };
77 |
78 | default:
79 | return state;
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/frontend/src/reducers/searchBarReducer.js:
--------------------------------------------------------------------------------
1 | import { SEARCH_BAR_TYPED } from '../constants/appConstants';
2 |
3 | export const searchBarReducer = (state = { input: '' }, action) => {
4 | switch (action.type) {
5 | case SEARCH_BAR_TYPED:
6 | return { input: action.payload };
7 |
8 | default:
9 | return state;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/frontend/src/services/articlesService.js:
--------------------------------------------------------------------------------
1 | import { post, getApiUrl, get } from './config';
2 |
3 | const createArticle = (postContent) =>
4 | post({
5 | url: getApiUrl(`api/articles/create/`),
6 | payload: postContent,
7 | });
8 |
9 | const getArticle = (articleId) =>
10 | get({
11 | url: getApiUrl(`api/articles/${articleId}/`),
12 | });
13 |
14 | const getArticlesByKeyword = (keyword) => get({ url: getApiUrl(`api/articles/${keyword}`) });
15 |
16 | const articlesService = {
17 | createArticle,
18 | getArticlesByKeyword,
19 | getArticle,
20 | };
21 |
22 | export default articlesService;
23 |
--------------------------------------------------------------------------------
/frontend/src/services/authService.js:
--------------------------------------------------------------------------------
1 | import { post, getApiUrl } from './config';
2 |
3 | const login = (credentials) =>
4 | post({
5 | url: getApiUrl(`api/users/login/`),
6 | payload: credentials,
7 | });
8 | const register = (inputs) =>
9 | post({
10 | url: getApiUrl(`api/users/register/`),
11 | payload: inputs,
12 | });
13 |
14 | const refreshToken = (refresh) =>
15 | post({
16 | url: getApiUrl('api/users/refresh_token/'),
17 | payload: {
18 | refresh,
19 | },
20 | });
21 |
22 | const deleteAccount = () =>
23 | post({
24 | url: getApiUrl('api/users/delete-profile/'),
25 | });
26 |
27 | const loginService = {
28 | login,
29 | register,
30 | refreshToken,
31 | deleteAccount,
32 | };
33 |
34 | export default loginService;
35 |
--------------------------------------------------------------------------------
/frontend/src/services/config.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import store from '../store';
4 |
5 | const getAccessToken = () => store.getState().auth.access;
6 |
7 | export const apiEndpointURL =
8 | process.env.REACT_APP_API_ENDPOINT || 'https://mumbleapi.herokuapp.com';
9 |
10 | export const getApiUrl = (path) => `${apiEndpointURL}/${path}`;
11 |
12 | const pullData = (request) => request.then(({ data }) => data);
13 |
14 | export const get = ({ url }) =>
15 | pullData(
16 | axios.get(url, {
17 | headers: {
18 | 'Content-type': 'application/json',
19 | Authorization: `Bearer ${getAccessToken()}`,
20 | },
21 | }),
22 | );
23 |
24 | export const post = ({ url, payload }) =>
25 | pullData(
26 | axios.post(url, payload, {
27 | headers: {
28 | 'Content-type': 'application/json',
29 | Authorization: `Bearer ${getAccessToken()}`,
30 | },
31 | }),
32 | );
33 |
34 | export const patch = ({ url, payload }) =>
35 | pullData(
36 | axios.patch(url, payload, {
37 | headers: {
38 | 'Content-type': 'application/json',
39 | Authorization: `Bearer ${getAccessToken()}`,
40 | },
41 | }),
42 | );
43 |
44 | export const put = ({ url, payload }) =>
45 | pullData(
46 | axios.put(url, payload, {
47 | headers: {
48 | 'Content-type': 'application/json',
49 | Authorization: `Bearer ${getAccessToken()}`,
50 | },
51 | }),
52 | );
53 |
54 | export const remove = ({ url }) =>
55 | pullData(
56 | axios.delete(url, {
57 | headers: {
58 | 'Content-type': 'application/json',
59 | Authorization: `Bearer ${getAccessToken()}`,
60 | },
61 | }),
62 | );
63 |
--------------------------------------------------------------------------------
/frontend/src/services/index.js:
--------------------------------------------------------------------------------
1 | export { default as PostsService } from './postsService';
2 | export { default as UsersService } from './usersService';
3 | export { default as AuthService } from './authService';
4 | export { default as ArticlesService } from './articlesService';
5 | export { default as NotificationsService } from './notificationsService';
6 | export { default as MessageService } from './messageService';
7 |
--------------------------------------------------------------------------------
/frontend/src/services/messageService.js:
--------------------------------------------------------------------------------
1 | import { get, getApiUrl, post, put } from './config';
2 |
3 | const getMessages = () => get({ url: getApiUrl(`api/messages/`) });
4 | const markAsRead = (messageId) => put({ url: getApiUrl(`api/messages/${messageId}/read/`) });
5 | const getUnreadMessagesCount = () => get({ url: getApiUrl(`api/messages/unread/count/`) });
6 | const createMessage = (message) =>
7 | post({
8 | url: getApiUrl(`api/messages/create/`),
9 | payload: message,
10 | });
11 |
12 | const messageSerivce = {
13 | getMessages,
14 | createMessage,
15 | markAsRead,
16 | getUnreadMessagesCount,
17 | };
18 |
19 | export default messageSerivce;
20 |
--------------------------------------------------------------------------------
/frontend/src/services/notificationsService.js:
--------------------------------------------------------------------------------
1 | import { get, put, getApiUrl } from './config';
2 |
3 | const getNotifications = () =>
4 | get({
5 | url: getApiUrl(`api/notifications/`),
6 | });
7 |
8 | const getUnreadNotifications = () =>
9 | get({
10 | url: getApiUrl(`api/notifications/?is_read=False`),
11 | });
12 |
13 | const markAsRead = (notificationId) =>
14 | put({
15 | url: getApiUrl(`api/notifications/${notificationId}/read/`),
16 | });
17 |
18 | const notificationsService = {
19 | getNotifications,
20 | markAsRead,
21 | getUnreadNotifications,
22 | };
23 |
24 | export default notificationsService;
25 |
--------------------------------------------------------------------------------
/frontend/src/services/postsService.js:
--------------------------------------------------------------------------------
1 | import { get, post, remove, getApiUrl } from './config';
2 |
3 | const getPostsByKeyword = (keyword) => get({ url: getApiUrl(`api/mumbles/${keyword}`) });
4 | const getPosts = (page = 1) =>
5 | get({
6 | url: getApiUrl(`api/mumbles/?page=${page}`),
7 | });
8 | const getPostsComments = (postId) =>
9 | get({
10 | url: getApiUrl(`api/mumbles/${postId}/comments/`),
11 | });
12 | const createPost = (postContent) =>
13 | post({
14 | url: getApiUrl(`api/mumbles/create/`),
15 | payload: postContent,
16 | });
17 | const modifyVote = (voteData) =>
18 | post({
19 | url: getApiUrl(`api/mumbles/vote/`),
20 | payload: voteData,
21 | });
22 | const createComment = (postData) =>
23 | post({
24 | url: getApiUrl(`api/mumbles/create/`),
25 | payload: postData,
26 | });
27 | const remumble = (postData) =>
28 | post({
29 | url: getApiUrl(`api/mumbles/remumble/`),
30 | payload: postData,
31 | });
32 | const deletePost = (postId) =>
33 | remove({
34 | url: getApiUrl(`api/mumbles/delete/${postId}/`),
35 | });
36 |
37 | const postsService = {
38 | getPostsByKeyword,
39 | getPosts,
40 | getPostsComments,
41 | createPost,
42 | createComment,
43 | deletePost,
44 | modifyVote,
45 | remumble,
46 | };
47 |
48 | export default postsService;
49 |
--------------------------------------------------------------------------------
/frontend/src/services/usersService.js:
--------------------------------------------------------------------------------
1 | import { get, getApiUrl, post, patch } from './config';
2 |
3 | const getRecommendedUsers = () => get({ url: getApiUrl(`api/users/recommended/`) });
4 |
5 | /**
6 | * @param {string} keyword - includes the search (query parameters) of the URL
7 | * keyword (only with search term) - `?q=john`
8 | * keyword (with search term and page no) - `?q=john&page=1`
9 | */
10 | const getUsersByKeyword = (keyword) => get({ url: getApiUrl(`api/users/${keyword}`) });
11 |
12 | const getUserByUsername = (username) => get({ url: getApiUrl(`api/users/${username}`) });
13 | const getUserPosts = (username) => get({ url: getApiUrl(`api/users/${username}/mumbles/`) });
14 | const getUsers = () => get({ url: getApiUrl(`api/users/`) });
15 | const followUser = (username) => post({ url: getApiUrl(`api/users/${username}/follow/`) }, {});
16 | const getUserArticles = (username) => get({ url: getApiUrl(`api/users/${username}/articles/`) });
17 | const getFollowing = () => get({ url: getApiUrl(`api/users/following/`) });
18 | const getProfile = () => get({ url: getApiUrl(`api/users/profile/`) });
19 |
20 | const updateUserProfileSkills = (skills) =>
21 | patch({
22 | url: getApiUrl(`api/users/profile_update/skills/`),
23 | payload: skills,
24 | });
25 |
26 | const updateUserProfile = (userData) =>
27 | patch({
28 | url: getApiUrl(`api/users/profile_update/`),
29 | payload: userData,
30 | });
31 | const updateUserProfilePic = (formData) =>
32 | patch({
33 | url: getApiUrl('api/users/profile_update/photo/'),
34 | payload: formData,
35 | });
36 | const getUsersBySkill = (skill, page = 1) =>
37 | get({
38 | url: getApiUrl(`api/users/skills/${skill}?page=${page}`),
39 | });
40 |
41 | const usersService = {
42 | getRecommendedUsers,
43 | getUserArticles,
44 | getUsersByKeyword,
45 | getUserByUsername,
46 | getUserPosts,
47 | getUsers,
48 | followUser,
49 | updateUserProfile,
50 | getUsersBySkill,
51 | updateUserProfilePic,
52 | updateUserProfileSkills,
53 | getProfile,
54 | getFollowing,
55 | };
56 | export default usersService;
57 |
--------------------------------------------------------------------------------
/frontend/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';
6 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension';
3 | import thunk from 'redux-thunk';
4 | import rootReducer from './reducers';
5 | import jwt_decode from 'jwt-decode';
6 |
7 | const accessToken = localStorage.getItem('access') ? localStorage.getItem('access') : null;
8 | const refreshToken = localStorage.getItem('refresh') ? localStorage.getItem('refresh') : null;
9 |
10 | let authState = {
11 | access: accessToken,
12 | refresh: refreshToken,
13 | isAuthenticated: false,
14 | user: null,
15 | };
16 |
17 | if (accessToken) {
18 | try {
19 | //Decode Token Here
20 | authState['user'] = jwt_decode(accessToken);
21 | authState['isAuthenticated'] = true;
22 | } catch (error) {
23 | authState['access'] = null;
24 | localStorage.removeItem('access');
25 | localStorage.removeItem('refresh');
26 | }
27 | }
28 |
29 | const initialState = {
30 | auth: authState,
31 | };
32 |
33 | const middleWare = [thunk];
34 |
35 | const store = createStore(
36 | rootReducer,
37 | initialState,
38 | composeWithDevTools({ mageAge: 200 })(applyMiddleware(...middleWare)),
39 | );
40 |
41 | export default store;
42 |
--------------------------------------------------------------------------------
/frontend/src/styles/App.css:
--------------------------------------------------------------------------------
1 | .custom-spacer {
2 | margin-top: 20px;
3 | margin-bottom: 20px;
4 | }
5 |
6 | .line-break {
7 | background-color: var(--color-light);
8 | height: 1px;
9 | margin-top: 20px;
10 | margin-bottom: 20px;
11 | }
12 |
13 | textarea:focus-within {
14 | outline: none;
15 | box-shadow: rgba(0, 0, 0, 0.16) 0 0 3px;
16 | }
17 |
18 | #feed-post-form textarea {
19 | display: block;
20 | width: 90%;
21 | margin: 0 auto;
22 | background-color: var(--color-bg);
23 | border: none;
24 | padding: 20px;
25 | font-size: 18px;
26 | border-radius: 50px;
27 | resize: none;
28 | }
29 |
30 | #post-btn-wrapper {
31 | display: flex;
32 | justify-content: flex-end;
33 | margin-top: 10px;
34 | padding-top: 10px;
35 | border-top: 1px solid var(--color-light);
36 | }
37 |
38 | #post-btn {
39 | text-align: right;
40 | }
41 |
42 | .post-user-name {
43 | text-decoration: none;
44 | color: var(--color-sub-light);
45 | }
46 |
47 | .contributor-wrapper {
48 | display: flex;
49 | justify-content: space-between;
50 | align-items: center;
51 | padding-top: 15px;
52 | padding-bottom: 15px;
53 | }
54 |
55 | .contributor-wrapper:first-child {
56 | padding-top: 0;
57 | }
58 |
59 | .contributor-wrapper:last-child {
60 | padding-bottom: 0;
61 | }
62 |
63 | .contributor-preview {
64 | display: flex;
65 | gap: 1rem;
66 | align-items: center;
67 | }
68 |
69 | .contributor-preview a {
70 | text-decoration: none;
71 | }
72 |
73 | .tags-wrapper {
74 | display: flex;
75 | gap: 1rem;
76 | flex-wrap: wrap;
77 | }
78 |
79 | .snippet-wrapper {
80 | display: flex;
81 | padding-top: 10px;
82 | padding-bottom: 10px;
83 | }
84 |
85 | .snippet-wrapper a {
86 | text-decoration: none;
87 | }
88 |
89 | .snippet-engagement-count {
90 | border-radius: 5px;
91 | display: flex;
92 | align-items: center;
93 | justify-content: center;
94 | background-color: var(--color-success);
95 | color: var(--color-text);
96 | min-width: 35px;
97 | padding: 5px;
98 | margin-right: 10px;
99 | }
100 |
101 | .snippet-engagement-count p {
102 | font-size: 1.8rem;
103 | color: var(--color-bg);
104 | font-weight: var(--font-medium);
105 | }
106 |
107 | .snippet-text {
108 | color: var(--color-light-gray);
109 | font-weight: var(--color-medium);
110 | font-size: 1.4rem;
111 | line-height: 1.35;
112 | transition: all 0.3s ease-out;
113 | }
114 |
115 | .snippet-text:hover {
116 | color: var(--color-main);
117 | }
118 |
119 | ::-webkit-scrollbar {
120 | width: 8px;
121 | background-color: #c4d0d3;
122 | }
123 |
124 | ::-webkit-scrollbar-thumb {
125 | background-color: #5aa5b9;
126 | border-radius: 5px;
127 | }
128 |
129 | ::-webkit-scrollbar-thumb:hover {
130 | background-color: #4d94a8;
131 | }
132 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_animation.css:
--------------------------------------------------------------------------------
1 | @keyframes move-in-left {
2 | 0% {
3 | width: 0%;
4 | transform: scale(2);
5 | }
6 |
7 | 80% {
8 | width: 70%;
9 | transform: scale(1);
10 | }
11 |
12 | 100% {
13 | width: 100%;
14 | }
15 | }
16 |
17 | @keyframes loading {
18 | from {
19 | transform: scale(0, 0);
20 | }
21 |
22 | to {
23 | transform: scale(1, 1);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_author-box.css:
--------------------------------------------------------------------------------
1 | .author-box {
2 | display: inline-flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | }
6 |
7 | .author-box__info {
8 | margin-left: 1rem;
9 | line-height: 1.2;
10 | }
11 |
12 | .author-box--md .author-box__info {
13 | margin-left: 1.6rem;
14 | line-height: 1.3;
15 | }
16 |
17 | .author-box--lg .author-box__info {
18 | margin-left: 2.8rem;
19 | line-height: 1.6;
20 | }
21 |
22 | .author-box__name {
23 | font-size: 1.5rem;
24 | font-weight: var(--font-medium);
25 | color: var(--color-sub);
26 | }
27 |
28 | .dark-theme .author-box__name {
29 | color: var(--color-text);
30 | }
31 |
32 | .author-box--md .author-box__name {
33 | font-size: 1.65rem;
34 | }
35 |
36 | .author-box--lg .author-box__name {
37 | font-size: 2.4rem;
38 | }
39 |
40 | .author-box__handle {
41 | font-size: 1.4rem;
42 | color: var(--color-text);
43 | font-weight: var(--font-regular);
44 | }
45 |
46 | .dark-theme .author-box__handle {
47 | color: var(--color-main);
48 | }
49 |
50 | .author-box--md .author-box__handle {
51 | font-size: 1.5rem;
52 | }
53 |
54 | .author-box--lg .author-box__handle {
55 | font-size: 1.65rem;
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_avatar.css:
--------------------------------------------------------------------------------
1 | .avatar {
2 | border-radius: 50%;
3 | border: 2px solid var(--color-main);
4 | object-fit: cover;
5 | }
6 |
7 | .avatar--xl {
8 | height: 20rem;
9 | width: 20rem;
10 | }
11 |
12 | .avatar--lg {
13 | height: 15rem;
14 | width: 15rem;
15 | }
16 |
17 | .avatar--md {
18 | height: 7rem;
19 | width: 7rem;
20 | }
21 |
22 | .avatar--sm {
23 | height: 5rem;
24 | width: 5rem;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_base.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | text-rendering: optimizeLegibility;
6 | color: inherit;
7 | font-size: inherit;
8 | }
9 |
10 | html {
11 | font-size: 50%;
12 | }
13 |
14 | @media only screen and (min-width: 480px) {
15 | html {
16 | font-size: 56.25%;
17 | }
18 | }
19 |
20 | @media only screen and (min-width: 1200px) {
21 | html {
22 | font-size: 62.5%;
23 | }
24 | }
25 |
26 | @media only screen and (min-width: 2100px) {
27 | html {
28 | font-size: 75%;
29 | }
30 | }
31 |
32 | body,
33 | .app {
34 | line-height: 1.6;
35 | font-weight: 400;
36 | font-size: 1.5rem;
37 | color: var(--color-text);
38 | background-color: var(--color-bg);
39 | min-height: 100vh;
40 | }
41 |
42 | code {
43 | border-radius: 0.5rem;
44 | padding: 1rem 2rem;
45 | background-color: var(--color-sub);
46 | color: var(--color-white);
47 | }
48 |
49 | .dark-theme code {
50 | color: var(--color-text);
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_card.css:
--------------------------------------------------------------------------------
1 | .card {
2 | border-radius: 1rem;
3 | background-color: var(--color-white);
4 | border: 1.5px solid var(--color-light);
5 | width: auto;
6 | height: auto;
7 | margin-top: 0.5rem;
8 | margin-bottom: 0.5rem;
9 | overflow: hidden;
10 | }
11 |
12 | .card__link {
13 | transition: all 0.3s ease-in;
14 | }
15 |
16 | .card__link:hover {
17 | text-decoration: none;
18 | }
19 |
20 | .card__body,
21 | .card__header {
22 | padding: 2rem 2.5rem;
23 | }
24 |
25 | .card__header {
26 | padding-top: 1.5rem;
27 | padding-bottom: 1.5rem;
28 | border-bottom: 1.5px solid var(--color-light);
29 | }
30 |
31 | .card__headerTitle {
32 | font-size: 2.2rem;
33 | font-weight: var(--font-medium);
34 | }
35 |
36 | .card__header .card__link:hover {
37 | text-decoration: underline;
38 | color: var(--color-main);
39 | }
40 |
41 | .card.card--dark {
42 | background-color: var(--color-light);
43 | }
44 |
45 | .card.card--dark .card__header {
46 | box-shadow: var(--generic-shadow);
47 | }
48 |
49 | .dark-theme .card {
50 | background-color: var(--color-sub-light);
51 | }
52 |
53 | .dark-theme .card.card--dark {
54 | background-color: var(--color-main-light);
55 | }
56 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_loading.css:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | flex-direction: column;
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | right: 0;
10 | bottom: 0;
11 | background-color: var(--color-bg);
12 | }
13 |
14 | .loading__logo img {
15 | height: 10rem;
16 | margin-bottom: 3.5rem;
17 | }
18 |
19 | .loading__loader {
20 | display: inline-block;
21 | font-size: 0;
22 | padding: 0;
23 | }
24 |
25 | .loading__loader span {
26 | vertical-align: middle;
27 | border-radius: 100%;
28 | display: inline-block;
29 | width: 2rem;
30 | height: 2rem;
31 | margin: 0.5rem;
32 | -webkit-animation: loading 0.8s linear infinite alternate;
33 | animation: loading 0.8s linear infinite alternate;
34 | background-color: var(--color-main);
35 | }
36 |
37 | .loading__loader span:nth-child(1) {
38 | -webkit-animation-delay: -1s;
39 | animation-delay: -1s;
40 | opacity: 0.6;
41 | }
42 |
43 | .loading__loader span:nth-child(2) {
44 | -webkit-animation-delay: -0.8s;
45 | animation-delay: -0.8s;
46 | opacity: 0.8;
47 | }
48 |
49 | .loading__loader span:nth-child(3) {
50 | -webkit-animation-delay: -0.26666s;
51 | animation-delay: -0.26666s;
52 | opacity: 1;
53 | }
54 |
55 | .loading__loader span:nth-child(4) {
56 | -webkit-animation-delay: -0.8s;
57 | animation-delay: -0.8s;
58 | opacity: 0.8;
59 | }
60 |
61 | .loading__loader span:nth-child(5) {
62 | -webkit-animation-delay: -1s;
63 | animation-delay: -1s;
64 | opacity: 0.4;
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_message.css:
--------------------------------------------------------------------------------
1 | .message {
2 | padding: 20px;
3 | border: 1px solid #888;
4 | border-radius: 2px;
5 | display: flex;
6 | justify-content: space-between;
7 | }
8 |
9 | .message--success {
10 | background-color: #f3fdf2;
11 | border-color: var(--color-success);
12 | color: var(--color-success);
13 | }
14 |
15 | .message--error {
16 | background-color: #f8d7da;
17 | border-color: #f5c2c7;
18 | color: #842029;
19 | }
20 |
21 | .message--warning {
22 | background-color: #fff3cd;
23 | border-color: #ffecb5;
24 | color: #664d03;
25 | }
26 |
27 | .message--warning p {
28 | color: #664d03;
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_page.css:
--------------------------------------------------------------------------------
1 | .contentArea {
2 | margin-top: 2.5rem;
3 | gap: 1.5rem;
4 | display: flex;
5 | justify-content: space-between;
6 | width: 100%;
7 | margin-left: auto;
8 | }
9 |
10 | .contentArea > *:first-of-type {
11 | flex-basis: 100%;
12 | }
13 |
14 | .contentArea > *:last-of-type {
15 | display: none;
16 | }
17 |
18 | .contentArea > *:first-of-type > *,
19 | .contentArea > *:last-of-type > * {
20 | margin-bottom: 1.5rem;
21 | }
22 |
23 | @media screen and (min-width: 650px) {
24 | .contentArea {
25 | width: 81%;
26 | }
27 |
28 | .contentArea > *:first-of-type {
29 | margin-left: auto;
30 | flex-basis: 85%;
31 | }
32 | }
33 |
34 | @media screen and (min-width: 900px) {
35 | .contentArea > *:first-of-type {
36 | flex-basis: 90%;
37 | }
38 | }
39 |
40 | @media screen and (min-width: 1150px) {
41 | .contentArea > *:first-of-type {
42 | flex-basis: 65%;
43 | }
44 |
45 | .contentArea > *:last-of-type {
46 | display: block;
47 | flex-basis: 35%;
48 | }
49 | }
50 |
51 | .contentArea--fullWidth {
52 | width: 100% !important;
53 | }
54 |
55 | .contentArea--singleContent > *:first-of-type {
56 | flex-basis: 100% !important;
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_tag-input.css:
--------------------------------------------------------------------------------
1 | .input-tags {
2 | width: 100%;
3 | border-radius: 0.5rem;
4 | border: 2px solid var(--color-light);
5 | padding: 0.6rem 0.8rem;
6 | display: flex;
7 | flex-wrap: wrap;
8 | transition: all 0.3s ease-in-out;
9 | cursor: text;
10 | }
11 |
12 | .input-tags:focus-within {
13 | border: 2px solid var(--color-main);
14 | }
15 |
16 | .input-tags input {
17 | border: none;
18 | font-size: 14px;
19 | padding: 0.5rem 1rem;
20 | width: 100%;
21 | }
22 |
23 | .input-tags input:focus {
24 | outline: none;
25 | }
26 |
27 | .input-tag-list {
28 | display: flex;
29 | column-gap: 4px;
30 | flex-wrap: wrap !important;
31 | row-gap: 4px;
32 | }
33 |
34 | .input-tag-item {
35 | white-space: nowrap;
36 | }
37 |
38 | .input-tag-item small {
39 | margin-right: 6px;
40 | }
41 |
42 | .input-tag-item i {
43 | color: var(--color-white);
44 | cursor: pointer;
45 | }
46 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_tag.css:
--------------------------------------------------------------------------------
1 | .tag {
2 | display: inline-block;
3 | background-color: var(--color-sub);
4 | height: -webkit-fit-content;
5 | height: -moz-fit-content;
6 | height: fit-content;
7 | width: -webkit-fit-content;
8 | width: -moz-fit-content;
9 | width: fit-content;
10 | border-radius: 0.5rem;
11 | padding: 0.5rem 1rem;
12 | font-size: 1.4rem;
13 | cursor: pointer;
14 | }
15 |
16 | .tag > small {
17 | color: var(--color-white);
18 | }
19 |
20 | .tag--outline > small {
21 | color: var(--color-sub);
22 | }
23 |
24 | .dark-theme .tag > small {
25 | color: var(--color-text);
26 | }
27 |
28 | .tag--outline {
29 | border: 2px solid var(--color-sub);
30 | background-color: transparent;
31 | padding: 0.4rem 1rem;
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_toastify.css:
--------------------------------------------------------------------------------
1 | .Toastify__toast {
2 | padding: 0.8rem 2rem;
3 | border-radius: 0.5rem;
4 | }
5 |
6 | .Toastify .Toastify__toast--default {
7 | color: var(--color-main);
8 | }
9 |
10 | .dark-theme .Toastify .Toastify__toast--default {
11 | background-color: var(--color-sub-light);
12 | }
13 |
14 | .Toastify .Toastify__close-button {
15 | position: absolute;
16 | top: 55%;
17 | transform: translateY(-50%);
18 | right: 2rem;
19 | }
20 |
21 | .Toastify .Toastify__close-button.Toastify__close-button--default > svg {
22 | fill: var(--color-main);
23 | }
24 |
25 | .Toastify .Toastify__progress-bar--default {
26 | background: var(--color-main);
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_typography.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: var(--font-base);
3 | }
4 |
5 | h1 {
6 | font-size: 4.8rem;
7 | font-weight: var(--font-medium);
8 | }
9 |
10 | h2 {
11 | font-size: 3.6rem;
12 | font-weight: var(--font-bold);
13 | }
14 |
15 | h3 {
16 | font-size: 3.2rem;
17 | font-weight: var(--font-medium);
18 | }
19 |
20 | h4 {
21 | font-size: 2.8rem;
22 | font-weight: var(--font-medium);
23 | }
24 |
25 | h5 {
26 | font-size: 2.4rem;
27 | font-weight: var(--font-medium);
28 | }
29 |
30 | h5,
31 | h6 {
32 | font-weight: var(--font-regular);
33 | }
34 |
35 | h6 {
36 | font-size: 1.8rem;
37 | }
38 |
39 | p,
40 | span,
41 | strong {
42 | font-size: 1.6rem;
43 | color: var(--color-text);
44 | font-weight: var(--font-regular);
45 | }
46 |
47 | strong {
48 | font-weight: var(--font-medium);
49 | }
50 |
51 | pre > *,
52 | pre > code * {
53 | font-family: var(--font-monospace) !important;
54 | }
55 |
56 | a {
57 | padding-bottom: 2px;
58 | text-decoration: none;
59 | display: inline-block;
60 | color: var(--color-main);
61 | font-weight: var(--font-medium);
62 | }
63 |
64 | .dark-theme .fas,
65 | .dark-theme .far {
66 | color: var(--color-text);
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_utilities.css:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 92%;
3 | max-width: 165rem;
4 | margin: 0 auto;
5 | box-sizing: border-box;
6 | }
7 |
8 | @media screen and (min-width: 1000px) {
9 | .container {
10 | width: 90%;
11 | }
12 | }
13 |
14 | @media screen and (min-width: 1200px) {
15 | .container {
16 | width: 80%;
17 | }
18 | }
19 |
20 | @media screen and (min-width: 1440px) {
21 | .container {
22 | width: 72%;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/styles/common/_variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-main: #5aa5b9;
3 | --color-main-light: #e1f6fb;
4 | --color-sub: #3f4156;
5 | --color-sub-light: #51546e;
6 | --color-text: #737373;
7 | --color-gray: #8b8b8b;
8 | --color-light: #e5e7eb;
9 | --color-light-gray: #767676;
10 | --color-bg: #f8fafd;
11 | --color-white: #fffefd;
12 | --color-white-light: #fafafa;
13 | --color-success: #5dd693;
14 | --color-error: #fc4b0b;
15 | --font-base: 'Poppins', arial, helvetica, 'Segoe UI', roboto, ubuntu, sans-serif;
16 | --font-monospace: 'Fira Code', 'Courier New', courier, monospace;
17 | --font-regular: 300;
18 | --font-medium: 500;
19 | --font-bold: 700;
20 | --generic-shadow: 0 1px 3px #00000015;
21 | }
22 |
23 | .dark-theme {
24 | --color-main: #71c6dd;
25 | --color-sub-light: #3f4156;
26 | --color-sub: #696d97;
27 | --color-main-light: #3f4156;
28 | --color-text: #f5f5f5;
29 | --color-gray: #c5c5c5;
30 | --color-light: #313131;
31 | --color-light-gray: #bbb;
32 | --color-bg: #2d2d39;
33 | --color-white: #1f1f1f;
34 | --color-white-light: #1f1f1f;
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/ArticlePage.css:
--------------------------------------------------------------------------------
1 | #article--layout {
2 | display: grid;
3 | grid-template-columns: 2fr 1fr;
4 | gap: 1rem;
5 | }
6 |
7 | @media screen and (max-width: 1000px) {
8 | #article--layout {
9 | grid-template-columns: 1fr;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/CreateArticlePage.css:
--------------------------------------------------------------------------------
1 | .create-article--layout {
2 | display: grid;
3 | grid-template-columns: 3fr 1fr;
4 | column-gap: 1rem;
5 | }
6 |
7 | .article-headline {
8 | font-size: 32px;
9 | }
10 |
11 | .article-header {
12 | margin-bottom: 20px;
13 | border-bottom: 1px solid var(--color-light);
14 | }
15 |
16 | /* WYSIWYG CKEDITOR */
17 | .ck-editor__editable_inline {
18 | min-height: 150px;
19 | margin-bottom: 1.5rem;
20 | padding-left: 30px;
21 | }
22 |
23 | /* TODO: Figure out why we need this? This seems like a bug in CK Editor with bullet lists not indenting. */
24 | .ck-content li {
25 | margin-left: 50px;
26 | }
27 |
28 | :root .dark-theme {
29 | /* CKEditor content area */
30 | --ck-color-base-background: var(--color-bg);
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/CreateDiscussionPage.css:
--------------------------------------------------------------------------------
1 | .discussion-headline {
2 | font-size: 32px;
3 | }
4 |
5 | .discussion-header {
6 | margin-bottom: 20px;
7 | border-bottom: 1px solid var(--color-light);
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/CreatePost.css:
--------------------------------------------------------------------------------
1 | /* until we need it, leaving this file */
2 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/DeleteAccountPage.css:
--------------------------------------------------------------------------------
1 | .DeleteAccountPage__message {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 | transform: translate(-50%, -50%);
6 | text-align: justify;
7 | text-justify: inter-word;
8 | }
9 |
10 | .DeleteAccountPage__btn--left {
11 | margin-right: 5%;
12 | }
13 |
14 | .DeleteAccountPage__buttons {
15 | display: flex;
16 | justify-content: center;
17 | }
18 |
19 | .DeleteAccountPage__themeToggler {
20 | -webkit-tap-highlight-color: transparent;
21 | float: right;
22 | font-size: 3rem;
23 | margin-top: 1.5%;
24 | margin-right: 14.5%;
25 | cursor: pointer;
26 | }
27 |
28 | @media screen and (max-width: 800px) {
29 | .DeleteAccountPage__themeToggler {
30 | font-size: 3rem;
31 | margin-top: 10%;
32 | margin-right: 12%;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/DiscussionPage.css:
--------------------------------------------------------------------------------
1 | .discussion--layout {
2 | display: grid;
3 | grid-template-columns: 3fr 1fr;
4 | column-gap: 1rem;
5 | }
6 |
7 | .question-wrapper {
8 | display: grid;
9 | grid-template-columns: 1fr 10fr;
10 | margin-bottom: 20px;
11 | }
12 |
13 | .discussion-headline {
14 | font-size: 32px;
15 | }
16 |
17 | .asked-by {
18 | margin-top: 10px;
19 | color: var(--color-gray);
20 | font-style: italic;
21 | }
22 |
23 | .question-sidebar {
24 | margin-right: 10px;
25 | justify-self: center;
26 | display: flex;
27 | flex-direction: column;
28 | align-items: center;
29 | }
30 |
31 | .big-vote-count {
32 | font-size: 16px;
33 | font-weight: 900;
34 | color: var(--color-success);
35 | }
36 |
37 | .big-up-arrow {
38 | font-size: 18px;
39 | }
40 |
41 | .big-down-arrow {
42 | font-size: 18px;
43 | }
44 |
45 | .voting-widget {
46 | margin-top: 15px;
47 | display: flex;
48 | flex-direction: column;
49 | align-items: center;
50 | }
51 |
52 | .question-body {
53 | margin-bottom: 20px;
54 | height: 100%;
55 | display: flex;
56 | flex-direction: column;
57 | justify-content: space-around;
58 | border-bottom: 1px solid var(--color-light);
59 | }
60 |
61 | /* .snippet-wrapper:hover {
62 | transition: 0.2s;
63 | border-radius: 5px;
64 | background-color: var(--color-white);
65 | } */
66 |
67 | @media screen and (max-width: 1300px) {
68 | .discussion--layout {
69 | grid-template-columns: 2fr 1fr;
70 | }
71 | }
72 |
73 | @media screen and (max-width: 1000px) {
74 | .discussion--layout {
75 | grid-template-columns: 1fr;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Error404Page.css:
--------------------------------------------------------------------------------
1 | .main404page {
2 | align-items: center;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | text-align: center;
7 | }
8 |
9 | .main404page__title {
10 | font-size: 18rem;
11 | margin: 0.025em 0;
12 | text-shadow: 0.05em 0.05em 0 rgba(0, 0, 0, 0.25);
13 | white-space: nowrap;
14 | }
15 |
16 | .main404page__sub-title {
17 | margin-bottom: 0.4em;
18 | }
19 |
20 | .main404page__info {
21 | margin-top: 0;
22 | font-size: 15px;
23 | }
24 |
25 | .main404page__logo {
26 | height: 130px;
27 | width: 130px;
28 | }
29 |
30 | @media screen and (max-width: 480px) {
31 | .main404page__logo {
32 | height: 100px;
33 | width: 100px;
34 | }
35 | }
36 |
37 | .main404page__themeToggler {
38 | float: right;
39 | font-size: 3rem;
40 | margin-top: 2%;
41 | margin-right: -80rem;
42 | cursor: pointer;
43 | -webkit-tap-highlight-color: transparent;
44 | }
45 |
46 | @media screen and (max-width: 800px) {
47 | .main404page__themeToggler {
48 | font-size: 3rem;
49 | margin-top: 4%;
50 | margin-right: -50rem;
51 | -webkit-tap-highlight-color: transparent;
52 | }
53 | }
54 |
55 | @media screen and (max-width: 580px) {
56 | .main404page__themeToggler {
57 | font-size: 3rem;
58 | margin-top: 4%;
59 | margin-right: -38rem;
60 | -webkit-tap-highlight-color: transparent;
61 | }
62 | }
63 |
64 | @media screen and (max-width: 380px) {
65 | .main404page__themeToggler {
66 | font-size: 3rem;
67 | margin-top: 4%;
68 | margin-right: -28rem;
69 | -webkit-tap-highlight-color: transparent;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Error500Page.css:
--------------------------------------------------------------------------------
1 | .error500page {
2 | text-align: center;
3 | }
4 |
5 | .error500page__banner {
6 | font-size: 18rem;
7 | text-shadow: var(--generic-shadow);
8 | white-space: nowrap;
9 | }
10 |
11 | .error500page__logo {
12 | height: 140px;
13 | width: 140px;
14 | }
15 |
16 | .error500page__title {
17 | margin-bottom: 0.4em;
18 | }
19 |
20 | .error500page__info {
21 | font-size: 2.4rem;
22 | }
23 |
24 | .error500page__link {
25 | margin: 1rem;
26 | font-size: 2rem;
27 | }
28 |
29 | .error500page__code,
30 | .error500page__code pre {
31 | margin-top: 3rem;
32 | }
33 |
34 | @media screen and (max-width: 480px) {
35 | .error500page__logo {
36 | height: 100px;
37 | width: 100px;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/HeaderBar.css:
--------------------------------------------------------------------------------
1 | .header {
2 | position: sticky;
3 | top: 0;
4 | width: 100%;
5 | background-color: var(--color-sub);
6 | color: var(--color-white);
7 | z-index: 100;
8 | box-shadow: 0 1px 6px 3px #00000015;
9 | }
10 |
11 | .header > .container {
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 | }
16 |
17 | .dark-theme .header {
18 | background-color: var(--color-main-light);
19 | }
20 |
21 | .header__logo {
22 | text-transform: lowercase;
23 | display: flex;
24 | justify-content: center;
25 | }
26 |
27 | .header__logo a {
28 | text-decoration: none;
29 | color: #fff;
30 | }
31 |
32 | .header__logo img {
33 | position: relative;
34 | height: 4.2rem;
35 | left: -0.6rem;
36 | }
37 |
38 | .header__logo h3 {
39 | display: inline-block;
40 | margin-left: 0.85rem;
41 | font-size: 3.2rem;
42 | font-weight: var(--font-bold);
43 | color: var(--color-main);
44 | }
45 |
46 | .header__menubar {
47 | font-size: 1.8rem;
48 | background-color: transparent;
49 | margin-right: 2.5rem;
50 | border: none;
51 | outline: none;
52 | cursor: pointer;
53 | display: inline-block;
54 | }
55 |
56 | @media screen and (min-width: 650px) {
57 | .header__menubar {
58 | display: none;
59 | }
60 | }
61 |
62 | .header__nav {
63 | display: flex;
64 | align-items: center;
65 | }
66 |
67 | .nav-icon {
68 | font-size: 24px;
69 | position: relative;
70 | cursor: pointer;
71 | }
72 |
73 | .nav-item {
74 | margin: 10px;
75 | }
76 |
77 | .nav-item:last-child {
78 | margin-right: 0;
79 | }
80 |
81 | .nav-avatar {
82 | margin: 10px;
83 | min-width: 5rem;
84 | }
85 |
86 | #user--navigation {
87 | position: fixed;
88 | width: 300px;
89 | top: 60px;
90 | right: 20px;
91 | max-height: 60vh;
92 | overflow-y: scroll;
93 | }
94 |
95 | #nav-toggle-icon {
96 | cursor: pointer;
97 | }
98 |
99 | .user-navigation--item {
100 | padding: 10px;
101 | border-bottom: 1px solid var(--color-light);
102 | display: flex;
103 | align-items: center;
104 | gap: 1rem;
105 | cursor: pointer;
106 | color: var(--color-text);
107 | text-transform: capitalize;
108 | }
109 |
110 | .user-navigation--item:hover {
111 | background-color: var(--color-light);
112 | }
113 |
114 | .user--nav--icon {
115 | color: var(--color-sub);
116 | margin-right: 10px;
117 | }
118 |
119 | .header-notification--unread {
120 | font-weight: 500;
121 | }
122 |
123 | .header-notification {
124 | font-weight: 300;
125 | }
126 |
127 | .header-notification__name {
128 | margin-right: 6px;
129 | font-weight: inherit;
130 | }
131 |
132 | .header-notification--read {
133 | font-weight: 300;
134 | }
135 |
136 | .nav-icon--unread {
137 | position: absolute;
138 | width: 10px;
139 | height: 10px;
140 | top: 0;
141 | right: 0;
142 | background: var(--color-error);
143 | border-radius: 50%;
144 | }
145 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/HomePage.css:
--------------------------------------------------------------------------------
1 | /* Leaving this file if we need custom styles in the future */
2 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/InboxPage.css:
--------------------------------------------------------------------------------
1 | .empty-mailbox__image {
2 | width: 30%;
3 | margin-top: 4rem;
4 | margin-bottom: 4rem;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/LogoutConfirmation.css:
--------------------------------------------------------------------------------
1 | .LogoutConfirmation__message {
2 | position: absolute;
3 | top: 50%;
4 | left: 50%;
5 | transform: translate(-50%, -50%);
6 | text-align: justify;
7 | text-justify: inter-word;
8 | }
9 |
10 | .LogoutConfirmation__btn--left {
11 | margin-right: 5%;
12 | }
13 |
14 | .LogoutConfirmation__buttons {
15 | display: flex;
16 | justify-content: center;
17 | }
18 |
19 | .LogoutConfirmation__themeToggler {
20 | -webkit-tap-highlight-color: transparent;
21 | float: right;
22 | font-size: 3rem;
23 | margin-top: 1.5%;
24 | margin-right: 14.5%;
25 | cursor: pointer;
26 | }
27 |
28 | @media screen and (max-width: 800px) {
29 | .LogoutConfirmation__themeToggler {
30 | font-size: 3rem;
31 | margin-top: 10%;
32 | margin-right: 12%;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/Modal.css:
--------------------------------------------------------------------------------
1 | .modal-backdrop {
2 | position: fixed;
3 | display: none;
4 | justify-content: center;
5 | align-items: center;
6 | width: 100%;
7 | height: 100%;
8 | background-color: rgba(0, 0, 0, 0.5);
9 | opacity: 0;
10 | transition: all 0.2s ease-in-out;
11 | z-index: 1000;
12 | }
13 |
14 | .model-backdrop.active {
15 | display: flex;
16 | opacity: 1;
17 | }
18 |
19 | .model-backdrop.inactive {
20 | opacity: 0;
21 | display: none;
22 | }
23 |
24 | .modal-backdrop .mumble-modal {
25 | background: white;
26 | border-radius: 6px;
27 | width: 100%;
28 | max-width: 600px;
29 | max-height: 98%;
30 | padding: 20px;
31 | margin: 0 15px;
32 | }
33 |
34 | .close-modal {
35 | position: absolute;
36 | top: 0;
37 | right: 0;
38 | cursor: pointer;
39 | }
40 |
41 | .close-icon {
42 | font-size: 14px;
43 | padding: 0;
44 | }
45 |
46 | @media (min-width: 400px) {
47 | .modal-backdrop .mumble-modal {
48 | margin: 0 25px;
49 | }
50 |
51 | .close-icon {
52 | font-size: 16px;
53 | padding: 2px;
54 | }
55 | }
56 |
57 | @media (min-width: 1000px) {
58 | .close-icon {
59 | font-size: 18px;
60 | padding: 3px;
61 | }
62 | }
63 |
64 | .mumble-modal .modal-header {
65 | position: relative;
66 | }
67 |
68 | .modal-header .modal-close {
69 | position: absolute;
70 | right: 0;
71 | }
72 |
73 | .mumble-modal .modal-actions {
74 | margin-top: 15px;
75 | display: flex;
76 | width: 100%;
77 | justify-content: flex-end;
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/NotificationPage.css:
--------------------------------------------------------------------------------
1 | .notification {
2 | padding: 10px 20px;
3 | border-bottom: 1px solid var(--color-light);
4 | display: flex;
5 | align-items: center;
6 | gap: 1rem;
7 | margin-bottom: 1rem;
8 | }
9 |
10 | .notification--item {
11 | padding: 15px 20px;
12 | border-bottom: 1px solid var(--color-light);
13 | display: flex;
14 | align-items: center;
15 | gap: 1rem;
16 | cursor: pointer;
17 | }
18 |
19 | .notification__content--unread {
20 | font-weight: 500;
21 | }
22 |
23 | .notification--item:hover {
24 | background-color: var(--color-light);
25 | }
26 |
27 | .notification__right-content {
28 | margin-left: 25px;
29 | flex-grow: 1;
30 | }
31 |
32 | .notification--meta {
33 | line-height: 1;
34 | margin-bottom: 0.5rem;
35 | font-weight: var(--font-medium);
36 | font-size: 1.3rem;
37 | color: var(--color-light-gray);
38 | margin-left: 80%;
39 | }
40 |
41 | .notification-link {
42 | width: 100%;
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/NotificationTitle.css:
--------------------------------------------------------------------------------
1 | .notification-title {
2 | font-weight: 500;
3 | }
4 |
5 | .notification-title__name {
6 | margin-right: 6px;
7 | }
8 |
9 | .notification-title__link {
10 | color: var(--color-main);
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/PostCardOptions.css:
--------------------------------------------------------------------------------
1 | .post--options {
2 | padding-bottom: 1.5rem;
3 | line-height: 2;
4 | }
5 |
6 | .post__dropmenu {
7 | position: relative;
8 | display: flex;
9 | justify-content: flex-end;
10 | }
11 |
12 | .post__dropmenu--icon {
13 | cursor: pointer;
14 | font-size: 1.8rem;
15 | font-weight: bold;
16 | letter-spacing: 0.15em;
17 | }
18 |
19 | .dropmenu {
20 | width: 16em;
21 | transform-origin: top right;
22 | transition: all 0.05s ease-in-out 0s;
23 | transform: scaleY(0);
24 | top: 2em;
25 | position: absolute;
26 | z-index: 10;
27 | }
28 |
29 | .dropmenu--show {
30 | transform: scaleY(1);
31 | }
32 |
33 | .dropmenu__menuitem {
34 | padding: 10px;
35 | border-bottom: 1px solid var(--color-light);
36 | display: flex;
37 | align-items: center;
38 | gap: 1rem;
39 | cursor: pointer;
40 | color: var(--color-text);
41 | text-transform: capitalize;
42 | }
43 |
44 | .dropmenu__menuitem:hover {
45 | background-color: var(--color-light);
46 | }
47 |
48 | .post__dropmenu--navicon {
49 | color: var(--color-sub);
50 | margin-right: 10px;
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/PostCardPlaceholder.css:
--------------------------------------------------------------------------------
1 | .post-card-placeholder {
2 | padding: 2.5rem;
3 | }
4 |
5 | .post-card-placeholder__header {
6 | display: flex;
7 | margin-bottom: 20px;
8 | }
9 |
10 | .post-card-placeholder__header-author-box {
11 | padding-left: 10px;
12 | padding-top: 20px;
13 | width: 120px;
14 | }
15 |
16 | .post-card-placeholder__body {
17 | margin-bottom: 60px;
18 | }
19 |
20 | .post-card-placeholder__actions {
21 | display: flex;
22 | justify-content: space-between;
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/ProfilePage.css:
--------------------------------------------------------------------------------
1 | /* Leaving this file for custom styles in the future */
2 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/SearchBox.css:
--------------------------------------------------------------------------------
1 | #search-form {
2 | margin: 0 1rem;
3 | display: flex;
4 | align-items: center;
5 | border-radius: 10rem;
6 | padding: 0.9rem 1.5rem;
7 | background-color: var(--color-sub-light);
8 | border: 2px solid transparent;
9 | transition: all 0.3s ease-in-out;
10 | -webkit-touch-callout: none;
11 | -webkit-user-select: none;
12 | -khtml-user-select: none;
13 | -moz-user-select: none;
14 | -ms-user-select: none;
15 | user-select: none;
16 | }
17 |
18 | .dark-theme #search-form {
19 | color: var(--color-text);
20 | background-color: var(--color-sub);
21 | }
22 |
23 | #search-form input {
24 | color: var(--color-light);
25 | background-color: transparent;
26 | outline: none;
27 | border: 2px solid transparent;
28 | width: 100%;
29 | margin-left: 1rem;
30 | margin-right: 1rem;
31 | }
32 |
33 | .dark-theme #search-form > input {
34 | color: var(--color-text);
35 | }
36 |
37 | #search-form:focus-within {
38 | outline: none;
39 | border: 2px solid var(--color-main);
40 | }
41 |
42 | #search-form input::placeholder {
43 | color: var(--color-gray);
44 | }
45 |
46 | .shortcut__Text {
47 | font-size: 8px;
48 | }
49 |
50 | .shortcut__Key {
51 | font-size: 10px;
52 | background-color: #303030;
53 | color: #fff;
54 | }
55 |
56 | @media screen and (max-width: 480px) {
57 | .shortcut__Text {
58 | display: none;
59 | }
60 | }
61 |
62 | @media screen and (max-width: 768px) {
63 | .shortcut__Text {
64 | display: none;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/SearchByPanel.css:
--------------------------------------------------------------------------------
1 | .search-categories {
2 | height: 160px;
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/SearchByUsersAndPostList.css:
--------------------------------------------------------------------------------
1 | .not-found h2 {
2 | color: var(--color-text);
3 | font-size: 6rem;
4 | }
5 |
6 | .not-found span img {
7 | height: 4.2rem;
8 | }
9 |
10 | @keyframes fadeInUp {
11 | from {
12 | opacity: 0;
13 | -webkit-transform: translate3d(0, 100%, 0);
14 | transform: translate3d(0, 100%, 0);
15 | }
16 |
17 | to {
18 | opacity: 1;
19 | -webkit-transform: none;
20 | transform: none;
21 | }
22 | }
23 |
24 | .not-found__logo {
25 | opacity: 0;
26 | animation: fadeInUp 1s ease-in-out 0s forwards;
27 | animation-delay: 0.2s;
28 | }
29 |
30 | .not-found h2,
31 | .not-found h3,
32 | .not-found p {
33 | text-align: center;
34 | }
35 |
36 | .not-found h3 {
37 | font-size: 28px;
38 | }
39 |
40 | .not-found {
41 | text-align: center;
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/SearchPage.css:
--------------------------------------------------------------------------------
1 | .searchitem {
2 | display: flex;
3 | gap: 1rem;
4 | justify-content: space-between;
5 | width: 100%;
6 | margin-bottom: 2rem;
7 | }
8 |
9 | .searchitem__link {
10 | width: 100%;
11 | }
12 |
13 | .searchitem__info {
14 | flex: 1;
15 | margin-top: 1.2rem;
16 | }
17 |
18 | .searchitem__info-top {
19 | display: flex;
20 | align-items: center;
21 | margin-bottom: 1rem;
22 | }
23 |
24 | .searchitem__info-top-text {
25 | flex: 1;
26 | }
27 |
28 | .article-item {
29 | display: flex;
30 | gap: 1rem;
31 | justify-content: space-between;
32 | }
33 |
34 | .article-item__profile {
35 | display: flex;
36 | flex-direction: column;
37 | align-items: center;
38 | }
39 |
40 | .category-link {
41 | color: var(--color-text);
42 | cursor: pointer;
43 | }
44 |
45 | .category-link:hover {
46 | text-decoration: underline;
47 | }
48 |
49 | .category-link--active {
50 | color: var(--color-main);
51 | font-weight: 900;
52 | }
53 |
54 | .hidden {
55 | display: none;
56 | }
57 |
58 | @media screen and (max-width: 600px) {
59 | .search--item--info-1-top-text {
60 | display: flex;
61 | flex-direction: column;
62 | }
63 | }
64 |
65 | /* Refactors */
66 |
67 | .searchItem__top {
68 | margin-bottom: 1rem;
69 | display: flex;
70 | justify-content: space-between;
71 | align-items: center;
72 | }
73 |
74 | .searchItem__bottom {
75 | margin-bottom: 1rem;
76 | }
77 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/SidebarNav.css:
--------------------------------------------------------------------------------
1 | .sidebarNav {
2 | position: fixed;
3 | display: none;
4 | }
5 |
6 | @media screen and (min-width: 650px) {
7 | .sidebarNav {
8 | display: block;
9 | }
10 | }
11 |
12 | .sidebarNav--full {
13 | background: var(--color-light);
14 | left: 0;
15 | right: 0;
16 | top: 0;
17 | bottom: 0;
18 | z-index: 11;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | animation: move-in-left 0.3s ease-in-out alternate;
23 | }
24 |
25 | .sidebarNav__menu {
26 | list-style: none;
27 | }
28 |
29 | .sidebarNav__menuItem {
30 | display: flex;
31 | align-items: center;
32 | font-size: 1.65rem;
33 | margin-bottom: 3.5rem;
34 | }
35 |
36 | .sidebarNav__menuItem > a {
37 | color: var(--color-text);
38 | display: block;
39 | }
40 |
41 | .sidebarNav__menuItem--disabled > a {
42 | pointer-events: none;
43 | color: rgb(141, 141, 141);
44 | }
45 |
46 | .sidebarNav__menuItem > a > i.fas {
47 | display: inline-block;
48 | margin-right: 2.5rem;
49 | }
50 |
51 | .sidebarNav__menuItem > a.active {
52 | border-radius: 0.5rem;
53 | color: var(--color-main);
54 | position: relative;
55 | }
56 |
57 | .sidebarNav__menuItem > a.active::before {
58 | content: '';
59 | display: inline-block;
60 | width: 0.8rem;
61 | height: 0.8rem;
62 | border-radius: 50%;
63 | position: absolute;
64 | left: -2.5rem;
65 | top: 1rem;
66 | z-index: 111;
67 | background-color: var(--color-main);
68 | }
69 |
70 | .sidebarNav__menuItem > a.active > i.fas {
71 | color: var(--color-main) !important;
72 | }
73 |
74 | .sidebarNav__menuItem .nav-icon--unread {
75 | right: -6px;
76 | top: 10px;
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/styles/components/UserCard.css:
--------------------------------------------------------------------------------
1 | .user-card__profile-stats-wrapper {
2 | margin: 20px;
3 | display: flex;
4 | justify-content: space-evenly;
5 | flex-wrap: wrap;
6 | }
7 |
8 | .user-card__profile-name {
9 | font-size: 28px;
10 | }
11 |
12 | .user-card__profile-pic {
13 | display: block;
14 | margin: 0 auto;
15 | margin-bottom: 20px;
16 | }
17 |
18 | .user-card__profile-summary {
19 | text-align: center;
20 | }
21 |
22 | .user-card__actions {
23 | display: flex;
24 | justify-content: center;
25 | }
26 |
27 | .user-card__actions button {
28 | margin-right: 10px;
29 | }
30 |
--------------------------------------------------------------------------------
/frontend/src/styles/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Project: Mumble
3 | * File: Global StyleSheet v0.1.1
4 | */
5 |
6 | /* Fonts */
7 |
8 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400&family=Poppins:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap');
9 |
10 | /* Base Styles */
11 | @import url('./common/_variables.css');
12 | @import url('./common/_base.css');
13 | @import url('./common/_typography.css');
14 | @import url('./common/_utilities.css');
15 | @import url('./common/_animation.css');
16 | @import url('./common/_page.css');
17 | @import url('./common/_avatar.css');
18 | @import url('./common/_button.css');
19 | @import url('./common/_card.css');
20 | @import url('./common/_form.css');
21 | @import url('./common/_tag.css');
22 | @import url('./common/_tag-input.css');
23 | @import url('./common/_author-box.css');
24 | @import url('./common/_toastify.css');
25 | @import url('./common/_message.css');
26 | @import url('./common/_loading.css');
27 |
--------------------------------------------------------------------------------
/frontend/src/uikit/scripts/app.js:
--------------------------------------------------------------------------------
1 | // Invoke Functions Call on Document Loaded
2 | document.addEventListener('DOMContentLoaded', function () {
3 | // eslint-disable-next-line no-undef
4 | hljs.highlightAll();
5 | });
6 |
7 | // TODO: Fix with Highlight Js
8 | // const codeBlocks = document.querySelectorAll('.code-header + .highlighter-rouge');
9 | // const copyCodeButtons = document.querySelectorAll('.copy-code-button');
10 |
11 | // copyCodeButtons.forEach((copyCodeButton, index) => {
12 | // const code = codeBlocks[index].innerText;
13 |
14 | // copyCodeButton.addEventListener('click', () => {
15 | // window.navigator.writeText(code);
16 | // copyCodeButton.classList.add('copied');
17 |
18 | // setTimeout(() => {
19 | // copyCodeButton.classList.remove('copied');
20 | // }, 2000);
21 | // });
22 | // });
23 |
--------------------------------------------------------------------------------
/frontend/src/uikit/styles/app.css:
--------------------------------------------------------------------------------
1 | /* Import Mumble Global Styles */
2 | @import url('../../styles/index.css');
3 |
4 | /* UI Kits Specific */
5 |
6 | @import url('./modules/_codeblock.css');
7 | @import url('./modules/_page.css');
8 |
--------------------------------------------------------------------------------
/frontend/src/uikit/styles/modules/_codeblock.css:
--------------------------------------------------------------------------------
1 | .codeblock code {
2 | border-radius: 0.5rem;
3 | height: auto;
4 | }
5 |
6 | .codeblock--sm {
7 | height: 24rem;
8 | }
9 |
10 | .codeblock--md {
11 | height: 32rem;
12 | }
13 |
14 | .codeblock--lg {
15 | height: 44rem;
16 | }
17 |
18 | .codeblock--xl {
19 | height: 64rem;
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/uikit/styles/modules/_page.css:
--------------------------------------------------------------------------------
1 | .mumble-block {
2 | padding: 4.2rem 0;
3 | }
4 |
5 | .mumble-block__title {
6 | color: var(--color-main);
7 | font-size: 3rem;
8 | font-weight: 700;
9 | }
10 |
11 | .mumble-block__info {
12 | padding-bottom: 1rem;
13 | border-bottom: 2px solid var(--color-main);
14 | }
15 |
16 | .mumble-block__preview {
17 | padding: 3rem 0;
18 | }
19 |
20 | .mumble-block--header {
21 | background-color: var(--color-light);
22 | }
23 |
24 | .mumble-block--typography .mumble-block__preview > * {
25 | line-height: 2;
26 | }
27 |
28 | .mumble-block--card .mumble-block__preview {
29 | display: flex;
30 | }
31 |
32 | .mumble-block--card .mumble-block__preview > * {
33 | margin-right: 2rem;
34 | }
35 |
36 | .mumble-block--avatar .mumble-block__preview > * {
37 | margin-right: 1rem;
38 | }
39 |
--------------------------------------------------------------------------------
/frontend/src/utilities/PrivateRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from 'react-router-dom';
3 | import { useSelector } from 'react-redux';
4 | import { Loading } from '../common';
5 |
6 | const PrivateRoute = ({ component: Component, ...rest }) => {
7 | const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
8 | const isLoading = useSelector((state) => state.auth.isLoading);
9 | return (
10 |
13 | isLoading ? (
14 |
15 | ) : !isAuthenticated ? (
16 |
17 | ) : (
18 |
19 | )
20 | }
21 | />
22 | );
23 | };
24 |
25 | export default PrivateRoute;
26 |
--------------------------------------------------------------------------------
/frontend/src/utilities/RouteHandler.js:
--------------------------------------------------------------------------------
1 | import { useHistory } from 'react-router';
2 | import { useLocationBlocker } from '../hooks';
3 |
4 | const RouteHandler = ({ children }) => {
5 | const history = useHistory();
6 | useLocationBlocker(history);
7 | return <>{children}>;
8 | };
9 |
10 | export default RouteHandler;
11 |
--------------------------------------------------------------------------------
/frontend/src/utilities/formatDate.js:
--------------------------------------------------------------------------------
1 | import { formatDistanceToNow } from 'date-fns';
2 |
3 | export const sqlDateToJsDate = (date) => new Date(date.replace(' ', ''));
4 |
5 | export const distanceDate = (date) =>
6 | formatDistanceToNow(sqlDateToJsDate(date), { addSuffix: true });
7 |
8 | const formatDate = { sqlDateToJsDate, distanceDate };
9 | export default formatDate;
10 |
--------------------------------------------------------------------------------
/frontend/src/utilities/getImageUrl.js:
--------------------------------------------------------------------------------
1 | import { getApiUrl } from '../services/config';
2 |
3 | export const getImageUrl = (imageUrl) => {
4 | return imageUrl.startsWith('/images') ? getApiUrl(`static/${imageUrl}`) : imageUrl;
5 | };
6 |
--------------------------------------------------------------------------------
/frontend/src/utilities/index.js:
--------------------------------------------------------------------------------
1 | export { default as formatDate } from './formatDate';
2 | export { default as PrivateRoute } from './PrivateRoute';
3 | export { default as RouteHandler } from './RouteHandler';
4 |
--------------------------------------------------------------------------------
/images/activate-project.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/activate-project.gif
--------------------------------------------------------------------------------
/images/dark-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/dark-logo.png
--------------------------------------------------------------------------------
/images/discussion-page-lightmode.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/discussion-page-lightmode.PNG
--------------------------------------------------------------------------------
/images/login-page-darkmode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/login-page-darkmode.png
--------------------------------------------------------------------------------
/images/login-page-lightmode.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/login-page-lightmode.PNG
--------------------------------------------------------------------------------
/images/mumble-ui-kit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/mumble-ui-kit.png
--------------------------------------------------------------------------------
/images/profile-page-lightmode.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/profile-page-lightmode.PNG
--------------------------------------------------------------------------------
/images/project-board.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/project-board.gif
--------------------------------------------------------------------------------
/images/projects-icon.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/projects-icon.PNG
--------------------------------------------------------------------------------
/images/user-feed-darkmode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/user-feed-darkmode.png
--------------------------------------------------------------------------------
/images/user-feed-lightmode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/divanov11/Mumble/f96a5852da73dd13e6ff8babfbd0710568ed81c2/images/user-feed-lightmode.png
--------------------------------------------------------------------------------