├── .DS_Store
├── .dockerignore
├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .obsidian
├── app.json
├── appearance.json
├── core-plugins.json
├── graph.json
├── hotkeys.json
└── workspace
├── Docker-compose.yml
├── LICENSE
├── README.md
├── frontend
├── .dockerignore
├── .gitignore
├── Dockerfile
├── README.md
├── babel.config.js
├── facebook.txt
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── auth-background.jpg
│ │ ├── backend.jpeg
│ │ ├── commnities-background.jpeg
│ │ ├── communities.jpeg
│ │ ├── database.png
│ │ ├── frontend.jpeg
│ │ ├── logo.png
│ │ ├── logo.svg
│ │ ├── notehub.jpg
│ │ ├── profile-background.png
│ │ ├── search-background.png
│ │ └── stat.png
│ ├── components
│ │ ├── CommentCard.vue
│ │ ├── CommunitiesCard.vue
│ │ ├── CommunityBar.vue
│ │ ├── CommunityCard.vue
│ │ ├── CommunityGroup.vue
│ │ ├── CommunityNote.vue
│ │ ├── CommunityNoteList.vue
│ │ ├── CreateCommunityDialog.vue
│ │ ├── CreateNotebookDialog.vue
│ │ ├── ImageUpload.vue
│ │ ├── InvitationDialog.vue
│ │ ├── MemberList.vue
│ │ ├── NavigationBar.vue
│ │ ├── NoteAccessList.vue
│ │ ├── NoteActionMenu.vue
│ │ ├── NoteTable.vue
│ │ ├── NotebookGrid.vue
│ │ ├── NotexCard.vue
│ │ ├── UserAvatar.vue
│ │ ├── UserSearchField.vue
│ │ ├── WorkspaceBar.vue
│ │ ├── WorkspaceChatList.vue
│ │ ├── WorkspaceCommentItem.vue
│ │ ├── WorkspaceCommentList.vue
│ │ ├── WorkspaceHeadline.vue
│ │ ├── WorkspaceInviteList.vue
│ │ ├── WorkspaceNoteItem.vue
│ │ ├── WorkspaceNoteList.vue
│ │ └── WorkspaceUserGroup.vue
│ ├── includes
│ │ ├── fake_data.js
│ │ ├── firebase.js
│ │ ├── http.js
│ │ ├── model.ts
│ │ ├── progressbar.js
│ │ └── utils.js
│ ├── main.js
│ ├── notex-editor
│ │ ├── NotexEditor.vue
│ │ ├── components
│ │ │ ├── BlockQuoteWrapper.vue
│ │ │ ├── BulletListWrapper.vue
│ │ │ ├── CodeBlockWrapper.vue
│ │ │ ├── HeadingWrapper.vue
│ │ │ └── OrderedListWrapper.vue
│ │ ├── extensions
│ │ │ ├── abbreviation.js
│ │ │ ├── default-nodes.js
│ │ │ ├── rainbow.js
│ │ │ └── trailing-node.js
│ │ └── nodes
│ │ │ └── draggable-block.js
│ ├── plugins
│ │ └── vuetify.js
│ ├── router
│ │ └── index.js
│ ├── scss
│ │ └── variables.scss
│ ├── store
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── auth.js
│ │ │ ├── chart.js
│ │ │ ├── communities.js
│ │ │ ├── community-notes.js
│ │ │ ├── community.js
│ │ │ ├── index.js
│ │ │ ├── note.js
│ │ │ ├── ranking.js
│ │ │ └── workspace.js
│ └── views
│ │ ├── Authentication.vue
│ │ ├── Communities.vue
│ │ ├── Community.vue
│ │ ├── Dashboard.vue
│ │ ├── LandingPage.vue
│ │ ├── NotFound.vue
│ │ ├── Ranking.vue
│ │ ├── UserProfile.vue
│ │ └── Workspace.vue
├── tests
│ └── unit
│ │ └── example.spec.js
└── vue.config.js
├── nginx
└── nginx-setup.conf
├── package.json
└── server
├── .dockerignore
├── .eslintrc.js
├── .gitignore
├── API-Book.md
├── Dockerfile
├── README.md
├── new_database_pure.sql
├── package-lock.json
├── package.json
├── public
├── css
│ ├── app.eb20d4cc.css
│ └── chunk-vendors.08f1971d.css
├── favicon.ico
├── img
│ ├── auth-background.9c92a012.jpg
│ └── search-background.3114fa6b.png
├── index.html
└── js
│ ├── app.2f0515fc.js
│ ├── app.2f0515fc.js.map
│ ├── chunk-vendors.d1d961fb.js
│ └── chunk-vendors.d1d961fb.js.map
├── src
├── app.js
├── hocuspocus-hooks
│ ├── hooks.js
│ └── startTemplate.json
├── models
│ ├── community.sql.js
│ ├── database.js
│ ├── membership.sql.js
│ ├── mongo.utils.js
│ ├── note.repository.js
│ ├── note.sql.js
│ └── user.sql.js
├── routes
│ ├── api.router.js
│ ├── community
│ │ ├── community.controller.js
│ │ └── community.router.js
│ ├── firebase
│ │ ├── .gitignore
│ │ ├── firebase.initialize.js
│ │ ├── firebase.middleware.js
│ │ └── firebase.utils.js
│ ├── note
│ │ ├── note.controller.js
│ │ └── note.router.js
│ └── user
│ │ ├── user.controller.js
│ │ └── user.router.js
└── server.js
└── test.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/.DS_Store
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | Docker-compose.yml
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: NoteHub CD
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | # Allows you to run this workflow manually from the Actions tab
8 | # workflow_dispatch:
9 |
10 | jobs:
11 | # test:
12 |
13 | # runs-on: ubuntu-latest
14 |
15 | # strategy:
16 | # matrix:
17 | # node-version: [14.x]
18 |
19 | # steps:
20 | # - uses: actions/checkout@v2
21 | # - name: Use Node.js ${{ matrix.node-version }}
22 | # uses: actions/setup-node@v1
23 | # with:
24 | # node-version: ${{ matrix.node-version }}
25 | # - name: npm install and test
26 | # run: |
27 | # npm install
28 | # npm test
29 | # env:
30 | # CI: true
31 |
32 | deploy:
33 | #needs: [test]
34 | runs-on: ubuntu-latest
35 |
36 | steps:
37 | - name: executing remote ssh commands using password
38 | uses: appleboy/ssh-action@master
39 | with:
40 | host: ${{ secrets.HOST }}
41 | username: ${{ secrets.USERNAME }}
42 | password: ${{ secrets.PASSWORD }}
43 | port: ${{ secrets.PORT }}
44 | key: ${{ secrets.KEY }}
45 | passphrase: ${{ secrets.PASSPHRASE }}
46 |
47 | script: |
48 | source /home/mynotehub/nodevenv/notehub/14/bin/activate && cd /home/mynotehub/notehub
49 | git stash
50 | git pull origin main
51 | cd ./frontend
52 | npm install --production ./
53 | npm run build
54 | cd ../server
55 | npm install --production ./
56 | touch ../tmp/restart.txt
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 | .env.production
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 | .parcel-cache
80 |
81 | # Next.js build output
82 | .next
83 | out
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and not Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Stores VSCode versions used for testing VSCode extensions
111 | .vscode-test
112 |
113 | # yarn v2
114 | .yarn/cache
115 | .yarn/unplugged
116 | .yarn/build-state.yml
117 | .yarn/install-state.gz
118 | .pnp.*
119 | server/src/routes/firebase/serviceAccountKey.json
120 |
--------------------------------------------------------------------------------
/.obsidian/app.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.obsidian/appearance.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseFontSize": 16
3 | }
--------------------------------------------------------------------------------
/.obsidian/core-plugins.json:
--------------------------------------------------------------------------------
1 | [
2 | "file-explorer",
3 | "global-search",
4 | "switcher",
5 | "graph",
6 | "backlink",
7 | "page-preview",
8 | "note-composer",
9 | "command-palette",
10 | "markdown-importer",
11 | "word-count",
12 | "open-with-default-app",
13 | "file-recovery"
14 | ]
--------------------------------------------------------------------------------
/.obsidian/graph.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapse-filter": true,
3 | "search": "",
4 | "showTags": false,
5 | "showAttachments": false,
6 | "hideUnresolved": false,
7 | "showOrphans": true,
8 | "collapse-color-groups": true,
9 | "colorGroups": [],
10 | "collapse-display": true,
11 | "showArrow": false,
12 | "textFadeMultiplier": 0,
13 | "nodeSizeMultiplier": 1,
14 | "lineSizeMultiplier": 1,
15 | "collapse-forces": true,
16 | "centerStrength": 0.518713248970312,
17 | "repelStrength": 10,
18 | "linkStrength": 1,
19 | "linkDistance": 250,
20 | "scale": 0.09126686187766794,
21 | "close": false
22 | }
--------------------------------------------------------------------------------
/.obsidian/hotkeys.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.obsidian/workspace:
--------------------------------------------------------------------------------
1 | {
2 | "main": {
3 | "id": "16f4e02fe85c779a",
4 | "type": "split",
5 | "children": [
6 | {
7 | "id": "838c3a850209c000",
8 | "type": "leaf",
9 | "state": {
10 | "type": "markdown",
11 | "state": {
12 | "file": "frontend/node_modules/axios/README.md",
13 | "mode": "source"
14 | }
15 | }
16 | }
17 | ],
18 | "direction": "vertical"
19 | },
20 | "left": {
21 | "id": "d6cde3ffebb6ddd5",
22 | "type": "split",
23 | "children": [
24 | {
25 | "id": "0e41c5f09622434f",
26 | "type": "tabs",
27 | "dimension": 74.35897435897436,
28 | "children": [
29 | {
30 | "id": "bc83c58138ab79a0",
31 | "type": "leaf",
32 | "state": {
33 | "type": "empty",
34 | "state": {}
35 | }
36 | },
37 | {
38 | "id": "0a76a73dd022fe54",
39 | "type": "leaf",
40 | "state": {
41 | "type": "graph",
42 | "state": {}
43 | }
44 | }
45 | ]
46 | },
47 | {
48 | "id": "f603429a8a2738ec",
49 | "type": "tabs",
50 | "dimension": 25.64102564102564,
51 | "children": [
52 | {
53 | "id": "5bf6fd9a1e3e1bc1",
54 | "type": "leaf",
55 | "state": {
56 | "type": "file-explorer",
57 | "state": {}
58 | }
59 | },
60 | {
61 | "id": "0d20ac51335547d9",
62 | "type": "leaf",
63 | "state": {
64 | "type": "search",
65 | "state": {
66 | "query": "",
67 | "matchingCase": false,
68 | "explainSearch": false,
69 | "collapseAll": false,
70 | "extraContext": false,
71 | "sortOrder": "alphabetical"
72 | }
73 | }
74 | }
75 | ],
76 | "currentTab": 1
77 | }
78 | ],
79 | "direction": "horizontal",
80 | "width": 537
81 | },
82 | "right": {
83 | "id": "46d8fef5f87f96dc",
84 | "type": "split",
85 | "children": [
86 | {
87 | "id": "d2d40f0d20322de2",
88 | "type": "tabs",
89 | "children": [
90 | {
91 | "id": "c4eb35e1f56c9936",
92 | "type": "leaf",
93 | "state": {
94 | "type": "backlink",
95 | "state": {
96 | "file": "frontend/node_modules/axios/README.md",
97 | "collapseAll": false,
98 | "extraContext": false,
99 | "sortOrder": "alphabetical",
100 | "showSearch": false,
101 | "searchQuery": "",
102 | "backlinkCollapsed": false,
103 | "unlinkedCollapsed": true
104 | }
105 | }
106 | }
107 | ]
108 | }
109 | ],
110 | "direction": "horizontal",
111 | "width": 300,
112 | "collapsed": true
113 | },
114 | "active": "838c3a850209c000",
115 | "lastOpenFiles": [
116 | "frontend/node_modules/axios/README.md"
117 | ]
118 | }
--------------------------------------------------------------------------------
/Docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 |
4 | frontend:
5 | build:
6 | context: ./frontend
7 | volumes:
8 | - vue_build:/server/public
9 | ports:
10 | - "8080:8080"
11 | container_name: frontend_container
12 |
13 | backend:
14 | build:
15 | context: ./server
16 | volumes:
17 | - vue_build:/server/public
18 | ports:
19 | - "8000:8000"
20 | container_name: backend_container
21 |
22 | nginx:
23 | image: nginx:latest
24 | ports:
25 | - "7070:7070"
26 | volumes:
27 | - ./nginx/nginx-setup.conf:/etc/nginx/conf.d/default.conf:ro
28 | - vue_build:/var/www/vue
29 | depends_on:
30 | - backend
31 | - frontend
32 |
33 | volumes:
34 | vue_build:
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Toubat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NoteHub
2 |
3 | NoteHub is an online note sharing platform where users can edit notes with a versatile rich-text editor in a real-time collaborative environment. NoteHub also provides notes sharing features between individuals or communities, and even more advanced features such as AI assistance, including content summarization, Q&A, voice to text transcription, and handwritten text recognition.
4 |
5 | ## Currently developing at:
6 | [NoteHub Development Website](https://mynotehub.web.illinois.edu/)
7 |
8 | ## Setup
9 |
10 | Run `npm install` under frontend and backend folder respectively.
11 |
12 | ### Compiles and hot-reloads for development
13 |
14 | ```
15 | npm run watch
16 | ```
17 |
18 | Run this command under the NOTEHUB directory will start both Vue cli and Node server in hot-reload (watch) mode
19 |
20 | ### Compiles and minifies for production
21 |
22 | ```
23 | npm run deploy
24 | ```
25 |
26 | Run this command under the NOTEHUB directory will build the frontend in server/public folder and start the node server.
27 | Production site is at backend port: http://localhost:8000/
28 |
29 | ## Test
30 |
31 | ```
32 | npm run test
33 | ```
34 |
35 | Run this command under the NOTEHUB directory will start both test for backend and frontend.
36 |
37 | ## Project Description
38 |
39 | Nowadays, as online education and remote learning become a new norm of life, more and more students choose to take notes on their electronic devices such as laptops and iPads. Although some of these tools provide a beautiful UI and electronic way of taking notes, there is no single platform that can fulfill all needs of students such as real-time collaboration with MarkDown support and executable code blocks in one place. To break the routines, NoteHub aims to bring a new era for taking notes by providing all functionalities for the best note taking experience that a student can imagine on one platform.
40 | NoteHub utilizes the newest technology in the industry to provide users the most innovative experience of taking notes. More specifically, we offer three major functionalities that can be the game changer for students to take notes more efficiently and pleasantly. First, enlightened by Markdown markup language, we design and implement a multifunctional rich-text editor, namely NoteX, which inherits the essence of Markdown but extends much more features for the finest experience. For example, our NoteHub is capable of handling multiple types of note elements such as executable code blocks, video embedding, math equations with Latex, drawable whiteboards, etc..
41 |
42 | Second, with all these fantastic features provided By NoteX, the editor of NoteHub platform, each NoteX (each instance of the note) supports real-time collaboration. This means that users are free to invite their friends, teammates, or even faculty members together, maximizing teamwork productivity and creativity. Although there is already some software that supports such functionalities such as Google doc, it still lacks the support of Markdown, and executable code blocks, and other essential functionalities that make NoteHub stand out. As we specifically design our app to suit the needs of students, users do not need to constantly change their window between different apps but solely focus on taking beautiful notes and working in a team.
43 | Third, besides the NoteX editor and the real-time collaboration, NoteHub is a place to create and join different communities, contributing notes with people who share the same interests. As the name NoteHub suggests, we aspire to create a note sharing platform that connects people and enhances their learning productivity. At NoteHub, each user is allowed to upload their works from their personal workspace to a community such as a specific course community. Through this community, users can comment on others’ notes and even contribute to these notes by asking for access permission with the note owners. Moreover, we plan to make the community more vibrant by providing paid or subscription functions. For example, a user can contribute his/her notes to a community with a paid access if the note owner thinks other members would like to pay for a cup of coffee.
44 |
45 | In conclusion, tired of the daily frustration and predicament of not having a productive note-taking application as a college student, we are determined to make NoteHub the ultimate solution for students to take the most effective and elegant notes all in one place.
46 |
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | /node_modules
3 | README.md
4 | Dockerfile
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 |
7 | # local env files
8 | .env
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 |
18 | # Editor directories and files
19 | .idea
20 | .vscode
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine
2 | WORKDIR /frontend
3 | COPY package*.json ./
4 | RUN npm install
5 | COPY . .
6 | RUN npm run build
7 | CMD [ "npm", "run","serve" ]
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # frontend
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Run your unit tests
19 | ```
20 | npm run test:unit
21 | ```
22 |
23 | ### Lints and fixes files
24 | ```
25 | npm run lint
26 | ```
27 |
28 | ### Customize configuration
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
--------------------------------------------------------------------------------
/frontend/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build --dest ../server/public",
8 | "test:unit": "vue-cli-service test:unit",
9 | "lint": "vue-cli-service lint"
10 | },
11 | "dependencies": {
12 | "@hocuspocus/provider": "^1.0.0-alpha.16",
13 | "@tiptap/extension-bullet-list": "^2.0.0-beta.23",
14 | "@tiptap/extension-code-block-lowlight": "^2.0.0-beta.57",
15 | "@tiptap/extension-collaboration": "^2.0.0-beta.20",
16 | "@tiptap/extension-collaboration-cursor": "^2.0.0-beta.27",
17 | "@tiptap/extension-dropcursor": "^2.0.0-beta.19",
18 | "@tiptap/extension-focus": "^2.0.0-beta.37",
19 | "@tiptap/extension-highlight": "^2.0.0-beta.30",
20 | "@tiptap/extension-image": "^2.0.0-beta.15",
21 | "@tiptap/extension-text-align": "^2.0.0-beta.28",
22 | "@tiptap/extension-text-style": "^2.0.0-beta.22",
23 | "@tiptap/extension-typography": "^2.0.0-beta.19",
24 | "@tiptap/starter-kit": "^2.0.0-beta.110",
25 | "@tiptap/vue-2": "^2.0.0-beta.52",
26 | "apexcharts": "^3.31.0",
27 | "axios": "^0.21.4",
28 | "core-js": "^3.6.5",
29 | "dotenv": "^10.0.0",
30 | "firebase": "^9.0.2",
31 | "js-cookie": "^3.0.1",
32 | "lodash": "^4.17.21",
33 | "lowlight": "^2.4.1",
34 | "mathlive": "^0.69.7",
35 | "nprogress": "^0.2.0",
36 | "simple-code-editor": "^1.0.5",
37 | "tiptap": "^1.27.1",
38 | "tiptap-extensions": "^1.29.1",
39 | "vue": "^2.6.11",
40 | "vue-apexcharts": "^1.6.2",
41 | "vue-nprogress": "^0.2.0",
42 | "vue-router": "^3.2.0",
43 | "vuefire": "^2.2.5",
44 | "vuetify": "^2.4.0",
45 | "vuex": "^3.4.0",
46 | "y-websocket": "^1.3.17",
47 | "yjs": "^13.5.18"
48 | },
49 | "devDependencies": {
50 | "@vue/cli-plugin-babel": "~4.5.0",
51 | "@vue/cli-plugin-eslint": "~4.5.0",
52 | "@vue/cli-plugin-router": "~4.5.0",
53 | "@vue/cli-plugin-unit-jest": "~4.5.0",
54 | "@vue/cli-plugin-vuex": "~4.5.0",
55 | "@vue/cli-service": "~4.5.0",
56 | "@vue/test-utils": "^1.0.3",
57 | "babel-eslint": "^10.1.0",
58 | "eslint": "^6.7.2",
59 | "eslint-plugin-vue": "^6.2.2",
60 | "sass": "~1.32.13",
61 | "sass-loader": "^10.0.0",
62 | "vue-cli-plugin-vuetify": "^2.4.2",
63 | "vue-template-compiler": "^2.6.11",
64 | "vuetify-loader": "^1.7.0"
65 | },
66 | "eslintConfig": {
67 | "root": true,
68 | "env": {
69 | "node": true
70 | },
71 | "extends": [
72 | "plugin:vue/essential",
73 | "eslint:recommended"
74 | ],
75 | "parserOptions": {
76 | "parser": "babel-eslint"
77 | },
78 | "rules": {},
79 | "overrides": [
80 | {
81 | "files": [
82 | "**/__tests__/*.{j,t}s?(x)",
83 | "**/tests/unit/**/*.spec.{j,t}s?(x)"
84 | ],
85 | "env": {
86 | "jest": true
87 | }
88 | }
89 | ]
90 | },
91 | "browserslist": [
92 | "> 1%",
93 | "last 2 versions",
94 | "not dead"
95 | ],
96 | "jest": {
97 | "preset": "@vue/cli-plugin-unit-jest"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
13 |
14 |
15 |
19 |
20 |
21 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ snackbarMessage }}
11 |
12 |
13 | Close
14 |
15 |
16 |
17 |
18 |
19 |
20 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/frontend/src/assets/auth-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/auth-background.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/backend.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/backend.jpeg
--------------------------------------------------------------------------------
/frontend/src/assets/commnities-background.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/commnities-background.jpeg
--------------------------------------------------------------------------------
/frontend/src/assets/communities.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/communities.jpeg
--------------------------------------------------------------------------------
/frontend/src/assets/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/database.png
--------------------------------------------------------------------------------
/frontend/src/assets/frontend.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/frontend.jpeg
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/assets/notehub.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/notehub.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/profile-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/profile-background.png
--------------------------------------------------------------------------------
/frontend/src/assets/search-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/search-background.png
--------------------------------------------------------------------------------
/frontend/src/assets/stat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/assets/stat.png
--------------------------------------------------------------------------------
/frontend/src/components/CommentCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 | {{
13 | `${comment.user.firstName} ${comment.user.lastName}`
14 | }}
15 |
16 |
17 |
22 |
23 |
24 | favorite
25 |
26 | {{ comment.likeCount || 0 }}
27 |
28 |
29 |
30 |
31 |
49 |
50 |
59 |
--------------------------------------------------------------------------------
/frontend/src/components/CommunitiesCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
14 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 | {{info.description}}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
74 |
75 |
--------------------------------------------------------------------------------
/frontend/src/components/CommunityCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | more_horiz
16 |
17 |
18 |
19 |
20 |
21 |
22 |
29 | exit_to_app
30 | {{ action.title }}
31 |
32 |
33 |
34 |
35 | Are you sure to leave this community?
36 |
37 | This may cause irriversable data loss.
38 |
39 |
40 |
41 | Leave
42 |
43 | Cancel
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 | {{ community.name }}
59 |
60 |
61 |
62 |
63 |
64 | Creator: {{ getFullName(community.owner) }}
65 |
66 |
67 |
68 | groups
69 |
70 | Members: {{ community.memberCount }}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
126 |
127 |
141 |
--------------------------------------------------------------------------------
/frontend/src/components/CommunityGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
17 |
18 |
25 | add
26 | Join New
27 |
28 |
29 |
30 |
41 |
61 |
89 |
90 |
91 |
92 |
143 |
144 |
161 |
--------------------------------------------------------------------------------
/frontend/src/components/CommunityNoteList.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateCommunityDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ loading ? "Creating..." : "Create a New Community" }}
6 |
7 |
8 |
9 |
10 |
23 |
24 |
37 |
43 |
44 |
45 |
46 |
47 | Create
48 | Cancel
49 |
50 |
51 |
52 |
53 |
54 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/frontend/src/components/CreateNotebookDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ loading ? "Creating..." : " Create a New Notebook" }}
6 |
7 |
8 |
9 |
10 |
21 |
22 |
34 |
35 |
44 | {{ data.item }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Create
53 | Cancel
54 |
55 |
56 |
57 |
58 |
59 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/frontend/src/components/ImageUpload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
20 |
mdi-cloud-upload
21 |
22 | Drop your file here, or click to select them.
23 |
24 |
25 | {{ message }}
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
50 | delete
51 |
52 |
53 | Delete Image
54 |
55 |
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
143 |
144 |
154 |
--------------------------------------------------------------------------------
/frontend/src/components/InvitationDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
--------------------------------------------------------------------------------
/frontend/src/components/MemberList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ role.icon }}
6 | {{ role.role }}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ `${user.firstName} ${user.lastName}` }}
14 | {{ user.subtitle }}
15 |
16 |
17 |
18 | expand_more
19 |
20 |
21 |
22 |
23 | User Information
25 |
26 |
27 |
28 | email
29 | {{ user.email }}
30 |
31 |
32 | work
33 | {{ user.subtitle }}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteAccessList.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 | {{ user.firstName }} {{ user.lastName }}
20 |
21 |
22 | {{ user.email }}
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ user.accessStatus }}
30 | arrow_drop_down
31 |
32 |
33 |
34 |
43 | {{ action.icon }}
44 | {{ action.label }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
116 |
117 |
122 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
22 | {{ unixTimeToDate(item.createdAt) }}
23 |
24 |
25 |
26 |
27 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/frontend/src/components/NotebookGrid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | share
26 | {{ sharedIdx !== null ? shareMessage : "All Documents" }}
27 | arrow_drop_down
28 |
29 |
30 |
31 |
32 | apps
33 | All Documents
34 |
35 |
36 |
41 | account_circle
42 | {{ getFullName(user) }}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
61 |
62 |
63 |
64 | {{ item }}
65 |
66 |
67 | (+{{ selectedCategories.length - 1 }} others)
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
88 |
93 |
94 |
95 |
96 |
97 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
173 |
174 |
190 |
--------------------------------------------------------------------------------
/frontend/src/components/NotexCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
24 | {{ note.noteTitle }}
25 |
26 |
27 | {{ note.noteTitle }}
28 |
29 |
30 |
37 |
38 |
39 | more_vert
40 |
41 |
42 |
43 |
44 |
45 | Owner: {{ `${note.owner.firstName} ${note.owner.lastName}` }}
50 |
51 |
52 |
53 |
54 |
55 |
56 | Created: {{ timeToDate(note.createdAt) }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | favorite_border
65 | {{ note.likeCount }}
66 |
67 |
68 | remove_red_eye
69 | {{ note.viewCount }}
70 |
71 |
72 | notes
73 | {{ note.commentCount }}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
116 |
117 |
127 |
--------------------------------------------------------------------------------
/frontend/src/components/UserAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ initial }}
8 |
9 |
10 |
11 |
45 |
--------------------------------------------------------------------------------
/frontend/src/components/UserSearchField.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
25 |
26 |
27 |
28 | {{ data.item.name }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ data.item.name }}
38 | {{ data.item.email }}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceChatList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Chat
7 |
8 |
9 |
10 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceCommentList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | first_page
9 |
10 | Reply to {{ repliedComment.user.firstName + " " + repliedComment.user.lastName }}
11 |
12 |
13 |
14 |
15 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
42 |
50 | send
51 | Send Message
52 |
53 |
54 |
55 |
56 |
57 |
58 |
135 |
136 |
158 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceHeadline.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ icon }}
5 |
{{ title }}
6 |
7 |
8 |
9 |
10 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceInviteList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mdi-account-plus
7 | Invite New Friend
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
77 |
78 |
87 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceNoteItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
15 | mdi-file-document-outline
16 |
17 |
18 |
22 | {{ note.noteTitle }}
23 |
24 |
25 | {{ unixTimeToDate(note.createdAt) }}
26 |
27 |
31 | Owned by: {{ note.owner.firstName + " " + note.owner.lastName }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceNoteList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 | filter_list
19 |
20 | Advanced Filtering
21 |
22 |
23 |
24 |
25 |
26 | add_circle_outline
27 | New Notebook
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
68 |
69 |
77 |
--------------------------------------------------------------------------------
/frontend/src/components/WorkspaceUserGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ title }}
6 |
7 |
8 |
9 | {{icon}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
28 |
29 |
30 | {{user.firstName + " " + user.lastName}}
31 |
32 |
33 |
34 | mdi-pencil
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
81 |
82 |
--------------------------------------------------------------------------------
/frontend/src/includes/firebase.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app";
2 | import { getAuth } from "firebase/auth";
3 | import { getFirestore } from "firebase/firestore";
4 | import { getStorage, ref } from "firebase/storage";
5 |
6 | const firebaseConfig = {
7 | apiKey: "AIzaSyCE8H066cgN969UuRrI02FxMYlwCK1TWQg",
8 | authDomain: "notehub-326303.firebaseapp.com",
9 | projectId: "notehub-326303",
10 | storageBucket: "notehub-326303.appspot.com",
11 | messagingSenderId: "121246877159",
12 | appId: "1:121246877159:web:70bf417836e3c18f2f2942",
13 | };
14 |
15 | // Initialize Firebase
16 | const app = initializeApp(firebaseConfig);
17 | const auth = getAuth();
18 | const db = getFirestore();
19 | const storage = getStorage(app);
20 |
21 | export { app, auth, db, storage, ref };
22 |
--------------------------------------------------------------------------------
/frontend/src/includes/http.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import Cookies from "js-cookie";
3 | import dotenv from "dotenv";
4 | dotenv.config();
5 |
6 | const http = axios.create({
7 | baseURL: process.env.VUE_APP_ROOT_URL,
8 | headers: {
9 | "Content-type": "application/json",
10 | "CSRF-Token": Cookies.get("XSRF-TOKEN"),
11 | },
12 | });
13 |
14 | export default http;
15 |
--------------------------------------------------------------------------------
/frontend/src/includes/model.ts:
--------------------------------------------------------------------------------
1 | interface User {
2 | userId: number;
3 | firstName: string;
4 | lastName: string;
5 | subtitle: string;
6 | email: string;
7 | avatarUrl: string;
8 | }
9 |
10 | interface Note {
11 | noteId: number;
12 | dataId: string;
13 | noteTitle: string;
14 | createdAt: Date;
15 | likeCount: number;
16 | viewCount: number;
17 | commentCount: number;
18 | accessStatus: string; // NoteAccess
19 | ownerId: number; // User
20 | ownerName: string; // full name (firstName + ' ' + lastName)
21 | owner: User;
22 | sharedUsers: Array; // add this field if accessStatus == "owner"; otherwise, leave it as []
23 | categories: Array;
24 | }
25 |
26 | interface Community {
27 | communityId: number;
28 | name: string;
29 | description: string;
30 | createdAt: Date;
31 | photo: string;
32 | memberCount: number;
33 | owner: User;
34 | }
35 |
36 | interface Category {
37 | categoryName: string;
38 | }
39 |
40 | interface CommunityRole {
41 | role: string; // Owner, Manager, Member
42 | icon: string; // icon name
43 | users: Array;
44 | }
45 |
46 | interface Comment {
47 | commentId: number;
48 | noteId: number;
49 | user: User;
50 | content: string;
51 | parendId: number;
52 | createdAt: number;
53 | likeCount: number;
54 | replies?: Array; // do not add this field for now
55 | }
56 |
57 | // const members = Array;
58 | interface CommunityNote {
59 | noteId: number;
60 | dataId: string;
61 | noteTitle: string;
62 | createdAt: Date;
63 | likeCount: number;
64 | viewCount: number;
65 | commentCount: number;
66 | owner: User;
67 | comments: Array;
68 | }
69 |
70 | interface SearchUser {
71 | userId: number;
72 | name: string; // concat(firstName + ' ' + lastName)
73 | subtitle: string;
74 | email: string;
75 | avatarUrl: string;
76 | searchFields: Array; // [firstName, lastName, email]
77 | }
78 |
--------------------------------------------------------------------------------
/frontend/src/includes/progressbar.js:
--------------------------------------------------------------------------------
1 | import NProgress from "nprogress";
2 |
3 | export default (router) => {
4 | /* eslint-disable */
5 | router.beforeEach((to, from, next) => {
6 | NProgress.start();
7 | next();
8 | });
9 | router.afterEach(NProgress.done);
10 | };
11 |
--------------------------------------------------------------------------------
/frontend/src/includes/utils.js:
--------------------------------------------------------------------------------
1 | export function unixTimeToDate(time) {
2 | const publishTime = new Date(parseInt(time));
3 | const currTime = new Date(Math.round(Date.now()));
4 | const diff = currTime.getTime() - publishTime.getTime();
5 | const diffDays = Math.floor(diff / (1000 * 60 * 60 * 24));
6 | const diffHours = Math.floor(diff / (1000 * 60 * 60));
7 | const diffMinutes = Math.floor(diff / (1000 * 60));
8 | const diffSeconds = Math.floor(diff / 1000);
9 | if (diffDays > 0) {
10 | return `${diffDays} days`;
11 | }
12 | if (diffHours > 0) {
13 | return `${diffHours} hours`;
14 | }
15 | if (diffMinutes > 0) {
16 | return `${diffMinutes} minutes`;
17 | }
18 | if (diffSeconds > 0) {
19 | return `${diffSeconds} seconds`;
20 | }
21 | return "just now";
22 | }
23 |
24 | export function getRandomColor() {
25 | const colors = [
26 | "#feca57",
27 | "#fa983a",
28 | "#38ada9",
29 | "#2e86de",
30 | "#ee5253",
31 | "#2ed573",
32 | "#FDA7DF",
33 | "#26c6da",
34 | ];
35 | // choose a random color from the array
36 | return colors[Math.floor(Math.random() * colors.length)];
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./App.vue";
3 | import router from "./router";
4 | import store from "./store";
5 | import vuetify from "./plugins/vuetify";
6 | import axios from "axios";
7 | import { auth } from "./includes/firebase.js";
8 | import ProgressBar from "./includes/progressbar.js";
9 | import "nprogress/nprogress.css";
10 |
11 | Vue.prototype.$axios = axios;
12 | Vue.config.productionTip = false;
13 |
14 | ProgressBar(router);
15 |
16 | let app;
17 |
18 | auth.onAuthStateChanged(async () => {
19 | await store.dispatch("initialLogin"); // important to be sync!
20 | if (!app) {
21 | app = new Vue({
22 | router,
23 | store,
24 | vuetify,
25 | render: (h) => h(App),
26 | }).$mount("#app");
27 | }
28 | });
29 |
30 | /* MathLive 0.69.7 */
31 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/components/BlockQuoteWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ focused ? "drag_indicator" : null }}
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
64 |
65 |
90 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/components/BulletListWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ focused ? "drag_indicator" : null }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
60 |
61 |
100 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/components/CodeBlockWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ focused ? "drag_indicator" : null }}
17 |
18 |
19 |
20 |
21 |
29 | {{ selectedLanguage !== null ? selectedLanguage : "Language" }}
30 |
31 |
32 |
33 |
34 |
43 |
44 |
45 |
46 |
47 |
52 | {{ language }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
167 |
168 |
212 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/components/HeadingWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ focused ? "drag_indicator" : null }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
60 |
61 |
101 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/components/OrderedListWrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
16 | {{ focused ? "drag_indicator" : null }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
60 |
61 |
95 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/extensions/abbreviation.js:
--------------------------------------------------------------------------------
1 | import { Mark } from "@tiptap/core";
2 |
3 | export const Abbreviation = Mark.create({
4 | name: "abbreviation",
5 |
6 | parseHTML() {
7 | return [
8 | {
9 | tag: "abbr",
10 | },
11 | ];
12 | },
13 |
14 | renderHTML({ HTMLAttributes }) {
15 | // … and return an object with HTML attributes.
16 | return ["abbr", HTMLAttributes, 0];
17 | },
18 |
19 | addAttributes() {
20 | return {
21 | title: {
22 | default: null,
23 | },
24 | };
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/extensions/default-nodes.js:
--------------------------------------------------------------------------------
1 | // tiptap
2 | import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
3 | import Paragraph from "@tiptap/extension-paragraph";
4 | import Heading from "@tiptap/extension-heading";
5 | import Blockquote from "@tiptap/extension-blockquote";
6 | import BulletList from "@tiptap/extension-bullet-list";
7 | import OrderedList from "@tiptap/extension-ordered-list";
8 | import ListItem from "@tiptap/extension-list-item";
9 | import { VueNodeViewRenderer } from "@tiptap/vue-2";
10 | // helper libraries
11 | import { lowlight } from "lowlight";
12 | // components
13 | import BulletListWrapper from "@/notex-editor/components/BulletListWrapper.vue";
14 | import OrderedListWrapper from "@/notex-editor/components/OrderedListWrapper.vue";
15 | import BlockQuoteWrapper from "@/notex-editor/components/BlockQuoteWrapper.vue";
16 | import CodeBlockWrapper from "@/notex-editor/components/CodeBlockWrapper.vue";
17 |
18 | export const NotexParagraph = Paragraph.extend({
19 | draggable: false,
20 | }).configure({
21 | HTMLAttributes: {
22 | class: "info--text text-body1 pl-6 pr-2 py-1 ma-0",
23 | },
24 | });
25 |
26 | export const NotexBulletList = BulletList.extend({
27 | draggable: true,
28 | addNodeView() {
29 | return VueNodeViewRenderer(BulletListWrapper);
30 | },
31 | });
32 |
33 | export const NotexOrderedList = OrderedList.extend({
34 | draggable: true,
35 | addNodeView() {
36 | return VueNodeViewRenderer(OrderedListWrapper);
37 | },
38 | });
39 |
40 | export const NotexListItem = ListItem.extend({
41 | content: "paragraph block*",
42 | draggable: false,
43 | });
44 |
45 | export const NotexHeading = Heading.extend({
46 | addOptions() {
47 | return {
48 | levels: [1, 2, 3],
49 | };
50 | },
51 | }).configure({
52 | HTMLAttributes: {
53 | class: "pa-2 pl-6 info--text",
54 | },
55 | });
56 |
57 | export const NotexCodeBlock = CodeBlockLowlight.extend({
58 | draggable: true,
59 | addNodeView() {
60 | return VueNodeViewRenderer(CodeBlockWrapper);
61 | },
62 | }).configure({
63 | lowlight,
64 | HTMLAttributes: {
65 | class: "code",
66 | },
67 | });
68 |
69 | export const NotexBlockquote = Blockquote.extend({
70 | content: "paragraph*",
71 | draggable: true,
72 | addNodeView() {
73 | return VueNodeViewRenderer(BlockQuoteWrapper);
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/extensions/rainbow.js:
--------------------------------------------------------------------------------
1 | import { Extension } from "@tiptap/core";
2 |
3 | export const Rainbow = Extension.create({
4 | name: "customClass",
5 |
6 | addGlobalAttributes() {
7 | return [
8 | {
9 | types: ["textStyle"],
10 | attributes: {
11 | class: {
12 | default: "",
13 | },
14 | },
15 | },
16 | ];
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/extensions/trailing-node.js:
--------------------------------------------------------------------------------
1 | import { Extension } from "@tiptap/core";
2 | import { PluginKey, Plugin } from "prosemirror-state";
3 |
4 | // @ts-ignore
5 | function nodeEqualsType({ types, node }) {
6 | return (Array.isArray(types) && types.includes(node.type)) || node.type === types;
7 | }
8 |
9 | /**
10 | * Extension based on:
11 | * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
12 | * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
13 | */
14 |
15 | export const TrailingNode = Extension.create({
16 | name: "trailingNode",
17 |
18 | addOptions() {
19 | return {
20 | node: "paragraph",
21 | notAfter: ["paragraph"],
22 | };
23 | },
24 |
25 | addProseMirrorPlugins() {
26 | const plugin = new PluginKey(this.name);
27 | const disabledNodes = Object.entries(this.editor.schema.nodes)
28 | .map(([, value]) => value)
29 | .filter((node) => this.options.notAfter.includes(node.name));
30 |
31 | return [
32 | new Plugin({
33 | key: plugin,
34 | appendTransaction: (_, __, state) => {
35 | const { doc, tr, schema } = state;
36 | const shouldInsertNodeAtEnd = plugin.getState(state);
37 | const endPosition = doc.content.size;
38 | const type = schema.nodes[this.options.node];
39 |
40 | if (!shouldInsertNodeAtEnd) {
41 | return;
42 | }
43 |
44 | return tr.insert(endPosition, type.create());
45 | },
46 | state: {
47 | init: (_, state) => {
48 | const lastNode = state.tr.doc.lastChild;
49 | return !nodeEqualsType({ node: lastNode, types: disabledNodes });
50 | },
51 | apply: (tr, value) => {
52 | if (!tr.docChanged) {
53 | return value;
54 | }
55 |
56 | const lastNode = tr.doc.lastChild;
57 | return !nodeEqualsType({ node: lastNode, types: disabledNodes });
58 | },
59 | },
60 | }),
61 | ];
62 | },
63 | });
64 |
--------------------------------------------------------------------------------
/frontend/src/notex-editor/nodes/draggable-block.js:
--------------------------------------------------------------------------------
1 | import { Node, mergeAttributes } from "@tiptap/core";
2 | import { VueNodeViewRenderer } from "@tiptap/vue-2";
3 | import DraggableBlock from "../components/DraggableBlock.vue";
4 |
5 | export default Node.create({
6 | name: "draggableBlock",
7 | group: "block",
8 | content: "codeBlock",
9 | draggable: true,
10 | code: true,
11 |
12 | parseHTML() {
13 | return [
14 | {
15 | tag: 'div[data-type="draggable-block"]',
16 | },
17 | ];
18 | },
19 |
20 | renderHTML({ HTMLAttributes }) {
21 | console.log("Testing Render Draggable Block.");
22 |
23 | return ["div", mergeAttributes(HTMLAttributes, { "data-type": "draggable-block" }), 0];
24 | },
25 |
26 | addNodeView() {
27 | // if it parent node is doc, render node view, else return
28 | return VueNodeViewRenderer(DraggableBlock);
29 | },
30 |
31 | addCommands() {
32 | return {
33 | setDraggableBlock: (attributes) => ({ commands }) => {
34 | console.log("Testing Draggable Block.");
35 | return commands.setNode("draggableBlock", attributes);
36 | },
37 | toggleDraggableBlock: (attributes) => ({ commands }) => {
38 | console.log("Testing Draggable Block.");
39 | return commands.toggleNode("draggableBlock", "paragraph", attributes);
40 | },
41 | };
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/frontend/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuetify from "vuetify/lib/framework";
3 | import colors from "vuetify/lib/util/colors";
4 |
5 | Vue.use(Vuetify, {});
6 |
7 | export default new Vuetify({
8 | iconfont: "md",
9 | breakpoint: {
10 | thresholds: {
11 | xl: 1280,
12 | },
13 | },
14 | theme: {
15 | options: {
16 | customProperties: true,
17 | },
18 | themes: {
19 | light: {
20 | primary: "#1e90ff",
21 | secondary: "#ff7f50",
22 | error: "#FF5252",
23 | info: "#4F4F4F",
24 | success: "#2ed573",
25 | warning: "#FFC107",
26 | appbar: "#2c2c2c",
27 | drawer: "#1e1e1e",
28 | background: "#f3f3f3",
29 | card: colors.white,
30 | cardtext: colors.grey,
31 | code: "#1e1e1e",
32 | notexBackground: "#f3f3f3",
33 | listBackground: "white",
34 | },
35 | dark: {
36 | primary: "#2ed573",
37 | secondary: "#FFC107",
38 | error: "#FF5252",
39 | info: colors.white,
40 | success: "#2ed573",
41 | warning: "#FFC107",
42 | appbar: "#2c2c2c",
43 | drawer: "#1e1e1e",
44 | background: "#292929",
45 | card: "#2f2f2f",
46 | cardtext: colors.white,
47 | code: "#1e1e1e",
48 | notexBackground: "#292929",
49 | listBackground: "#1e1e1e",
50 | },
51 | },
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/frontend/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueRouter from "vue-router";
3 | import Dashboard from "@/views/Dashboard.vue";
4 | import LandingPage from "@/views/LandingPage.vue";
5 | import UserProfile from "@/views/UserProfile.vue";
6 | import Workspace from "@/views/Workspace.vue";
7 | import Community from "@/views/Community.vue";
8 | import Communities from "@/views/Communities.vue";
9 | import Authentication from "@/views/Authentication.vue";
10 | import NotFound from "@/views/NotFound.vue";
11 | import Ranking from "@/views/Ranking.vue";
12 | /* eslint-disable */
13 | import store from "@/store";
14 |
15 | Vue.use(VueRouter);
16 |
17 | const routes = [
18 | {
19 | name: "home",
20 | path: "/",
21 | meta: {
22 | navbar: "NavigationBar",
23 | },
24 | component: LandingPage,
25 | },
26 | {
27 | name: "dashboard",
28 | path: "/dashboard",
29 | meta: {
30 | requireAuth: true,
31 | navbar: "NavigationBar",
32 | },
33 | component: Dashboard,
34 | },
35 | {
36 | name: "profile",
37 | path: "/profile",
38 | meta: {
39 | requireAuth: true,
40 | navbar: "NavigationBar",
41 | },
42 | component: UserProfile,
43 | },
44 | {
45 | name: "workspace",
46 | path: "/workspace/:id",
47 | meta: {
48 | requireAuth: true,
49 | navbar: "WorkspaceBar",
50 | },
51 | component: Workspace,
52 | },
53 |
54 | {
55 | name: "community",
56 | path: "/community/:id",
57 | meta: {
58 | requireAuth: true,
59 | navbar: "CommunityBar",
60 | },
61 | component: Community,
62 | },
63 | {
64 | name: "communities",
65 | path: "/communities",
66 | meta: {
67 | requireAuth: true,
68 | navbar: "NavigationBar",
69 | },
70 | component: Communities,
71 | },
72 | {
73 | name: "ranking",
74 | path: "/ranking",
75 | meta: {
76 | requireAuth: true,
77 | navbar: "NavigationBar",
78 | },
79 | component: Ranking,
80 | },
81 | {
82 | name: "auth",
83 | path: "/auth",
84 | meta: {
85 | navbar: null,
86 | },
87 | component: Authentication,
88 | },
89 | {
90 | name: "not-found",
91 | path: "/not-found",
92 | component: NotFound,
93 | },
94 | { path: "/:catchAll(.*)*", redirect: { name: "not-found" } }, // redirect random page
95 | ];
96 |
97 | const router = new VueRouter({
98 | mode: "history",
99 | base: process.env.BASE_URL,
100 | routes,
101 | scrollBehavior(to) {
102 | if (to.hash) {
103 | return {
104 | selector: to.hash,
105 | behavior: "smooth",
106 | };
107 | }
108 | },
109 | });
110 |
111 | router.beforeEach((to, from, next) => {
112 | // console.log("Initial Login:", store.state.auth.isAuthenticated);
113 | if (!to.matched.some((record) => record.meta.requireAuth)) {
114 | return next();
115 | }
116 | if (store.state.auth.isAuthenticated) {
117 | return next();
118 | } else {
119 | return next({ name: "auth" });
120 | }
121 | });
122 |
123 | export default router;
124 |
--------------------------------------------------------------------------------
/frontend/src/scss/variables.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 | import modules from "./modules";
4 | import http from "@/includes/http";
5 |
6 | Vue.use(Vuex);
7 |
8 | export default new Vuex.Store({
9 | modules,
10 | // globally defined states (theme, snackbar)
11 | state: {
12 | darkTheme: localStorage.getItem("darkTheme") || false,
13 | snackbarShow: false,
14 | snackbarMessage: "",
15 | snackbarColor: "",
16 | workspaceLeftDrawer: localStorage.getItem("workspaceLeftDrawer") || undefined,
17 | workspaceRightDrawer: localStorage.getItem("workspaceRightDrawer") || undefined,
18 | },
19 | getters: {
20 | rootUser: (state) => {
21 | return state.auth.currentUser;
22 | },
23 | rootIdToken: async (state) => {
24 | return await state.auth.currentUser.user.getIdToken();
25 | },
26 | workspaceLeftDrawer: (state) => {
27 | return state.workspaceLeftDrawer;
28 | },
29 | workspaceRightDrawer: (state) => {
30 | return state.workspaceRightDrawer;
31 | },
32 | },
33 | mutations: {
34 | toggleTheme: (state) => {
35 | state.darkTheme = !state.darkTheme;
36 | },
37 | snackbarInfo: (state, message) => {
38 | state.snackbarMessage = message;
39 | state.snackbarColor = "primary";
40 | state.snackbarShow = true;
41 | },
42 | snackbarSuccess: (state, message) => {
43 | state.snackbarMessage = message;
44 | state.snackbarColor = "success";
45 | state.snackbarShow = true;
46 | },
47 | snackbarError: (state, message) => {
48 | state.snackbarMessage = message;
49 | state.snackbarColor = "error";
50 | state.snackbarShow = true;
51 | },
52 | setSnackbarShow: (state, show) => {
53 | state.snackbarShow = show;
54 | },
55 | setWorkspaceLeftDrawer: (state, idx) => {
56 | console.log(idx);
57 | localStorage.setItem("workspaceLeftDrawer", idx);
58 | },
59 | setWorkspaceRightDrawer: (state, idx) => {
60 | console.log(idx);
61 | localStorage.setItem("workspaceRightDrawer", idx);
62 | },
63 | },
64 | actions: {
65 | rootStateReset({ commit }) {
66 | commit("resetNotes", null, { root: true });
67 | commit("resetCommunities", null, { root: true });
68 | commit("resetCommunityNotes", null, { root: true });
69 | },
70 | async searchUserByKeyword({ rootGetters }, keyword) {
71 | const token = await rootGetters.rootIdToken;
72 | const requestHeader = {
73 | headers: { authorization: `Bearer ${token}` },
74 | };
75 | const res = await http.post("user/search-user-by-keyword", { keyword }, requestHeader);
76 | return res.data;
77 | },
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | getAuth,
3 | createUserWithEmailAndPassword,
4 | signInWithEmailAndPassword,
5 | signOut,
6 | } from "firebase/auth";
7 | import http from "@/includes/http";
8 |
9 |
10 |
11 | export default {
12 | state: {
13 | isAuthenticated: false,
14 | currentUser: null,
15 | currentLevel: 0,
16 | },
17 | getters: {
18 | currentLevel: (state) => {
19 | return state.currentLevel
20 | },
21 | currentUser: (state) => {
22 | return state.currentUser;
23 | },
24 | getIdToken: async (state) => {
25 | return state.currentUser ? null : await state.currentUser.getIdToken();
26 | },
27 | getFullName: (state) => {
28 | return `${state.currentUser.firstname} ${state.currentUser.firstname}`;
29 | },
30 | },
31 | mutations: {
32 | toggleAuth: (state, isAuthenticated) => {
33 | state.isAuthenticated = isAuthenticated;
34 | },
35 | setUser: (state, user) => {
36 | state.currentUser = user;
37 | },
38 | },
39 | actions: {
40 | async getLevel({rootGetters, state}, userId) {
41 | try {
42 | const token = await rootGetters.rootIdToken;
43 | const requestHeader = {
44 | headers: { authorization: `Bearer ${token}` },
45 | };
46 | const { data: res } = await http.get(
47 | `user/${userId}/get-level`,
48 | requestHeader
49 | );
50 | state.currentLevel = res[0].userLevel
51 | console.log(state.currentLevel)
52 | } catch (e) {
53 | console.log(e.message);
54 | }
55 | }
56 | ,
57 | async signup({ commit }, payload) {
58 | const { email, password, firstName, lastName } = payload;
59 | try {
60 | // Firebase signup
61 | const userCredential = await createUserWithEmailAndPassword(getAuth(), email, password);
62 | const token = await userCredential.user.getIdToken();
63 | // insert a new user into Database
64 | const {
65 | data: { userId },
66 | } = await http.post(
67 | "user/insert-user/",
68 | {
69 | email,
70 | firstName,
71 | lastName,
72 | },
73 | { headers: { authorization: `Bearer ${token}` } }
74 | );
75 | commit("setUser", {
76 | userId,
77 | firstname: firstName,
78 | lastname: lastName,
79 | email,
80 | avatarUrl: null,
81 | subtitle: "New User",
82 | user: userCredential.user,
83 | });
84 | // console.log("UseId: ", userId);
85 | commit("toggleAuth", true);
86 |
87 | return true;
88 | } catch (e) {
89 | return e.message;
90 | }
91 | // const idToken = await userCredential.user.getIdToken();
92 | },
93 | async login({ commit }, payload) {
94 | const { email, password } = payload;
95 | try {
96 | const userCredential = await signInWithEmailAndPassword(getAuth(), email, password);
97 | const token = await userCredential.user.getIdToken();
98 | const res = await http.get("user/get-user-by-token/", {
99 | headers: { authorization: `Bearer ${token}` },
100 | });
101 |
102 | const { userId, firstName, lastName, avatarUrl, subtitle } = res.data;
103 | commit("setUser", {
104 | userId: userId,
105 | firstname: firstName,
106 | lastname: lastName,
107 | email,
108 | avatarUrl,
109 | subtitle,
110 | user: userCredential.user,
111 | });
112 | commit("toggleAuth", true);
113 | // refresh page
114 | return true;
115 | } catch (e) {
116 | return e.message;
117 | }
118 | },
119 | async logout({ commit }, { router, route }) {
120 | await signOut(getAuth());
121 | commit("toggleAuth", false);
122 | commit("setUser", null);
123 | if (route.meta.requireAuth) {
124 | router.push({ name: "auth" });
125 | }
126 | },
127 | async initialLogin({ commit, state }) {
128 | if (state.currentUser) return;
129 | try {
130 | const user = getAuth().currentUser;
131 | const token = await user.getIdToken();
132 | if (user) {
133 | const res = await http.get("user/get-user-by-token/", {
134 | headers: { authorization: `Bearer ${token}` },
135 | });
136 | const { userId, firstName, lastName, avatarUrl, subtitle, email } = res.data;
137 | commit("setUser", {
138 | userId: userId,
139 | firstname: firstName,
140 | lastname: lastName,
141 | email: email,
142 | avatarUrl,
143 | subtitle,
144 | user,
145 | });
146 | console.log(state.currentUser);
147 | commit("toggleAuth", true);
148 | }
149 | } catch (e) {
150 | console.log(e.message);
151 | }
152 | },
153 | },
154 | };
155 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/chart.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 |
3 | export default {
4 | state: {
5 | mixData: [],
6 | pieCategories: [],
7 | pieSeries: [],
8 | pieLabels: [],
9 | mixCategories: [],
10 | },
11 | actions: {
12 | async getChartData({ state, rootGetters }) {
13 | try {
14 | const token = await rootGetters.rootIdToken;
15 | const requestHeader = {
16 | headers: { authorization: `Bearer ${token}` },
17 | };
18 | const res = await http.get("user/get-notehub-statistics/", requestHeader);
19 | const allData = res.data;
20 | console.log("asd", allData);
21 | let count = 0;
22 | for (const [, info] of Object.entries(allData)) {
23 | if (count > 4) {
24 | break;
25 | }
26 | console.log("asdas", info.UserStatistics[count]);
27 | state.pieCategories.push(info.UserStatistics[count].categoryName);
28 | console.log(info.UserStatistics[count]);
29 | let labels = [];
30 | let series = [];
31 | for (const obj of Object.entries(info.UserStatistics)) {
32 | labels.push("UserLevel: " + obj[1].userLevel);
33 | series.push(obj[1].userLevelCounts);
34 | }
35 | state.pieLabels.push(labels);
36 | state.pieSeries.push(series);
37 | count++;
38 | }
39 | count = 0;
40 | let data1 = [];
41 | let data2 = [];
42 | let data3 = [];
43 | for (const [, info] of Object.entries(allData)) {
44 | if (count > 9) {
45 | break;
46 | }
47 | let temp = Object.entries(info.NoteStatistics);
48 | state.mixCategories.push(temp[0][1]);
49 | data1.push(temp[2][1]);
50 | data2.push(temp[3][1]);
51 | data3.push(temp[4][1]);
52 | count++;
53 | }
54 | state.mixData = [
55 | { name: "Average Like Counts", type: "column", data: data1 },
56 | { name: "Average View Counts", type: "area", data: data2 },
57 | { name: "Average Comment Counts", type: "line", data: data3 },
58 | ];
59 | console.log([
60 | { name: "Average Like Counts", type: "column", data: data1 },
61 | { name: "Average View Counts", type: "area", data: data2 },
62 | { name: "Average Comment Counts", type: "line", data: data3 },
63 | ]);
64 | } catch (e) {
65 | console.log("there is an error: " + e);
66 | }
67 | },
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/communities.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 |
3 | export default {
4 | state: {
5 | searchResult: [],
6 | },
7 | getters: {
8 | searchResult(state) {
9 | return state.searchResult;
10 | },
11 | },
12 | actions: {
13 | async getSearchResult({ state, rootGetters }, inputVal) {
14 | if (inputVal != ""){
15 | try {
16 | const token = await rootGetters.rootIdToken;
17 | const requestHeader = {
18 | headers: { authorization: `Bearer ${token}` },
19 | };
20 | const res = await http.post(
21 | "community/search-community-by-name/",
22 | { name: inputVal },
23 | requestHeader
24 | );
25 | // res.data.sort((a,b) => a.name.localCompare(b.name))
26 | state.searchResult = res.data;
27 | console.log(state.searchResult);
28 | } catch (e) {
29 | console.log("there is an error: " + e);
30 | }
31 |
32 | }
33 | else{
34 | state.searchResult = []
35 | }
36 | },
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/community-notes.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 |
3 | export default {
4 | state: {
5 | communityNotes: [],
6 | topRankingNotes: [],
7 | },
8 | getters: {
9 | communityNotes: (state) => state.communityNotes,
10 | topRankingNotes: (state) => {
11 | return state.topRankingNotes;
12 | },
13 | },
14 | mutations: {
15 | setCommunityNotes(state, communityNotes) {
16 | state.communityNotes = communityNotes;
17 | },
18 | resetCommunityNotes: (state) => (state.communityNotes = []),
19 | },
20 | actions: {
21 | async initCommunityNotes({ commit, rootGetters }, communityId) {
22 | const token = await rootGetters.rootIdToken;
23 | const requestHeader = {
24 | headers: { authorization: `Bearer ${token}` },
25 | };
26 | let res1 = await http.post("note/get-notes-by-communityId", { communityId }, requestHeader);
27 | commit("setCommunityNotes", res1.data);
28 | // let res2 = await http.post("community/get-top-10-notes", { communityId }, requestHeader);
29 | },
30 | async importNotesByNoteId({ rootGetters }, { communityId, notes }) {
31 | const token = await rootGetters.rootIdToken;
32 | const requestHeader = {
33 | headers: { authorization: `Bearer ${token}` },
34 | };
35 | const data = { communityId, noteIds: notes.map((note) => note.noteId) };
36 | await http.post("community/add-note-to-community", data, requestHeader);
37 | },
38 | /* eslint-disable */
39 | async removeNoteFromCommunityById({ commit, rootGetters }, noteId) {
40 | console.log("I love you");
41 | },
42 | async incrementLikeByNoteId({ rootGetters, state }, info) {
43 | const token = await rootGetters.rootIdToken;
44 | console.log("user id is:" + info.userId);
45 | console.log("note id: " + info.noteId)
46 | const requestHeader = {
47 | headers: { authorization: `Bearer ${token}` },
48 | };
49 | await http.get(`note/${info.userId}&${info.noteId}/like-note`, requestHeader);
50 | // state.communityNotes = state.communityNotes.map((note) => {
51 | // if (note.noteId === noteId) note.likeCount++;
52 | // return note;
53 | // });
54 | // state.topRankingNotes = state.topRankingNotes.map((note) => {
55 | // if (note.noteId === noteId) note.likeCount++;
56 | // return note;
57 | // });
58 | },
59 | async getTopRankingNotes({ rootGetters, state }, communityId) {
60 | const token = await rootGetters.rootIdToken;
61 | const requestHeader = {
62 | headers: { authorization: `Bearer ${token}` },
63 | };
64 | const res = await http.post("community/get-top-10-notes/", { communityId }, requestHeader);
65 | console.log(res.data);
66 | state.topRankingNotes = res.data;
67 | },
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/community.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 | import { uploadBytesResumable, getDownloadURL } from "firebase/storage";
3 | import { storage, ref } from "@/includes/firebase";
4 |
5 | export default {
6 | state: {
7 | communities: [],
8 | communitiesInitialized: false,
9 | communityMembers: [],
10 | },
11 | getters: {
12 | communities: (state) => {
13 | return state.communities;
14 | },
15 | communitiesInitialized: (state) => {
16 | return state.communitiesInitialized;
17 | },
18 | communityMembers: (state) => {
19 | return state.communityMembers;
20 | },
21 | },
22 | mutations: {
23 | setCommunities: (state, communities) => {
24 | state.communities = communities;
25 | },
26 | initCommunitiesCompleted: (state) => {
27 | state.communitiesInitialized = true;
28 | },
29 | resetCommunities: (state) => {
30 | state.communities = [];
31 | state.communitiesInitialized = false;
32 | state.communityMembers = [];
33 | },
34 | setCommunityMembers: (state, members) => {
35 | state.communityMembers = members;
36 | },
37 | },
38 | actions: {
39 | async initCommunityState({ commit, rootGetters, state }) {
40 | state.communitiesInitialized = false;
41 | try {
42 | const token = await rootGetters.rootIdToken;
43 | const requestHeader = {
44 | headers: { authorization: `Bearer ${token}` },
45 | };
46 | const { data: communities } = await http.get(
47 | "community/get-all-communities/",
48 | requestHeader
49 | );
50 | commit("setCommunities", communities);
51 | } catch (e) {
52 | console.log(e.message);
53 | }
54 | commit("initCommunitiesCompleted");
55 | },
56 | async initCommunityMembers({ commit, rootGetters }, communityId) {
57 | const token = await rootGetters.rootIdToken;
58 | const requestHeader = {
59 | headers: { authorization: `Bearer ${token}` },
60 | };
61 | const res = await http.post(
62 | "community/get-members-by-community-id",
63 | { communityId },
64 | requestHeader
65 | );
66 | commit("setCommunityMembers", res.data);
67 | console.log(res.data);
68 | },
69 | async getCommunityById({ rootGetters }, communityId) {
70 | const token = await rootGetters.rootIdToken;
71 | const requestHeader = {
72 | headers: { authorization: `Bearer ${token}` },
73 | };
74 | const res = await http.post(
75 | "community/get-community-by-community-id",
76 | { communityId },
77 | requestHeader
78 | );
79 | return res.data;
80 | },
81 | async shareCommunityToUserById({ rootGetters }, { communityId, userIds }) {
82 | const token = await rootGetters.rootIdToken;
83 | const requestHeader = {
84 | headers: { authorization: `Bearer ${token}` },
85 | };
86 | const data = userIds.map((id) => ({
87 | communityId,
88 | userId: id,
89 | role: "member",
90 | }));
91 | await http.post("community/insert-membership/", data, requestHeader);
92 | },
93 | async createCommunityByUser({ rootGetters, state }, payload) {
94 | const { name, description, photoFile } = payload;
95 | const token = await rootGetters.rootIdToken;
96 | const currentUser = rootGetters.rootUser;
97 | const requestHeader = {
98 | headers: { authorization: `Bearer ${token}` },
99 | };
100 | const storageRef = ref(storage);
101 | const fileName = `${Math.floor(Date.now() / 1000)}-${photoFile.name}`;
102 | const communityPhotoRef = ref(storageRef, `community_photos/${fileName}`);
103 | const task = uploadBytesResumable(communityPhotoRef, photoFile);
104 | task.on(
105 | "state_changed",
106 | (snapshot) => {
107 | // Observe state change events such as progress, pause, and resume
108 | // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
109 | const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
110 | console.log(progress);
111 | },
112 | (error) => {
113 | console.log(error);
114 | },
115 | async () => {
116 | const photo = await getDownloadURL(task.snapshot.ref);
117 | const community = {
118 | name,
119 | description,
120 | photo,
121 | };
122 | const res = await http.post("community/insert-community", community, requestHeader);
123 | state.communities.push({
124 | ...res.data,
125 | owner: {
126 | userId: currentUser.userId,
127 | firstName: currentUser.firstname,
128 | lastName: currentUser.lastname,
129 | subtitle: currentUser.subtitle,
130 | email: currentUser.email,
131 | avatarUrl: currentUser.avatarUrl,
132 | },
133 | });
134 | }
135 | );
136 | },
137 | /* eslint-disable */
138 | async leaveCommunityById({ commit, state, rootGetters }, payload) {
139 | const { communityId, userId } = payload;
140 | const token = await rootGetters.rootIdToken;
141 | const requestHeader = {
142 | headers: { authorization: `Bearer ${token}` },
143 | };
144 | const data = { communityId, userId };
145 | await http.post("community/delete-membership/", data, requestHeader);
146 | state.communities = state.communities.filter(
147 | (community) => community.communityId !== communityId
148 | );
149 | },
150 | },
151 | };
152 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import camelCase from "lodash/camelCase";
3 |
4 | const requireModule = require.context(".", false, /\.js$/);
5 |
6 | const modules = {};
7 |
8 | requireModule.keys().forEach((fileName) => {
9 | if (fileName === "./index.js" || fileName === "./dummy.js") return;
10 |
11 | const moduleConfig = requireModule(fileName);
12 | const moduleName = camelCase(fileName.replace(/(\.\/|\.js)/g, ""));
13 | modules[moduleName] = moduleConfig.default || moduleConfig;
14 | });
15 |
16 | export default modules;
17 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/note.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 | /* eslint-disable */
3 | import { doc, getDoc, addDoc, query, where, getDocs, setDoc, collection } from "firebase/firestore";
4 | import { db } from "@/includes/firebase";
5 |
6 | export default {
7 | state: {
8 | notes: [],
9 | sharedUsers: [],
10 | noteCategories: [],
11 | notesInitialized: false,
12 | },
13 | getters: {
14 | sharedUsers: (state) => {
15 | return state.sharedUsers;
16 | },
17 | notes: (state) => {
18 | return state.notes;
19 | },
20 | noteCategories: (state) => {
21 | return state.noteCategories;
22 | },
23 | notesInitialized: (state) => {
24 | return state.notesInitialized;
25 | },
26 | },
27 | mutations: {
28 | setNotes: (state, notes) => {
29 | state.notes = notes;
30 | },
31 | setSharedUsers: (state, sharedUsers) => {
32 | state.sharedUsers = sharedUsers;
33 | },
34 | setNoteCategories: (state, categories) => {
35 | state.noteCategories = categories;
36 | },
37 | initNotesCompleted: (state) => {
38 | state.notesInitialized = true;
39 | },
40 | resetNotes: (state) => {
41 | state.notesInitialized = false;
42 | state.sharedUsers = [];
43 | state.notes = [];
44 | state.noteCategories = [];
45 | },
46 | updateNoteSharedUsers: (state, noteId, sharedUsers) => {
47 | state.notes = notes.map((note) => {
48 | if (note.noteId === noteId) {
49 | return { ...note, sharedUsers };
50 | }
51 | return note;
52 | });
53 | },
54 | },
55 | actions: {
56 | async initNoteState({ commit, rootGetters, state }) {
57 | state.notesInitialized = false;
58 | try {
59 | const token = await rootGetters.rootIdToken;
60 | const requestHeader = {
61 | headers: { authorization: `Bearer ${token}` },
62 | };
63 | const { data: sharedUsers } = await http.get("user/get-note-providers/", requestHeader);
64 | const { data: notes } = await http.get("note/get-user-notes/", requestHeader);
65 | const { data: categories } = await http.get("note/get-all-categories", requestHeader);
66 | commit("setNotes", notes);
67 | commit("setSharedUsers", sharedUsers);
68 | commit("setNoteCategories", categories);
69 | } catch (e) {}
70 | commit("initNotesCompleted");
71 | },
72 | async createNoteByUser({ rootGetters, state }, payload) {
73 | const { noteTitle, userId, accessStatus, categories } = payload;
74 | const token = await rootGetters.rootIdToken;
75 | const requestHeader = {
76 | headers: { authorization: `Bearer ${token}` },
77 | };
78 | try {
79 | const noteCollection = collection(db, "notes");
80 | const noteRef = await addDoc(noteCollection, {
81 | data: {},
82 | accessingUsers: [],
83 | chats: [],
84 | });
85 | // const docRef = doc(db, "notes", noteRef.id);
86 | // const docSnap = await getDoc(docRef);
87 | const note = {
88 | noteTitle: noteTitle,
89 | dataId: noteRef.id,
90 | categories: categories,
91 | };
92 | const res = await http.post("note/insert-note", note, requestHeader);
93 | const user = rootGetters.rootUser;
94 | state.notes.push({
95 | ...res.data,
96 | owner: { ...user, firstName: user.firstname, lastName: user.lastname }, // well...
97 | sharedUsers: [],
98 | });
99 | } catch (error) {
100 | console.log(error);
101 | }
102 | },
103 | async editNoteTitleById({ state, rootGetters }, payload) {
104 | const { noteId, newNoteTitle } = payload;
105 | const token = await rootGetters.rootIdToken;
106 | const requestHeader = {
107 | headers: { authorization: `Bearer ${token}` },
108 | };
109 | const data = { noteTitle: newNoteTitle, noteId: noteId };
110 | const { data: editedNote } = await http.post("note/update-note/", data, requestHeader);
111 | state.notes = state.notes.map((note) =>
112 | note.noteId === editedNote.noteId ? { ...note, noteTitle: editedNote.noteTitle } : note
113 | );
114 | },
115 | async deleteNoteAccessById({ rootGetters, state }, payload) {
116 | const { noteId, newOwnerId } = payload;
117 | const token = await rootGetters.rootIdToken;
118 | const requestHeader = {
119 | headers: { authorization: `Bearer ${token}` },
120 | };
121 | const data = { noteId };
122 | if (newOwnerId != null) data.newOwnerId = newOwnerId;
123 |
124 | await http.post("note/transfer-note-ownership/", data, requestHeader);
125 | state.notes = state.notes.filter((note) => note.noteId != noteId);
126 | },
127 | async deleteSharedUserById({ rootGetters }, payload) {
128 | const { noteId, userId } = payload;
129 | const token = await rootGetters.rootIdToken;
130 | const requestHeader = {
131 | headers: { authorization: `Bearer ${token}` },
132 | };
133 | await http.post(
134 | "note/alter-note-access/",
135 | { noteId, userId, command: "DELETE" },
136 | requestHeader
137 | );
138 | },
139 | async changeNoteAccessStatus({ rootGetters }, payload) {
140 | const { noteId, userId, accessStatus } = payload;
141 | const token = await rootGetters.rootIdToken;
142 | const requestHeader = {
143 | headers: { authorization: `Bearer ${token}` },
144 | };
145 | await http.post(
146 | "note/alter-note-access/",
147 | { noteId, userId, accessStatus, command: "UPDATE" },
148 | requestHeader
149 | );
150 | },
151 | async shareNoteByUserId({ rootGetters }, payload) {
152 | const { noteId, userIds } = payload;
153 | const token = await rootGetters.rootIdToken;
154 | const requestHeader = {
155 | headers: { authorization: `Bearer ${token}` },
156 | };
157 | return await http.post(
158 | "note/alter-note-access/",
159 | { noteId, userIds, command: "INSERT" },
160 | requestHeader
161 | );
162 | },
163 | },
164 | };
165 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/ranking.js:
--------------------------------------------------------------------------------
1 | import http from "@/includes/http";
2 |
3 | export default {
4 | state: {
5 | popularUser: [],
6 | },
7 | getters: {
8 | returnPopularUser(state) {
9 | return state.popularUser;
10 | },
11 | },
12 | actions: {
13 | async getPopularUser({ state, rootGetters }) {
14 | try {
15 | const token = await rootGetters.rootIdToken;
16 | const requestHeader = {
17 | headers: { authorization: `Bearer ${token}` },
18 | };
19 | const res = await http.get(
20 | "user/get-top-10-users/",
21 | requestHeader
22 | );
23 | state.popularUser = res.data;
24 | console.log(state.popularUser)
25 | } catch (e) {
26 | console.log("there is an error: " + e);
27 | }
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/frontend/src/store/modules/workspace.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import http from "@/includes/http";
3 |
4 | export default {
5 | state: {
6 | note: null,
7 | comments: [],
8 | },
9 | getters: {
10 | workspaceNote: (state) => {
11 | return state.note;
12 | },
13 | workspaceComments: (state) => {
14 | return state.comments;
15 | },
16 | },
17 | mutations: {
18 | updateWorkspaceNote(state, note) {
19 | state.note = note;
20 | },
21 | updateWorkspaceComments(state, comments) {
22 | state.comments = comments;
23 | },
24 | },
25 | actions: {
26 | async getWorkspaceNote({ commit, rootGetters }, id) {
27 | try {
28 | const token = await rootGetters.rootIdToken;
29 | const requestHeader = {
30 | headers: { authorization: `Bearer ${token}` },
31 | };
32 | const response = await http.post(
33 | `note/get-note-by-noteId/`,
34 | { noteId: parseInt(id) },
35 | requestHeader
36 | );
37 | commit("updateWorkspaceNote", response.data);
38 | } catch (e) {
39 | console.log(e);
40 | }
41 | },
42 | async getWorkspaceComments({ commit, rootGetters }, id) {
43 | try {
44 | const token = await rootGetters.rootIdToken;
45 | const requestHeader = {
46 | headers: { authorization: `Bearer ${token}` },
47 | };
48 | const response = await http.post(
49 | `note/get-comments-by-noteId/`,
50 | { noteId: parseInt(id) },
51 | requestHeader
52 | );
53 | console.log(response.data);
54 | commit("updateWorkspaceComments", response.data);
55 | } catch (e) {
56 | console.log(e);
57 | }
58 | },
59 | async addCommentById({ rootGetters }, data) {
60 | const token = await rootGetters.rootIdToken;
61 | const requestHeader = {
62 | headers: { authorization: `Bearer ${token}` },
63 | };
64 | const response = await http.post(`note/${data.noteId}/insert-comment`, data, requestHeader);
65 | console.log(response.data);
66 | },
67 | async updateCommentContentById({ rootGetters }, data) {
68 | const token = await rootGetters.rootIdToken;
69 | const requestHeader = {
70 | headers: { authorization: `Bearer ${token}` },
71 | };
72 | const response = await http.post(`note/update-comment-content`, data, requestHeader);
73 | console.log(response.data);
74 | },
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/frontend/src/views/Community.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 | {{ community.name }}
12 |
13 |
14 |
15 |
16 |
17 | brightness_medium
20 |
21 | dark mode
22 |
23 |
24 |
25 |
26 | brightness_high
27 |
28 |
29 | light mode
30 |
31 |
32 |
33 |
34 |
39 | {{ tab.tabName }}
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/frontend/src/views/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Dashboard
6 |
7 |
8 |
9 |
10 |
11 |
12 |
21 |
22 |
23 |
24 | {{ fab ? "close" : "add" }}
25 |
26 |
27 |
28 |
29 |
30 |
39 | edit_note
40 |
41 |
42 | Create New Notebook
43 |
44 |
45 |
46 |
55 | group_add
56 |
57 |
58 | Create New Community
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
101 |
102 |
128 |
--------------------------------------------------------------------------------
/frontend/src/views/LandingPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
{{pieCategories[i]}}
14 |
15 |
User Level Percent in Top 5 Popular Category
16 |
17 |
20 |
Notes Data in Top 10 Popular Category
21 |
22 |
23 |
24 |
25 |
141 |
142 |
--------------------------------------------------------------------------------
/frontend/src/views/NotFound.vue:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/frontend/src/views/NotFound.vue
--------------------------------------------------------------------------------
/frontend/src/views/Ranking.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
16 | {{ user.firstName }} {{ user.lastName }}
19 |
20 | mdi-thumb-up
21 | {{ user.likeCount }}
22 | mdi-eye
23 | {{ user.commentCount }}
24 |
25 |
26 | Follow
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/frontend/src/views/UserProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
16 |
17 |
18 |
{{ `${user.firstname} ${user.lastname}`}}
19 |
User Level: {{ currentLevel }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
52 |
53 |
65 |
--------------------------------------------------------------------------------
/frontend/src/views/Workspace.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/frontend/tests/unit/example.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import HelloWorld from '@/components/HelloWorld.vue'
3 |
4 | describe('HelloWorld.vue', () => {
5 | it('renders props.msg when passed', () => {
6 | const msg = 'new message'
7 | const wrapper = shallowMount(HelloWorld, {
8 | propsData: { msg }
9 | })
10 | expect(wrapper.text()).toMatch(msg)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/frontend/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transpileDependencies: [
3 | 'vuetify'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/nginx/nginx-setup.conf:
--------------------------------------------------------------------------------
1 | upstream api {
2 | server localhost:8000;
3 | }
4 |
5 |
6 | server {
7 | listen 7070;
8 |
9 | location / {
10 | root /var/www/vue;
11 | }
12 |
13 | location /api/ {
14 | proxy_pass http://api;
15 | proxy_set_header Host $http_host;
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notehub",
3 | "version": "1.0.0",
4 | "description": "NoteHub is a real-time collaborative note-taking web app ",
5 | "main": "index.js",
6 | "scripts": {
7 | "server": "npm run watch --prefix server",
8 | "frontend": "npm run serve --prefix frontend",
9 | "watch": "npm-run-all --parallel server frontend",
10 | "test": "npm run test --prefix server && npm run test --prefix frontend",
11 | "deploy": "npm run build --prefix frontend && npm run start --prefix server"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/Toubat/NoteHub.git"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/Toubat/NoteHub/issues"
21 | },
22 | "homepage": "https://github.com/Toubat/NoteHub#readme",
23 | "dependencies": {
24 | "dotenv": "^10.0.0",
25 | "nodemon": "^2.0.15",
26 | "npm-run-all": "^4.1.5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | /node_modules
3 | README.md
4 | test
5 | /public
6 | Dockerfile
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "commonjs": true,
6 | "es2021": true
7 | },
8 | "extends": "plugin:vue/essential",
9 | "parserOptions": {
10 | "ecmaVersion": 2020
11 | },
12 | "plugins": [
13 | "vue"
14 | ],
15 | "rules": {
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | database/
3 | # local env files
4 | .env.local
5 | .env.*.local
6 |
7 | # Editor directories and files
8 | .idea
9 | .vscode
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 | *.sw?
15 |
16 | # Logs
17 | logs
18 | *.log
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 | lerna-debug.log*
23 | .pnpm-debug.log*
24 |
25 | # Diagnostic reports (https://nodejs.org/api/report.html)
26 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
27 |
28 | # Runtime data
29 | pids
30 | *.pid
31 | *.seed
32 | *.pid.lock
33 |
34 | # Directory for instrumented libs generated by jscoverage/JSCover
35 | lib-cov
36 |
37 | # Coverage directory used by tools like istanbul
38 | coverage
39 | *.lcov
40 |
41 | # nyc test coverage
42 | .nyc_output
43 |
44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
45 | .grunt
46 |
47 | # Bower dependency directory (https://bower.io/)
48 | bower_components
49 |
50 | # node-waf configuration
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 | build/Release
55 |
56 | # Dependency directories
57 | node_modules/
58 | jspm_packages/
59 |
60 | # Snowpack dependency directory (https://snowpack.dev/)
61 | web_modules/
62 |
63 | # TypeScript cache
64 | *.tsbuildinfo
65 |
66 | # Optional npm cache directory
67 | .npm
68 |
69 | # Optional eslint cache
70 | .eslintcache
71 |
72 | # Microbundle cache
73 | .rpt2_cache/
74 | .rts2_cache_cjs/
75 | .rts2_cache_es/
76 | .rts2_cache_umd/
77 |
78 | # Optional REPL history
79 | .node_repl_history
80 |
81 | # Output of 'npm pack'
82 | *.tgz
83 |
84 | # Yarn Integrity file
85 | .yarn-integrity
86 |
87 | # dotenv environment variables file
88 | .env
89 | .env.test
90 | .env.production
91 |
92 | # parcel-bundler cache (https://parceljs.org/)
93 | .cache
94 | .parcel-cache
95 |
96 | # Next.js build output
97 | .next
98 | out
99 |
100 | # Nuxt.js build / generate output
101 | .nuxt
102 | dist
103 |
104 | # Gatsby files
105 | .cache/
106 | # Comment in the public line in if your project uses Gatsby and not Next.js
107 | # https://nextjs.org/blog/next-9-1#public-directory-support
108 | # public
109 |
110 | # vuepress build output
111 | .vuepress/dist
112 |
113 | # Serverless directories
114 | .serverless/
115 |
116 | # FuseBox cache
117 | .fusebox/
118 |
119 | # DynamoDB Local files
120 | .dynamodb/
121 |
122 | # TernJS port file
123 | .tern-port
124 |
125 | # Stores VSCode versions used for testing VSCode extensions
126 | .vscode-test
127 |
128 | # yarn v2
129 | .yarn/cache
130 | .yarn/unplugged
131 | .yarn/build-state.yml
132 | .yarn/install-state.gz
133 | .pnp.*
134 |
135 | src/routes/firebase/serviceAccountKey.json
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine
2 | WORKDIR /server
3 | COPY package*.json ./
4 | RUN npm install
5 | COPY . .
6 | CMD [ "npm", "run","watch" ]
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # NoteHub Backend
2 |
3 | ## Project setup
4 |
5 | ```
6 | npm install
7 | ```
8 |
9 | ### Hot-reloads for development server
10 |
11 | ```
12 | npm run watch
13 | ```
14 |
15 | ### Start server
16 |
17 | ```
18 | npm run start
19 | ```
20 |
21 | ### Compiles and minifies for production
22 |
23 | Please refer to the README under NoteHub folder
24 |
25 | ### A database is required to configured in local environment:
26 |
27 | Update all your local database info in `.env` file at `NOTEHUB/server/.env`.
28 |
29 | **Note**: ".env" files in `src` and `models` folders are not intended to be changed in normal case.
30 | They are intended for testing purpose. If you need to test your database connection, you can change them as well.
31 |
32 | If the remote GCP database is enabled, you can uncomment that part of info and comment your info, then a remote database is connected.
33 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "lint": "./node_modules/.bin/eslint src/**/*.js --fix",
8 | "watch": "cross-env PORT=8000 nodemon ./src/server.js",
9 | "start": "cross-env PORT=8000 node ./src/server.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@hocuspocus/extension-rocksdb": "^1.0.0-alpha.79",
16 | "@hocuspocus/provider": "^1.0.0-alpha.21",
17 | "@hocuspocus/server": "^1.0.0-alpha.78",
18 | "@hocuspocus/transformer": "^1.0.0-alpha.15",
19 | "@tiptap/extension-document": "^2.0.0-beta.15",
20 | "@tiptap/extension-heading": "^2.0.0-beta.23",
21 | "@tiptap/extension-paragraph": "^2.0.0-beta.22",
22 | "@tiptap/extension-text": "^2.0.0-beta.15",
23 | "@tiptap/starter-kit": "^2.0.0-beta.140",
24 | "cookie-parser": "^1.4.5",
25 | "cors": "^2.8.5",
26 | "csurf": "^1.11.0",
27 | "debounce": "^1.2.1",
28 | "dotenv": "^10.0.0",
29 | "ejs": "^3.1.6",
30 | "express": "^4.17.1",
31 | "express-unless": "^1.0.0",
32 | "express-ws": "^5.0.2",
33 | "firebase": "^9.0.2",
34 | "firebase-admin": "^9.11.1",
35 | "mongodb": "^4.1.4",
36 | "morgan": "^1.10.0",
37 | "mysql2": "^2.3.0",
38 | "nodemon": "^2.0.15",
39 | "sequelize": "^6.6.5",
40 | "websocket": "^1.0.34",
41 | "yjs": "^13.5.20"
42 | },
43 | "devDependencies": {
44 | "cross-env": "^7.0.3",
45 | "eslint": "^7.32.0",
46 | "eslint-plugin-vue": "^7.17.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/favicon.ico
--------------------------------------------------------------------------------
/server/public/img/auth-background.9c92a012.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/img/auth-background.9c92a012.jpg
--------------------------------------------------------------------------------
/server/public/img/search-background.3114fa6b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoteHub-official/NoteHub/bf82746bb08be5fcb4b09bae917089c98eb7584e/server/public/img/search-background.3114fa6b.png
--------------------------------------------------------------------------------
/server/public/index.html:
--------------------------------------------------------------------------------
1 | frontend
--------------------------------------------------------------------------------
/server/src/app.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | const express = require("express");
4 |
5 | const cors = require("cors");
6 |
7 | const morgan = require("morgan");
8 |
9 | const csrf = require("csurf");
10 |
11 | const cookieParser = require("cookie-parser");
12 |
13 | const admin = require("firebase-admin");
14 |
15 | const apiRouter = require("./routes/api.router");
16 |
17 | require("dotenv").config();
18 |
19 | const connection = require("./models/database");
20 |
21 | const app = express();
22 |
23 | app.use(
24 | cors({
25 | origin: "http://localhost:8080",
26 | })
27 | );
28 |
29 | // logging in Apache combined standard
30 |
31 | app.use(morgan("combined"));
32 |
33 | // recognize the incoming Request Object as a JSON Object.
34 |
35 | // parse requests of content-type - application/x-www-form-urlencoded
36 |
37 | app.use(express.json());
38 |
39 | app.use(express.urlencoded({ extended: true }));
40 |
41 | app.use(cookieParser());
42 |
43 | //const csrfMiddleware = csrf({ cookie: true });
44 |
45 | //app.use(csrfMiddleware);
46 |
47 | // The build folder for storing static files generated by Vue
48 |
49 | app.use(express.static(path.join(__dirname, "..", "public")));
50 |
51 | // Swagger: a API documentation tool
52 |
53 | app.all("*", (req, res, next) => {
54 | //res.cookie("XSRF-TOKEN", req.csrfToken());
55 |
56 | next();
57 | });
58 |
59 | //routes to api
60 |
61 | app.use("/api", apiRouter);
62 |
63 | app.get("/*", (req, res, next) => {
64 | if (
65 | req.url === "/test123" ||
66 | req.url.match(/^\/websocket\/.+/) !== null ||
67 | req.upgrade === true
68 | ) {
69 | return next();
70 | }
71 | res.sendFile(path.join(__dirname, "..", "public", "index.html"));
72 | });
73 |
74 | module.exports = { app };
75 |
--------------------------------------------------------------------------------
/server/src/hocuspocus-hooks/hooks.js:
--------------------------------------------------------------------------------
1 | /*
2 | A list of hooks provided by hocuspocus
3 | See details at HocusPocus websote
4 | */
5 | const { Document } = require("@tiptap/extension-document");
6 | const { Paragraph } = require("@tiptap/extension-paragraph");
7 | const { Text } = require("@tiptap/extension-text");
8 | const { Heading } = require("@tiptap/extension-heading");
9 | const { CodeBlock } = require("@tiptap/extension-code-block");
10 | const { Blockquote } = require("@tiptap/extension-blockquote");
11 |
12 | const { selectUserByEmail } = require("../models/user.sql");
13 | const { getUserInfoFromFirebase } = require("../routes/firebase/firebase.utils");
14 | const { selectNoteAccessByNoteIdAndUserId } = require("../models/note.sql");
15 |
16 | const fs = require("fs");
17 | const path = require("path");
18 | const mongo = require("../models/mongo.utils");
19 |
20 | const { TiptapTransformer, ProsemirrorTransformer } = require("@hocuspocus/transformer");
21 |
22 | var debounce = require("debounce");
23 | const { ConnectionPoolClearedEvent } = require("mongodb");
24 |
25 | let debounced;
26 |
27 | async function saveToMongo(data) {
28 | const prosemirrorJSON = TiptapTransformer.fromYdoc(data.document);
29 | // Save your document. In a real-world app this could be a database query
30 | // a webhook or something else
31 |
32 | const out = await mongo.notes.upsert(data.documentName, prosemirrorJSON);
33 | // Maybe you want to store the user who changed the document?
34 | // Guess what, you have access to your custom context from the
35 | // onConnect hook here. See authorization & authentication for more
36 | // details
37 | console.log(`Saving ${data.documentName} last changed by ${data.context.user.name}`);
38 | }
39 |
40 | const hooks = {
41 | async onConnect(data) {
42 | console.log("onConnect: A new connection has been established");
43 | },
44 |
45 | async onAuthenticate(data) {
46 | const { token } = data;
47 | console.log("onAuthenticate: Authentication requested");
48 |
49 | //console.log(token)
50 |
51 | const userInfo = await getUserInfoFromFirebase(token);
52 | if (userInfo.email === "Invalid") {
53 | throw new Error("invalid email");
54 | }
55 |
56 | const user = await selectUserByEmail(userInfo.email);
57 |
58 | if (user === null) {
59 | throw new Error("invalid user");
60 | }
61 |
62 | const access = await selectNoteAccessByNoteIdAndUserId(data.documentName, user.userId);
63 | if (access === null) {
64 | throw new Error("You cannot access this note!");
65 | }
66 |
67 | // If the user is only a viewer
68 | if (access === "viewer") {
69 | data.connection.readOnly = true;
70 | }
71 | console.log("onAuthenticate: Authentication pass");
72 | return {
73 | user: {
74 | id: user.userId,
75 | name: user.firstName,
76 | },
77 | };
78 | },
79 |
80 | async onLoadDocument(data) {
81 | console.log(`onLoadDocument: "${data.context.user.name}"`);
82 | // Print current working directory:
83 |
84 | // The tiptap collaboration extension uses shared types of a single y-doc
85 | // to store different fields in the same document.
86 | // The default field in tiptap is simply called 'default'
87 | const fieldName = "default";
88 |
89 | // Check if the given field already exists in the given y-doc.
90 | // Important: Only import a document if it doesn't exist in the primary data storage!
91 | if (!data.document.isEmpty(fieldName)) {
92 | console.log("Document is already in primary dbfield: default, no need to load again");
93 | return;
94 | }
95 |
96 | const rawJson = await mongo.notes.findOne(data.documentName);
97 |
98 | defaultJsonPath = path.join(__dirname, "startTemplate.json");
99 | console.log("onLoadDocument finished");
100 | const fileToLoad =
101 | rawJson && rawJson.default
102 | ? rawJson.default
103 | : JSON.parse(fs.readFileSync(defaultJsonPath, "utf8"));
104 | // Convert the editor format to a y-doc. The TiptapTransformer requires you to pass the list
105 | // of extensions you use in the frontend to create a valid document
106 | return TiptapTransformer.toYdoc(fileToLoad, fieldName, [Document, Paragraph, Text, Heading]);
107 | },
108 |
109 | async onDisconnect(data) {
110 | // Output some information
111 | console.log(` Websocket disconnection`);
112 | console.log(`"${data.context.user.name}" has disconnected.`);
113 |
114 | // Save the document
115 | saveToMongo(data);
116 | },
117 |
118 | async onChange(data) {
119 | // return null if debounced is undefined
120 | debounced?.clear();
121 | debounced = debounce(() => {
122 | console.log(`onChange: ${data.documentName}`);
123 | const prosemirrorJSON = TiptapTransformer.fromYdoc(data.document);
124 |
125 | const size = new TextEncoder().encode(JSON.stringify(prosemirrorJSON)).length;
126 | const kiloBytes = size / 1024;
127 | console.log(`JSON size in KB: ${kiloBytes}`);
128 | saveToMongo(data);
129 | }, 500);
130 | debounced();
131 | },
132 | };
133 |
134 | module.exports = hooks;
135 |
--------------------------------------------------------------------------------
/server/src/hocuspocus-hooks/startTemplate.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "doc",
3 | "content": [
4 | {
5 | "type": "heading",
6 | "attrs": {
7 | "textAlign": "left",
8 | "level": 2
9 | },
10 | "content": [
11 | {
12 | "type": "text",
13 | "text": "Start your learning journey ~ :)"
14 | }
15 | ]
16 | },
17 | {
18 | "type": "paragraph",
19 | "attrs": {
20 | "textAlign": "left"
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/models/database.js:
--------------------------------------------------------------------------------
1 | // Database setup for mysql2
2 | const { Sequelize } = require("sequelize");
3 | require("dotenv").config();
4 |
5 | const sequelize = new Sequelize(
6 | process.env.DB_DATABASE ? process.env.DB_DATABASE : "mynotehub_db",
7 | process.env.DB_USER ? process.env.DB_USER : "mynotehub_root",
8 | process.env.DB_PASS ? process.env.DB_PASS : "Notehub123123",
9 | {
10 | host: process.env.DB_HOST ? process.env.DB_HOST : "127.0.0.1",
11 | dialect: "mysql",
12 | }
13 | );
14 | module.exports = sequelize;
15 | /*This function is used to test your connection to the database*/
16 | // async function test() {
17 | // try {
18 | // await sequelize.authenticate();
19 | // console.log("Connection has been established successfully.");
20 | // } catch (error) {
21 | // console.error("Unable to connect to the database:", error);
22 | // }
23 | // }
24 | // test();
25 |
--------------------------------------------------------------------------------
/server/src/models/membership.sql.js:
--------------------------------------------------------------------------------
1 | const sequelize = require("./database");
2 | const { QueryTypes } = require("sequelize");
3 |
4 | //CREATE Membership
5 | async function insertMembership(info) {
6 | let insertTuples = [];
7 | info.forEach((item) => {
8 | insertTuples.push(`(${item.communityId}, '${item.userId}', '${item.role}')`);
9 | });
10 | const insertString = insertTuples.join(",");
11 | try {
12 | console.log(`INSERT IGNORE INTO Membership(communityId, userId, role) VALUES ${insertString}`);
13 | await sequelize.query(
14 | `INSERT IGNORE INTO Membership(communityId, userId, role) VALUES ${insertString}`,
15 | {
16 | type: QueryTypes.INSERT,
17 | }
18 | );
19 |
20 | return `Membership is successfully inserted`;
21 | } catch (e) {
22 | console.error(e);
23 | throw new Error(e.message);
24 | }
25 | }
26 |
27 | //DELETE membership
28 | async function deleteMembership(info) {
29 | try {
30 | await sequelize.query(
31 | `DELETE FROM Membership WHERE userId = '${info.userId}' AND communityId = '${info.communityId}'`,
32 | {
33 | type: QueryTypes.DELETE,
34 | }
35 | );
36 |
37 | console.log(`${info.communityId} with user ${info.userId} membership is successfully deleted`);
38 | } catch (e) {
39 | throw new Error(e.message);
40 | }
41 | }
42 |
43 | async function selectMembersByCommunityId(commId) {
44 | try {
45 | const data = await sequelize.query(
46 | `SELECT DISTINCT * FROM User NATURAL JOIN Membership WHERE communityId = '${commId}'`,
47 | { type: QueryTypes.SELECT }
48 | );
49 | const output = [
50 | {
51 | role: "Owner",
52 | icon: "psychology",
53 | users: data.filter(({ role }) => role === "owner"),
54 | },
55 | {
56 | role: "Manager",
57 | icon: "manage_accounts",
58 | users: data.filter(({ role }) => role === "manager"),
59 | },
60 | {
61 | role: "Member",
62 | icon: "groups",
63 | users: data.filter(({ role }) => role === "member"),
64 | },
65 | ];
66 | return output;
67 | } catch (e) {
68 | throw new Error(e.message);
69 | }
70 | }
71 |
72 | async function alterMembershipRole(info) {
73 | try {
74 | await sequelize.query(
75 | `UPDATE Membership
76 | SET role = '${info.role}'
77 | WHERE communityId = ${info.communityId}, userId = '${info.userId}'`,
78 | {
79 | type: QueryTypes.UPDATE,
80 | }
81 | );
82 | } catch (e) {
83 | throw new Error(e.message);
84 | }
85 | }
86 |
87 | module.exports = {
88 | insertMembership,
89 | selectMembersByCommunityId,
90 | deleteMembership,
91 | alterMembershipRole,
92 | };
93 |
--------------------------------------------------------------------------------
/server/src/models/mongo.utils.js:
--------------------------------------------------------------------------------
1 | const { MongoClient } = require("mongodb");
2 | const Note = require("./note.repository");
3 |
4 |
5 | const url =
6 | "mongodb+srv://notehubRoot:notehub123123@cluster0.n6o3n.mongodb.net/notehub?retryWrites=true&w=majority";
7 | class MongoBot {
8 | constructor() {
9 | this.client = new MongoClient(url, {
10 | useNewUrlParser: true,
11 | useUnifiedTopology: true,
12 | });
13 | }
14 | async init() {
15 | await this.client.connect();
16 | console.log("mongodb connected");
17 |
18 | this.db = this.client.db("notehub");
19 | this.notes = new Note(this.db);
20 | //return all data in the collections
21 |
22 | //console.log(await this.db.collection("notes").find({}));
23 | }
24 | }
25 |
26 | module.exports = new MongoBot();
27 |
--------------------------------------------------------------------------------
/server/src/models/note.repository.js:
--------------------------------------------------------------------------------
1 | class Note {
2 | constructor(db) {
3 | this.collection = db.collection("notes");
4 | }
5 |
6 | async upsert(noteId, note) {
7 | const result = await this.collection.updateOne(
8 | { _id: noteId },
9 | { $set: note },
10 | { upsert: true }
11 | );
12 | return result;
13 | }
14 |
15 | async findOne(noteId) {
16 | const note = await this.collection.findOne({ _id: noteId });
17 | return note;
18 | }
19 | }
20 |
21 | //You can only access methods of this class from mongo.utils.
22 | module.exports = Note;
23 |
--------------------------------------------------------------------------------
/server/src/routes/api.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const userRouter = require("./user/user.router");
3 | const commRouter = require("./community/community.router");
4 | const noteRouter = require("./note/note.router");
5 | const { checkIfAuthenticated } = require("./firebase/firebase.middleware");
6 |
7 | const apiRouter = express.Router();
8 |
9 | //apiRouter.use(checkIfAuthenticated);
10 | apiRouter.use("/user", checkIfAuthenticated, userRouter);
11 |
12 | apiRouter.use("/community", checkIfAuthenticated, commRouter);
13 |
14 | apiRouter.use("/note", checkIfAuthenticated, noteRouter);
15 | module.exports = apiRouter;
16 |
--------------------------------------------------------------------------------
/server/src/routes/community/community.controller.js:
--------------------------------------------------------------------------------
1 | const {
2 | insertCommunity,
3 | selectCommunitiesByUserId,
4 | selectCommunityByCommunityId,
5 | searchCommunityByName,
6 | updateCommunityByCommunityId,
7 | insertCommunityNote,
8 | top10NotesByCommunityId,
9 | } = require("../../models/community.sql");
10 |
11 | const {
12 | insertMembership,
13 | selectMembersByCommunityId,
14 | deleteMembership,
15 | alterMembershipRole,
16 | } = require("../../models/membership.sql");
17 |
18 | const { httpSelectNotesByCommunityId } = require("../note/note.controller");
19 |
20 | async function httpInsertCommunity(req, res) {
21 | const newInfo = req.body;
22 | newInfo.ownerId = req.userId;
23 | newInfo.createdAt = Date.now();
24 |
25 | if (!newInfo.name || !newInfo.description || !newInfo.photo) {
26 | return res.status(400).json({ error: "Missing info" });
27 | }
28 |
29 | try {
30 | return res.status(201).json(await insertCommunity(newInfo));
31 | } catch (e) {
32 | return res.status(400).json({ error: e.message });
33 | }
34 | }
35 |
36 | async function httpSelectCommunitiesByUserId(req, res) {
37 | try {
38 | return res.status(200).json(await selectCommunitiesByUserId(req.userId));
39 | } catch (e) {
40 | return res.status(400).json({ error: e.message });
41 | }
42 | }
43 |
44 | async function httpSelectCommunityByCommunityId(req, res) {
45 | try {
46 | return res.status(200).json(await selectCommunityByCommunityId(req.body.communityId));
47 | } catch (e) {
48 | return res.status(400).json({ error: e.message });
49 | }
50 | }
51 |
52 | async function httpSearchCommunityByName(req, res) {
53 | try {
54 | return res.status(200).json(await searchCommunityByName(req.body.name));
55 | } catch (e) {
56 | return res.status(400).json({ error: e.message });
57 | }
58 | }
59 |
60 | async function httpUpdateCommunity(req, res) {
61 | const newComm = req.body;
62 | if (newComm.ownerId != req.userId) {
63 | return res.status(400).json({
64 | error: "You are not the owner, you cannot edit the community info.",
65 | });
66 | }
67 |
68 | try {
69 | return res.status(200).json(await updateCommunityByCommunityId(newComm));
70 | } catch (e) {
71 | return res.status(400).json({ error: e.message });
72 | }
73 | }
74 |
75 | /* Below is Community function */
76 | async function httpInsertMembership(req, res) {
77 | const newInfo = req.body;
78 | try {
79 | return res.status(201).json(await insertMembership(newInfo));
80 | } catch (e) {
81 | return res.status(400).json({ error: e.message });
82 | }
83 | }
84 |
85 | async function httpSelectMembersByCommunityId(req, res) {
86 | try {
87 | return res.status(200).json(await selectMembersByCommunityId(req.body.communityId));
88 | } catch (e) {
89 | return res.status(400).json({ error: e.message });
90 | }
91 | }
92 |
93 | async function httpDeleteMembership(req, res) {
94 | try {
95 | return res.status(204).json(await deleteMembership(req.body));
96 | } catch (e) {
97 | return res.status(400).json({ error: e.message });
98 | }
99 | }
100 | async function httpAlterMembershipRole(req, res) {
101 | const info = req.body;
102 | try {
103 | return res.status(200).json(await alterMembershipRole(info));
104 | } catch (e) {
105 | return res.status(400).json({ error: e.message });
106 | }
107 | }
108 |
109 | async function httpInsertCommunityNote(req, res) {
110 | const newInfo = req.body;
111 | try {
112 | return res.status(200).json(await insertCommunityNote(newInfo));
113 | } catch (e) {
114 | return res.status(400).json({ error: e.message });
115 | }
116 | }
117 |
118 | async function httpGetTop10Notes(req, res) {
119 | try {
120 | return res.status(200).json(await top10NotesByCommunityId(req.body.communityId));
121 | } catch (e) {
122 | return res.status(400).json({ error: e.message });
123 | }
124 | }
125 |
126 | module.exports = {
127 | httpInsertCommunity,
128 | httpSelectCommunitiesByUserId,
129 | httpSelectCommunityByCommunityId,
130 | httpSearchCommunityByName,
131 | httpUpdateCommunity,
132 | httpSelectMembersByCommunityId,
133 | httpInsertMembership,
134 | httpDeleteMembership,
135 | httpAlterMembershipRole,
136 | httpInsertCommunityNote,
137 | httpGetTop10Notes,
138 | };
139 |
--------------------------------------------------------------------------------
/server/src/routes/community/community.router.js:
--------------------------------------------------------------------------------
1 | const {
2 | httpInsertCommunity,
3 | httpSelectCommunitiesByUserId,
4 | httpSelectCommunityByCommunityId,
5 | httpSearchCommunityByName,
6 | httpUpdateCommunity,
7 | httpSelectMembersByCommunityId,
8 | httpInsertMembership,
9 | httpDeleteMembership,
10 | httpAlterMembershipRole,
11 | httpInsertCommunityNote,
12 | httpGetTop10Notes,
13 | } = require("./community.controller");
14 |
15 | const express = require("express");
16 |
17 | const commRouter = express.Router();
18 |
19 | commRouter.post("/insert-community", httpInsertCommunity);
20 |
21 | commRouter.get("/get-all-communities", httpSelectCommunitiesByUserId);
22 |
23 | commRouter.post(
24 | "/get-community-by-community-id",
25 | httpSelectCommunityByCommunityId
26 | );
27 |
28 | commRouter.post("/search-community-by-name", httpSearchCommunityByName);
29 |
30 | commRouter.post("/update-community", httpUpdateCommunity);
31 |
32 | commRouter.post("/get-members-by-community-id", httpSelectMembersByCommunityId);
33 |
34 | commRouter.post("/insert-membership", httpInsertMembership);
35 |
36 | commRouter.post("/delete-membership", httpDeleteMembership);
37 |
38 | commRouter.post("/alter-membership", httpAlterMembershipRole);
39 |
40 | commRouter.post("/add-note-to-community", httpInsertCommunityNote);
41 |
42 | commRouter.post("/get-top-10-notes", httpGetTop10Notes);
43 | module.exports = commRouter;
44 |
--------------------------------------------------------------------------------
/server/src/routes/firebase/.gitignore:
--------------------------------------------------------------------------------
1 | serviceAccount.json
--------------------------------------------------------------------------------
/server/src/routes/firebase/firebase.initialize.js:
--------------------------------------------------------------------------------
1 | var admin = require("firebase-admin");
2 |
3 | var serviceAccount = require("./serviceAccount.json");
4 |
5 | admin.initializeApp({
6 | credential: admin.credential.cert(serviceAccount),
7 | });
8 |
9 | module.exports = admin;
10 |
--------------------------------------------------------------------------------
/server/src/routes/firebase/firebase.middleware.js:
--------------------------------------------------------------------------------
1 | const admin = require("./firebase.initialize");
2 |
3 | function getAuthToken(req, res, next) {
4 | if (
5 | req.headers.authorization &&
6 | req.headers.authorization.split(" ")[0] === "Bearer"
7 | ) {
8 | req.authToken = req.headers.authorization.split(" ")[1];
9 | } else {
10 | req.authToken = null;
11 | }
12 | next();
13 | }
14 |
15 | function checkIfAuthenticated(req, res, next) {
16 | getAuthToken(req, res, async () => {
17 | try {
18 |
19 | const { authToken } = req;
20 | const userInfo = await admin.auth().verifyIdToken(authToken);
21 | req.userId = userInfo.uid;
22 | req.email = userInfo.email;
23 | console.log(`\n${req.email}, ${req.userId} is being authenticated\n`);
24 | next();
25 | } catch (e) {
26 | console.log(e.message);
27 | return res
28 | .status(401)
29 | .send({ error: "You are not authorized to make this request" });
30 | }
31 | });
32 | }
33 |
34 | // function checkIfCurrentUser(req, email) {
35 | // console.log("currentUser:", req.email);
36 | // console.log("requestUser:", email);
37 | // if (email != req.email) {
38 | // return false;
39 | // }
40 | // return true;
41 | // }
42 |
43 | module.exports = { checkIfAuthenticated };
44 |
--------------------------------------------------------------------------------
/server/src/routes/firebase/firebase.utils.js:
--------------------------------------------------------------------------------
1 | const admin = require("./firebase.initialize");
2 |
3 | function getAuthTokenFromHeader(req) {
4 | if (
5 | req.headers.authorization &&
6 | req.headers.authorization.split(" ")[0] === "Bearer"
7 | ) {
8 | return req.headers.authorization.split(" ")[1];
9 | } else {
10 | return null;
11 | }
12 | }
13 |
14 | const getUserInfoFromFirebase = async (authToken) => {
15 | try {
16 | return await admin.auth().verifyIdToken(authToken);
17 | } catch (error) {
18 | console.log(error);
19 | return { email: "Invalid" };
20 | }
21 | };
22 |
23 | module.exports = { getAuthTokenFromHeader, getUserInfoFromFirebase };
24 |
--------------------------------------------------------------------------------
/server/src/routes/note/note.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | httpInsertNote,
4 | httpSelectUserNotes,
5 | httpTransferOwnership,
6 | httpSelectAllAccessorsByNoteId,
7 | httpSelectNoteByNoteId,
8 | httpAlterNoteCommunity,
9 | httpAlterNoteCategories,
10 | httpAlterNoteAccess,
11 | httpSelectNoteAccessByNoteIdAndUserId,
12 | httpSelectAllCategories,
13 | httpUpdateNoteByNoteId,
14 | httpSelectNotesByCommunityId,
15 | httpSelectCommentsByNoteId,
16 | httpInsertComment,
17 | httpLikeNote,
18 | httpUpdateCommentContent,
19 | } = require("./note.controller");
20 |
21 | const noteRouter = express.Router();
22 |
23 | noteRouter.post("/insert-note", httpInsertNote);
24 |
25 | noteRouter.get("/get-user-notes", httpSelectUserNotes);
26 |
27 | noteRouter.post("/transfer-note-ownership", httpTransferOwnership);
28 |
29 | noteRouter.post("/get-all-accessors", httpSelectAllAccessorsByNoteId);
30 |
31 | noteRouter.post("/get-note-by-noteId", httpSelectNoteByNoteId);
32 |
33 | noteRouter.post("/alter-note-community", httpAlterNoteCommunity);
34 |
35 | noteRouter.post("/alter-note-categories", httpAlterNoteCategories);
36 |
37 | noteRouter.post("/alter-note-access", httpAlterNoteAccess);
38 |
39 | noteRouter.post("/get-note-access-by-noteId-userId", httpSelectNoteAccessByNoteIdAndUserId);
40 |
41 | noteRouter.get("/get-all-categories", httpSelectAllCategories);
42 |
43 | noteRouter.post("/update-note", httpUpdateNoteByNoteId);
44 |
45 | noteRouter.post("/get-notes-by-communityId", httpSelectNotesByCommunityId);
46 |
47 | noteRouter.post("/get-comments-by-noteId", httpSelectCommentsByNoteId);
48 |
49 | noteRouter.post("/:noteId/insert-comment", httpInsertComment);
50 |
51 | noteRouter.post("/update-comment-content", httpUpdateCommentContent);
52 |
53 | noteRouter.get("/like-note/:userId/:noteId", httpLikeNote);
54 |
55 | module.exports = noteRouter;
56 |
--------------------------------------------------------------------------------
/server/src/routes/user/user.controller.js:
--------------------------------------------------------------------------------
1 | const {
2 | selectAllUser,
3 | selectUserByEmail,
4 | insertUser,
5 | updateUserByEmail,
6 | deleteUserByEmail,
7 | selectNoteProvidersById,
8 | selectUserByuserId,
9 | searchUserByKeyword,
10 | selectTopUsers,
11 | getUserLevel,
12 | getStatistics
13 | } = require("../../models/user.sql");
14 |
15 | const { checkIfAuthenticated } = require("../firebase/firebase.middleware");
16 |
17 | // async function httpSelectAllUsers(req, res) {
18 | // return res.status(200).json(await selectAllUser());
19 | // }
20 |
21 | async function httpSelectUserByToken(req, res) {
22 | if (!req.email) {
23 | return res.status(200).json({ error: "Missing email" });
24 | }
25 | try {
26 | return res.status(200).json(await selectUserByEmail(req.email));
27 | } catch (e) {
28 | return res.status(400).json({ error: e.message });
29 | }
30 | }
31 |
32 | async function httpInsertUser(req, res) {
33 | const newUser = req.body;
34 | if (!newUser.firstName || !newUser.lastName || !newUser.email) {
35 | return res.status(400).json({ error: "Missing info" });
36 | }
37 | if (!newUser.subtitle) {
38 | newUser.subtitle = "New user";
39 | }
40 | newUser.userId = req.userId;
41 | try {
42 | return res.status(201).json(await insertUser(newUser));
43 | } catch (e) {
44 | return res.status(400).json({ error: e.message });
45 | }
46 | }
47 |
48 | async function httpUpdateUserByEmail(req, res) {
49 | const userInfo = req.body;
50 | userInfo.email = req.email;
51 | if (!userInfo.email) {
52 | return res.status(400).json({ error: "Missing email" });
53 | }
54 | try {
55 | return res.status(201).json(await updateUserByEmail(userInfo));
56 | } catch (e) {
57 | return res.status(400).json({ error: e.message });
58 | }
59 | }
60 |
61 | async function httpDeleteUserByEmail(req, res) {
62 | /* TODO: Code here needs to check if this request is allowed!*/
63 |
64 | const userInfo = req.body;
65 | userInfo.email = req.email;
66 | try {
67 | return res.status(204).json(await deleteUserByEmail(userInfo));
68 | } catch (e) {
69 | return res.status(400).json({ error: e.message });
70 | }
71 | ``;
72 | }
73 |
74 | async function httpSelectNoteProviders(req, res) {
75 | try {
76 | return res.status(200).json(await selectNoteProvidersById(req.userId));
77 | } catch (e) {
78 | return res.status(400).json({ error: e.message });
79 | }
80 | }
81 |
82 | async function httpSearchUserByKeyword(req, res) {
83 | try {
84 | return res.status(200).json(await searchUserByKeyword(req.body.keyword));
85 | } catch (e) {
86 | return res.status(400).json({ error: e.message });
87 | }
88 | }
89 |
90 | async function httpSelectTopUsers(req, res) {
91 | try {
92 | return res.status(200).json(await selectTopUsers());
93 | } catch (e) {
94 | return res.status(400).json({ error: e.message });
95 | }
96 | }
97 |
98 | async function httpGetUserLevel(req, res) {
99 | try {
100 | return res.status(200).json(await getUserLevel(req.params.userId));
101 | } catch (e) {
102 | return res.status(400).json({ error: e.message });
103 | }
104 | }
105 |
106 | async function httpGetNoteHubStats(req, res) {
107 | try {
108 | return res.status(200).json(await getStatistics());
109 | } catch (e) {
110 | return res.status(400).json({ error: e.message });
111 | }
112 | }
113 |
114 | module.exports = {
115 | httpInsertUser,
116 | httpUpdateUserByEmail,
117 | httpSelectUserByToken,
118 | httpDeleteUserByEmail,
119 | httpSelectNoteProviders,
120 | httpSearchUserByKeyword,
121 | httpSelectTopUsers,
122 | httpGetUserLevel,
123 | httpGetNoteHubStats,
124 | };
125 |
--------------------------------------------------------------------------------
/server/src/routes/user/user.router.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | httpInsertUser,
4 | httpUpdateUserByEmail,
5 | httpSelectUserByToken,
6 | httpDeleteUserByEmail,
7 | httpSelectNoteProviders,
8 | httpSearchUserByKeyword,
9 | httpSelectTopUsers,
10 | httpGetUserLevel,
11 | httpGetNoteHubStats,
12 | } = require("./user.controller");
13 |
14 | const userRouter = express.Router();
15 |
16 | userRouter.post("/insert-user", httpInsertUser);
17 |
18 | userRouter.put("/update-user", httpUpdateUserByEmail);
19 |
20 | userRouter.get("/get-user-by-token", httpSelectUserByToken);
21 |
22 | userRouter.delete("/delete-user-by-email", httpDeleteUserByEmail);
23 |
24 | //userRouter.get("/get-all-users", httpSelectAllUsers);
25 |
26 | userRouter.get("/get-note-providers", httpSelectNoteProviders);
27 |
28 | userRouter.post("/search-user-by-keyword", httpSearchUserByKeyword);
29 |
30 | userRouter.get("/get-top-10-users", httpSelectTopUsers);
31 |
32 | userRouter.get("/:userId/get-level", httpGetUserLevel);
33 |
34 | userRouter.get("/get-notehub-statistics", httpGetNoteHubStats);
35 |
36 | module.exports = userRouter;
37 |
--------------------------------------------------------------------------------
/server/src/server.js:
--------------------------------------------------------------------------------
1 | const http = require("http");
2 | const expressWebsockets = require("express-ws");
3 | const { Server } = require("@hocuspocus/server");
4 | const hooks = require("./hocuspocus-hooks/hooks");
5 | const { app: expressServer } = require("./app");
6 | const { RocksDB } = require("@hocuspocus/extension-rocksdb");
7 | const mongo = require("./models/mongo.utils");
8 | const PORT = process.env.PORT || 8000;
9 | const https = require("https");
10 | const fs = require("fs");
11 | const path = require("path");
12 | // Configure hocuspocus
13 | const server = Server.configure({
14 | //port: 1234,
15 | ...hooks,
16 | extensions: [
17 | new RocksDB({
18 | path: "./database",
19 | options: {
20 | // This option is only a example. See here for a full list:
21 | // https://www.npmjs.com/package/leveldown#options
22 | createIfMissing: true,
23 | },
24 | }),
25 | ],
26 | // Move all hooks to the hooks.js file
27 | // async onConnect(data) {
28 | // ...
29 | // }
30 | });
31 |
32 | // Initialize a HTTPS server
33 |
34 | if (process.env.ON_SERVER === "true") {
35 | var options = {
36 | key: fs.readFileSync(path.join(__dirname, "..", "private.key")),
37 | cert: fs.readFileSync(path.join(__dirname, "..", "key.crt")),
38 | };
39 | var httpsServer = https.createServer(options, expressServer);
40 | httpsServer.listen(443);
41 | var { app } = expressWebsockets(expressServer, httpsServer);
42 | } else {
43 | var { app } = expressWebsockets(expressServer);
44 | }
45 | app.get("/test123", (request, response) => {
46 | response.send("Hello World!");
47 | });
48 |
49 | app.ws("/websocket/note/:noteId", (websocket, request) => {
50 | console.log("requesting upgrade to websocket");
51 | server.handleConnection(websocket, request, request.params.noteId, {});
52 | });
53 |
54 | const start = async () => {
55 | await mongo.init();
56 | app.listen(PORT, () => {
57 | console.log(`Listening to ${PORT}`);
58 | });
59 | };
60 |
61 | start();
62 |
--------------------------------------------------------------------------------
/server/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "doc",
3 | "content": [
4 | {
5 | "type": "heading",
6 | "attrs": {
7 | "textAlign": "left",
8 | "level": 2
9 | },
10 | "content": [
11 | {
12 | "type": "text",
13 | "text": "Test Success"
14 | }
15 | ]
16 | },
17 | {
18 | "type": "paragraph",
19 | "attrs": {
20 | "textAlign": "left"
21 | }
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------