update,
This commit is contained in:
35
assignment/build_delivery.sh
Normal file
35
assignment/build_delivery.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
rm -rf delivery.zip
|
||||
|
||||
rm -rf delivery _temp/*
|
||||
|
||||
mkdir -p _temp/NoteService/routes
|
||||
mkdir -p _temp/noteapp
|
||||
|
||||
# backend (NoteService)
|
||||
cp src/backend/routes/notes.js _temp/NoteService/routes/notes.js
|
||||
cp src/backend/app.js _temp/NoteService/app.js
|
||||
|
||||
# frontend (noteapp)
|
||||
cp src/frontend/src/App.js _temp/noteapp/App.js
|
||||
cp src/frontend/src/App.css _temp/noteapp/App.css
|
||||
cp src/frontend/src/constants.js _temp/noteapp/constants.js
|
||||
cp src/frontend/src/index.js _temp/noteapp/index.js
|
||||
|
||||
pushd _temp
|
||||
7za a -tzip ../delivery.zip NoteService/* noteapp/*
|
||||
popd
|
||||
|
||||
7za a -tzip source.zip src
|
||||
|
||||
|
||||
# 7za a -tzip delivery.zip _temp/NoteService/* _temp/noteapp/*
|
||||
# 7za a -tzip -ppassword source.zip src/
|
||||
|
||||
# mv _temp/delivery.zip delivery
|
||||
# mv _temp/source.zip delivery
|
||||
# rm -rf _temp
|
||||
rm -rf delivery _temp/*
|
BIN
assignment/doc/Ass2 V2.pdf
Normal file
BIN
assignment/doc/Ass2 V2.pdf
Normal file
Binary file not shown.
282
assignment/doc/TEST.md
Normal file
282
assignment/doc/TEST.md
Normal file
@@ -0,0 +1,282 @@
|
||||
In this assignment, we are going to develop a simple single-page iNotes application using the MERN stack (MongoDB, Express.JS, ReactJS, and Node.js). The main workflow of the iNotes
|
||||
application is as follows.
|
||||
|
||||
|
||||
Upon loading, the sketch of the page is shown in Fig. 1:
|
||||
|
||||
Fig. 1
|
||||

|
||||
|
||||
Fig. 2
|
||||

|
||||
|
||||
Fig. 3
|
||||

|
||||
|
||||
Fig. 4
|
||||

|
||||
|
||||
Fig. 5
|
||||

|
||||
|
||||
Fig. 6
|
||||

|
||||
|
||||
|
||||
-----
|
||||
|
||||
## TEST1
|
||||
|
||||
### After a user (e.g., Andy) has logged in, the sketch of the page is in Fig. 2.
|
||||
- ✔️The user’s icon,
|
||||
- ✔️user name and a
|
||||
- ✔️logout button are displayed on the top.
|
||||
- ✔️A list of this user’s notes are shown on the left panel (node `title` only) together with a
|
||||
- ✔️search bar, and the
|
||||
- ✔️right panel is empty except an icon indicating the creation of a new note
|
||||
- (i.e., the `New note icon`).
|
||||
- ✔️The node titles in the left panel should be listed in reverse chronological order of the last saved time of the notes.
|
||||
- ✔️The total number of notes should be given in the ( ) on the top of the list.
|
||||
|
||||
-----
|
||||
|
||||
✔️❌📖📄
|
||||
|
||||
## TEST2
|
||||
|
||||
### After one clicks on the `New note icon` (on any page view where it is shown),
|
||||
- ✔️A new node creation panel shows in the right panel (Fig. 3).
|
||||
- ✔️There is a node `title` input field and a note `content` input field, into which the user can enter texts.
|
||||
- ✔️There is a `Cancel` button, clicking which a confirmation box `Are you sure to quit editing the note?` will be popped up:
|
||||
- ✔️if the user confirms quitting, the page view goes back to the one shown in Fig. 2;
|
||||
- ✔️otherwise, the current page view remains.
|
||||
- ✔️There is a `Save` button, clicking which the newly created note is shown on the right panel,
|
||||
- ✔️with the `Last saved` time and a `Delete` button displayed on top of the note, as shown in Fig. 4;
|
||||
- ✔️The note `title` should be listed in the left panel, as the first in the list (as it is the latest),
|
||||
- ✔️The note `title` should be highlighted in a different color than the rest of the note titles in the list (since this note’s `content` is shown in the right panel),
|
||||
- ✔️and the total number of notes in ( ) on top of the list should be incremented.
|
||||
|
||||
Fig. 3 Fig. 4
|
||||
|
||||
-----
|
||||
|
||||
## TEST3
|
||||
|
||||
### At any time, when one clicks on one note `title` in the left panel, the note’s `content` should be displayed in the right panel
|
||||
- ✔️as shown in Fig. 5 (which is in fact the same page view as Fig. 4),
|
||||
- and the node `title` in the left panel should be highlighted.
|
||||
- On the page view (i.e., Fig. 4 or Fig. 5’s page view), if one clicks into the note `title` input field or note `content` input field,
|
||||
- ✔️the page view changes to the one in Fig. 6, with a `Cancel` button and a `Save` button (indicating a note editing mode).
|
||||
- ✔️When `Cancel` is clicked, a confirmation box `Are you sure to quit editing the note?` will be popped up:
|
||||
- ✔️if the user confirms quitting, the page goes back to the previous note view (Fig. 4 or Fig. 5);
|
||||
- otherwise, the current page view remains.
|
||||
- ✔️When `Save` is clicked, a page view as in Fig. 4 or Fig. 5 is shown, except that the `Last saved` time on the top of the right panel should be the updated latest note saved time.
|
||||
|
||||
Fig. 5 Fig. 6
|
||||
|
||||
-----
|
||||
|
||||
### TEST4
|
||||
|
||||
### One can input a search string into the search bar at the top of the left panel and press `Enter` on the keyboard.
|
||||
- ✔️Then only the notes whose `title` or `content` contains the search string will be listed in the left panel,
|
||||
- ✔️ordered in reserve chronological order of their last saved time, and the number in ( ) shows the number of matched notes.
|
||||
- On a page view as in Fig. 3, 4, 5 or 6,
|
||||
- ✔️the search does not influence the display in the right panel,
|
||||
- and if the note whose details are displayed in the right panel matches the search,
|
||||
- its `title` in the searched list in the left panel should be highlighted.
|
||||
- For a search on a page view as in Fig. 6,
|
||||
- ✔️last saved `title` and `content` of the note that is being edited are used for matching the search;
|
||||
- ✔️when the note is saved, if its `title` and `content` do not match the search, it will not be displayed in the searched list in the left panel.
|
||||
|
||||
-----
|
||||
|
||||
### TEST5
|
||||
|
||||
### On a page view as in Fig. 4 or Fig. 5, after one clicks the `Delete` button,
|
||||
- ✔️a confirmation box pops up showing `Confirm to delete this note?`
|
||||
- ✔️If the user confirms the deletion, the note information will be removed from both the left and right panels.
|
||||
- ✔️In the left panel, the total note number will be decremented;
|
||||
- ✔️the right panel will show no note information as in Fig. 2.
|
||||
- ✔️If the user cancels the deletion, the page view remains unchanged.
|
||||
|
||||
-----
|
||||
|
||||
### TEST6
|
||||
|
||||
### When one clicks the `log out` button on a page view as in Fig. 2, 4 or 5,
|
||||
- ✔️the page view directly goes back to Fig. 1.
|
||||
- When one clicks the `log out` button on a page view as in Fig. 3 or 6,
|
||||
- ✔️an alert box `Are you sure to quit editing the note and log out?` will be popped up:
|
||||
- ✔️if the user confirms quitting, the page view goes back to Fig.1;
|
||||
- ✔️ otherwise, the current page view remains.
|
||||
|
||||
We are going to achieve this web application by implementing code in a backend Express app
|
||||
and a frontend React app.
|
||||
• Express app:
|
||||
app.js
|
||||
./routes/notes.js
|
||||
• React app:
|
||||
./src/App.js
|
||||
./src/index.js
|
||||
./src/App.css
|
||||
|
||||
# Task 1. Backend Web Service
|
||||
We implement the backend web service logic using Express.js. The web service is accessed at `http://localhost:3001/xx`.
|
||||
|
||||
### Preparations
|
||||
|
||||
1. Following steps in setup_nodejs_runtime_and_examples_1.pdf, create an Express project named NoteService.
|
||||
1. Following steps in AJAX_JSON_MongoDB_setup_and_examples.pdf, run MongoDB server, and create a database `assignment2` in the database server.
|
||||
1. Insert a few user records to a userList collection in the database in the format as follows.
|
||||
|
||||
We will assume that user names are all different in this application. `db.userList.insert({'name': 'Andy', 'password': '123456', 'icon': 'icons/andy.jpg'})`. Create a folder `icons` under the public folder in your Express project directory (NoteService). Copy a few icon images to the icons folder. For implementation simplicity,
|
||||
|
||||
we do not store icon images in MongoDB.
|
||||
|
||||
Instead, we store them in the harddisk under the NoteService/public/icons/ folder, and store the path of an icon in the userList collection only, using which we can identify the icon image in the icons folder.
|
||||
|
||||
Insert a number of records to a `noteList` collection in the database in the format as follows, each corresponding to one note in the app. Here userId should be the value of `_id` of the record in the userList collection, corresponding to the user who added the note.
|
||||
|
||||
```
|
||||
db.noteList.insert(
|
||||
{ 'userId': 'xxx',
|
||||
'lastsavedtime': '20:12:10 Tue Nov 15 2022',
|
||||
'title': 'assigment2',
|
||||
'content': 'an iNotes app based on react'
|
||||
})
|
||||
```
|
||||
|
||||
Implement backend web service logic (NoteService/app.js, NoteService/routes/notes.js)
|
||||
|
||||
|
||||
# Marking scheme
|
||||
### app.js (10 marks)
|
||||
|
||||
In app.js, load the router module implemented in ./routes/notes.js. Then add the middleware to specify that all requests for http://localhost:3001/ should be handled by this router.
|
||||
|
||||
Add necessary code for loading the MongoDB database you have created, creating an instance of the database, and passing the database instance for usage of all middlewares.
|
||||
|
||||
Also load any other modules and add other middlewares which you may need to implement
|
||||
this application.
|
||||
|
||||
We will let the server run on the port 3001 and launch the Express app using command
|
||||
`node app.js`.
|
||||
|
||||
### ./routes/notes.js (22 marks)
|
||||
In notes.js, implement the router module with middlewares to handle the following
|
||||
endpoints:
|
||||
|
||||
1. HTTP `POST` requests for http://localhost:3001/signin.
|
||||
- The middleware should parse the body of the HTTP `POST` request and extract the username and password carried in request body.
|
||||
- Then it checks whether the username and password match any record in the userList collection in the database.
|
||||
- If no, send `Login failure` in the body of the response message.
|
||||
- If yes, create a session variable `userId` and store this user’s `_id` in the session variable.
|
||||
- Retrieve name and icon of the current user (according to the value of the `userId` session variable), `_id`, `lastsavedtime` and `title` of all notes of the current user from the respective collections in the MongoDB database.
|
||||
- Send all retrieved information as a JSON string to the client if database operations are successful, and the error if failure.
|
||||
- You should decide the format of the JSON string and parse it accordingly in the front-end code to be implemented in Task 2.
|
||||
|
||||
1. HTTP `GET` requests for http://localhost:3001/logout.
|
||||
- The middleware should clear the `userId` session variable and send an empty string back to the user.
|
||||
|
||||
1. HTTP `GET` requests for http://localhost:3001/getnote?noteid=xx.
|
||||
- Retrieve `_id`, `lastsavedtime`, `title` and `content` of the note from the `noteList` collection based on the value of `nodeid` carried in the URL. Send retrieved information as a JSON string in the body of the response message if database operations are successful, and the error if failure.
|
||||
- You should decide the format of the JSON string to be included in the response body.
|
||||
|
||||
1. HTTP `POST` requests for http://localhost:3001/addnote.
|
||||
- The middleware should parse the body of the HTTP `POST` request and extract the note `title` and `content` carried in the request body.
|
||||
- Then it saves
|
||||
- the new note into the noteList collection together with the `_id` of the current user (based on the value of `userId` session variable)
|
||||
- and the current time on the server as the `lastsavedtime`.
|
||||
- if database operations are successful.
|
||||
- Return the `lastsavedtime` and `_id` of the note document in the nodeList collection to the client in JSON
|
||||
- and the error if failure
|
||||
|
||||
1. HTTP `PUT` requests for http://localhost:3001/savenote/:noteid
|
||||
|
||||
- The middleware should update the `lastsavedtime`, `title` and `content` of the note in the noteList collection based on
|
||||
- the `nodeid` carried in the URL,
|
||||
- the current time on the server and
|
||||
- the data contained in the body of the request message.
|
||||
- if success Return the `lastsavedtime` to the client .
|
||||
- and the error if failure
|
||||
|
||||
1. HTTP `GET` requests for http://localhost:3001/searchnotes?searchstr=xx.
|
||||
|
||||
- The middleware should find in the noteList collection all notes of the current user (based on the value of `userId` session variable) whose `title` or `content` contains the searchstr carried in the URL.
|
||||
- Send `_id`, `lastsavedtime` and `title` of those notes in JSON to the client if database operations are successful, and
|
||||
- error if failure.
|
||||
|
||||
1. HTTP `DELETE` requests for http://localhost:3001/deletenote/:noteid
|
||||
|
||||
- The middleware, should delete the note from the `noteList` collection according to the `noteid` carried in the URL.
|
||||
- Return an empty string to the client if success and
|
||||
- the error if failure
|
||||
|
||||
## Task 2
|
||||
### Front-end React App
|
||||
|
||||
Implement the front-end as a React application.
|
||||
The application is accessed at http://localhost:3000/.
|
||||
|
||||
### Preparations
|
||||
Following steps in React_I_examples.pdf, create a React app named noteapp and install the jQuery module in the React app.
|
||||
|
||||
Implement the React app (`noteapp/src/index.js`, `noteapp/src/App.js`, `noteapp/src/App.css`)
|
||||
|
||||
### index.js (3 marks)
|
||||
|
||||
Modify the generated `index.js` in the `./src` folder of your react app directory,
|
||||
|
||||
which should render the component you create in `App.js` in the `root` division in the default `index.html`, and remove any unnecessary code.
|
||||
|
||||
### App.js (50 marks)
|
||||
|
||||
`App.js` should import the jQuery module and link to the style sheet `App.css`.
|
||||
|
||||
Design and implement the component structure in `App.js`, such that the front-end page
|
||||
views and functionalities as illustrated in Figures 1-6 can be achieved.
|
||||
|
||||
### Hints:
|
||||
- You can use conditional rendering to decide if the page view in Fig. 1 or Fig. 2 should be rendered. Suppose the root component you are creating in App.js is iNoteApp. In iNoteApp, you may use a state variable to indicate if the user has logged in or not,
|
||||
and then render the component presenting the respective page view accordingly.
|
||||
|
||||
Initialize the state variable to indicate that no user has logged in, and update it when
|
||||
a user has successfully logged in and logged out, respectively.
|
||||
|
||||
- In the component implementing the page view as in Fig. 1, add an event handler for the onClick event on the `Sign in` button. When handling the event, send an HTTP `POST` request for http://localhost:3001/signin (refer to this website for AJAX cross-origin with cookies: http://promincproductions.com/blog/cross-domain-ajax-request-cookies-cors/).
|
||||
|
||||
- According to the response received from the backend service, remain on the page view and display the `Login failure` message at the top of the page, or render the page view as in Fig. 2. You can limit the number of characters in each note `title` in the left panel to a small fixed number n, i.e., only the first n characters of the note `title` is shown and the rest shown as `…`.
|
||||
|
||||
- When handling the `onClick` event on the `log out` button in a page view as in Figures
|
||||
2-6, send an HTTP `GET` request for http://localhost:3001/logout and handle the
|
||||
response accordingly.
|
||||
|
||||
- When handling the onClick event on a node `title` in the left panel of a page view as in Figures 2-6, send an HTTP `GET` request for http://localhost:3001/getnote?nodeid=xx, where xx should be the `_id` of the note that you store with the note `title` in the list. Then render a page view as in Fig. 5.
|
||||
|
||||
- When handling the onClick event on the `Save` button in a page view as in Fig. 3, send an HTTP `POST` request for http://localhost:3001/addnote carrying the new node’s `title` and `content` in the body of the request message, and handle the response accordingly.
|
||||
|
||||
- When handling the onClick event on the `Save` button in a page view as in Fig. 6, send an HTTP `PUT` request for http://localhost:3001/updatenote/xx where xx should be the `_id` of the note being updated, and handle the response accordingly.
|
||||
|
||||
- When handling the onClick event on the `Delete` button in a page view as in Fig. 4 or Fig. 5, send an HTTP `DELETE` request for http://localhost:3001/deletenote/xxx (where xxx is `_id` of the note to be deleted). If success response is received, update the page view accordingly.
|
||||
|
||||
- When handling the onKeyUp event (event.key == "Enter") on the search input box in a page view as in Figures 2-6, send an HTTP `GET` request for http://localhost:3001/searchnotes?searchstr=xxx (where xx is the input search string). When success response is received, update the page view accordingly.
|
||||
|
||||
### App.css (10 marks)
|
||||
|
||||
Style your page views nicely using CSS rules in App.css.
|
||||
|
||||
### Other marking criteria: (5 marks)
|
||||
- Good programming style (avoid redundant code, easy to understand and maintain).
|
||||
- You are encouraged to provide a readme.txt file to let us know more about your programs.
|
||||
|
||||
### Submission:
|
||||
You should zip the following files (in the indicated directory structure) into a yourstudentID-a2.zip file
|
||||
|
||||
```
|
||||
NoteService/app.js
|
||||
NoteService /routes/notes.js
|
||||
noteapp/src/App.js
|
||||
noteapp/src/index.js
|
||||
noteapp/src/App.css
|
||||
```
|
BIN
assignment/doc/fig1.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/doc/fig2.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig2.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/doc/fig3.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig3.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/doc/fig4.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig4.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/doc/fig5.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig5.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/doc/fig6.png
(Stored with Git LFS)
Normal file
BIN
assignment/doc/fig6.png
(Stored with Git LFS)
Normal file
Binary file not shown.
48
assignment/docker-compose.yml
Normal file
48
assignment/docker-compose.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
version: "3.1"
|
||||
|
||||
services:
|
||||
mongo:
|
||||
image: mongo
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:27017:27017
|
||||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8081:8081
|
||||
|
||||
backend:
|
||||
image: node:14-buster
|
||||
# user: 1000:1000
|
||||
volumes:
|
||||
- ./src/backend:/app
|
||||
- be_node_modules:/app/node_modules
|
||||
working_dir: /app
|
||||
ports:
|
||||
- 127.0.0.1:3001:3001
|
||||
- 127.0.0.1:9229:9229
|
||||
environment:
|
||||
- NODE_OPTIONS=--inspect=0.0.0.0:9229
|
||||
command: sleep infinity
|
||||
|
||||
frontend:
|
||||
image: node:14-buster
|
||||
# user: 1000:1000
|
||||
volumes:
|
||||
- ./src/frontend:/app
|
||||
- fe_node_modules:/app/node_modules
|
||||
working_dir: /app
|
||||
ports:
|
||||
- 3000:3000
|
||||
# - 9229:9229
|
||||
environment:
|
||||
# - NODE_OPTIONS=--inspect=0.0.0.0:9229
|
||||
- HELLO=WORLD
|
||||
# command: ./entry.sh
|
||||
command: sleep infinity
|
||||
|
||||
volumes:
|
||||
fe_node_modules:
|
||||
be_node_modules:
|
213
assignment/notes.md
Normal file
213
assignment/notes.md
Normal file
@@ -0,0 +1,213 @@
|
||||
|
||||
In this assignment, we are going to develop a simple single-page iNotes application using the
|
||||
MERN stack (MongoDB, Express.JS, ReactJS, and Node.js). The main workflow of the iNotes
|
||||
application is as follows.
|
||||
Ø Upon loading, the sketch of the page is shown in Fig. 1:
|
||||
Fig. 1
|
||||
Ø After a user (e.g., Andy) has logged in, the sketch of the page is in Fig. 2. The user’s
|
||||
icon, user name and a logout button are displayed on the top. A list of this user’s
|
||||
notes are shown on the left panel (node title only) together with a search bar, and
|
||||
the right panel is empty except an icon indicating the creation of a new note (i.e., the
|
||||
“New note icon”). The node titles in the left panel should be listed in reverse
|
||||
chronological order of the last saved time of the notes. The total number of notes
|
||||
should be given in the ( ) on the top of the list.
|
||||
Fig. 2
|
||||
Ø After one clicks on the “New note icon” (on any page view where it is shown), a new
|
||||
node creation panel shows in the right panel (Fig. 3). There is a node title input field
|
||||
and a note content input field, into which the user can enter texts. There is a
|
||||
“Cancel” button, clicking which a confirmation box “Are you sure to quit editing the
|
||||
note?” will be popped up: if the user confirms quitting, the page view goes back to
|
||||
the one shown in Fig. 2; otherwise, the current page view remains. There is a “Save”
|
||||
button, clicking which the newly created note is shown on the right panel, with the
|
||||
“Last saved” time and a “Delete” button displayed on top of the note, as shown in
|
||||
Fig. 4; besides, the note title should be listed in the left panel, as the first in the list
|
||||
(as it is the latest), the note title should be highlighted in a different color than the
|
||||
rest of the note titles in the list (since this note’s content is shown in the right panel),
|
||||
and the total number of notes in ( ) on top of the list should be incremented.
|
||||
Fig. 3 Fig. 4
|
||||
Ø At any time, when one clicks on one note title in the left panel, the note’s content
|
||||
should be displayed in the right panel, as shown in Fig. 5 (which is in fact the same
|
||||
page view as Fig. 4), and the node title in the left panel should be highlighted. On the
|
||||
page view (i.e., Fig. 4 or Fig. 5’s page view), if one clicks into the note title input field
|
||||
or note content input field, the page view changes to the one in Fig. 6, with a
|
||||
“Cancel” button and a “Save” button (indicating a note editing mode). When
|
||||
“Cancel” is clicked, a confirmation box “Are you sure to quit editing the note?” will
|
||||
be popped up: if the user confirms quitting, the page goes back to the previous note
|
||||
view (Fig. 4 or Fig. 5); otherwise, the current page view remains. When “Save” is
|
||||
clicked, a page view as in Fig. 4 or Fig. 5 is shown, except that the “Last saved” time
|
||||
on the top of the right panel should be the updated latest note saved time.
|
||||
Fig. 5 Fig. 6
|
||||
Ø One can input a search string into the search bar at the top of the left panel and
|
||||
press “Enter” on the keyboard. Then only the notes whose title or content contains
|
||||
the search string will be listed in the left panel, ordered in reserve chronological
|
||||
order of their last saved time, and the number in ( ) shows the number of matched
|
||||
notes. On a page view as in Fig. 3, 4, 5 or 6, the search does not influence the display
|
||||
in the right panel, and if the note whose details are displayed in the right panel
|
||||
matches the search, its title in the searched list in the left panel should be
|
||||
highlighted. For a search on a page view as in Fig. 6, last saved title and content of
|
||||
the note that is being edited are used for matching the search; when the note is
|
||||
saved, if its title and content do not match the search, it will not be displayed in the
|
||||
searched list in the left panel.
|
||||
Ø On a page view as in Fig. 4 or Fig. 5, after one clicks the “Delete” button, a
|
||||
confirmation box pops up showing “Confirm to delete this note?” If the user
|
||||
confirms the deletion, the note information will be removed from both the left and
|
||||
right panels. In the left panel, the total note number will be decremented; the right
|
||||
panel will show no note information as in Fig. 2. If the user cancels the deletion, the
|
||||
page view remains unchanged.
|
||||
Ø When one clicks the “log out” button on a page view as in Fig. 2, 4 or 5, the page
|
||||
view directly goes back to Fig. 1. When one clicks the “log out” button on a page
|
||||
view as in Fig. 3 or 6, an alert box “Are you sure to quit editing the note and log
|
||||
out?” will be popped up: if the user confirms quitting, the page view goes back to Fig.
|
||||
1; otherwise, the current page view remains.
|
||||
We are going to achieve this web application by implementing code in a backend Express app
|
||||
and a frontend React app.
|
||||
• Express app:
|
||||
app.js
|
||||
./routes/notes.js
|
||||
• React app:
|
||||
./src/App.js
|
||||
./src/index.js
|
||||
./src/App.css
|
||||
Task 1. Backend Web Service
|
||||
We implement the backend web service logic using Express.js. The web service is accessed at
|
||||
http://localhost:3001/xx.
|
||||
Preparations
|
||||
1. Following steps in setup_nodejs_runtime_and_examples_1.pdf, create an Express project
|
||||
named NoteService.
|
||||
2. Following steps in AJAX_JSON_MongoDB_setup_and_examples.pdf, run MongoDB server,
|
||||
and create a database “assignment2” in the database server.
|
||||
3. Insert a few user records to a userList collection in the database in the format as follows.
|
||||
We will assume that user names are all different in this application.
|
||||
db.userList.insert({'name': 'Andy', 'password': '123456', 'icon': 'icons/andy.jpg'})
|
||||
Create a folder “icons” under the public folder in your Express project directory
|
||||
(NoteService). Copy a few icon images to the icons folder. For implementation simplicity, we
|
||||
do not store icon images in MongoDB. Instead, we store them in the harddisk under the
|
||||
NoteService/public/icons/ folder, and store the path of an icon in the userList collection only,
|
||||
using which we can identify the icon image in the icons folder.
|
||||
Insert a number of records to a noteList collection in the database in the format as follows,
|
||||
each corresponding to one note in the app. Here userId should be the value of _id of the
|
||||
record in the userList collection, corresponding to the user who added the note.
|
||||
db.noteList.insert({'userId': 'xxx', 'lastsavedtime': '20:12:10 Tue Nov 15 2022', 'title':
|
||||
'assigment2', 'content': 'an iNotes app based on react'})
|
||||
Implement backend web service logic (NoteService/app.js,
|
||||
NoteService/routes/notes.js)
|
||||
app.js (10 marks)
|
||||
In app.js, load the router module implemented in ./routes/notes.js. Then add the
|
||||
middleware to specify that all requests for http://localhost:3001/ should be handled by this
|
||||
router.
|
||||
Add necessary code for loading the MongoDB database you have created, creating an
|
||||
instance of the database, and passing the database instance for usage of all middlewares.
|
||||
Also load any other modules and add other middlewares which you may need to implement
|
||||
this application.
|
||||
We will let the server run on the port 3001 and launch the Express app using command
|
||||
“node app.js”.
|
||||
./routes/notes.js (22 marks)
|
||||
In notes.js, implement the router module with middlewares to handle the following
|
||||
endpoints:
|
||||
1. HTTP POST requests for http://localhost:3001/signin. The middleware should parse the
|
||||
body of the HTTP POST request and extract the username and password carried in request
|
||||
body. Then it checks whether the username and password match any record in the userList
|
||||
collection in the database. If no, send “Login failure” in the body of the response message. If
|
||||
yes, create a session variable “userId” and store this user’s _id in the session variable.
|
||||
Retrieve name and icon of the current user (according to the value of the “userId” session
|
||||
variable), _id, lastsavedtime and title of all notes of the current user from the respective
|
||||
collections in the MongoDB database. Send all retrieved information as a JSON string to the
|
||||
client if database operations are successful, and the error if failure. You should decide the
|
||||
format of the JSON string and parse it accordingly in the front-end code to be implemented
|
||||
in Task 2.
|
||||
2. HTTP GET requests for http://localhost:3001/logout. The middleware should clear the
|
||||
“userId” session variable and send an empty string back to the user.
|
||||
3. HTTP GET requests for http://localhost:3001/getnote?noteid=xx. Retrieve _id,
|
||||
lastsavedtime, title and content of the note from the noteList collection based on the value
|
||||
of “nodeid” carried in the URL. Send retrieved information as a JSON string in the body of the
|
||||
response message if database operations are successful, and the error if failure. You should
|
||||
decide the format of the JSON string to be included in the response body.
|
||||
4. HTTP POST requests for http://localhost:3001/addnote. The middleware should parse the
|
||||
body of the HTTP POST request and extract the note title and content carried in the request
|
||||
body. Then it saves the new note into the noteList collection together with the _id of the
|
||||
current user (based on the value of “userId” session variable) and the current time on the
|
||||
server as the lastsavedtime. Return the lastsavedtime and _id of the note document in the
|
||||
nodeList collection to the client in JSON if database operations are successful, and the error
|
||||
if failure.
|
||||
5. HTTP PUT requests for http://localhost:3001/savenote/:noteid. The middleware should
|
||||
update the lastsavedtime, title and content of the note in the noteList collection based on
|
||||
the nodeid carried in the URL, the current time on the server and the data contained in the
|
||||
body of the request message. Return the lastsavedtime to the client if success and the error
|
||||
if failure.
|
||||
6. HTTP GET requests for http://localhost:3001/searchnotes?searchstr=xx. The middleware
|
||||
should find in the noteList collection all notes of the current user (based on the value of
|
||||
“userId” session variable) whose title or content contains the searchstr carried in the URL.
|
||||
Send _id, lastsavedtime and title of those notes in JSON to the client if database operations
|
||||
are successful, and the error if failure.
|
||||
7. HTTP DELETE requests for http://localhost:3001/deletenote/:noteid. The middleware,
|
||||
should delete the note from the noteList collection according to the noteid carried in the URL.
|
||||
Return an empty string to the client if success and the error if failure.
|
||||
Task 2 Front-end React App
|
||||
Implement the front-end as a React application. The application is accessed at
|
||||
http://localhost:3000/.
|
||||
Preparations
|
||||
Following steps in React_I_examples.pdf, create a React app named noteapp and install the
|
||||
jQuery module in the React app.
|
||||
Implement the React app (noteapp/src/index.js,
|
||||
noteapp/src/App.js, noteapp/src/App.css)
|
||||
index.js (3 marks)
|
||||
Modify the generated Index.js in the ./src folder of your react app directory, which should
|
||||
render the component you create in App.js in the “root” division in the default index.html,
|
||||
and remove any unnecessary code.
|
||||
App.js (50 marks)
|
||||
App.js should import the jQuery module and link to the style sheet App.css.
|
||||
Design and implement the component structure in App.js, such that the front-end page
|
||||
views and functionalities as illustrated in Figures 1-6 can be achieved.
|
||||
Hints:
|
||||
• You can use conditional rendering to decide if the page view in Fig. 1 or Fig. 2 should
|
||||
be rendered. Suppose the root component you are creating in App.js is iNoteApp. In
|
||||
iNoteApp, you may use a state variable to indicate if the user has logged in or not,
|
||||
and then render the component presenting the respective page view accordingly.
|
||||
Initialize the state variable to indicate that no user has logged in, and update it when
|
||||
a user has successfully logged in and logged out, respectively.
|
||||
• In the component implementing the page view as in Fig. 1, add an event handler for
|
||||
the onClick event on the “Sign in” button. When handling the event, send an HTTP
|
||||
POST request for http://localhost:3001/signin (refer to this website for AJAX
|
||||
cross-origin with cookies:
|
||||
http://promincproductions.com/blog/cross-domain-ajax-request-cookies-cors/).
|
||||
According to the response received from the backend service, remain on the page
|
||||
view and display the “Login failure” message at the top of the page, or render the
|
||||
page view as in Fig. 2. You can limit the number of characters in each note title in the
|
||||
left panel to a small fixed number n, i.e., only the first n characters of the note title is
|
||||
shown and the rest shown as “...”.
|
||||
• When handling the onClick event on the “log out” button in a page view as in Figures
|
||||
2-6, send an HTTP GET request for http://localhost:3001/logout and handle the
|
||||
response accordingly.
|
||||
• When handling the onClick event on a node title in the left panel of a page view as in
|
||||
Figures 2-6, send an HTTP GET request for http://localhost:3001/getnote?nodeid=xx,
|
||||
where xx should be the _id of the note that you store with the note title in the list.
|
||||
Then render a page view as in Fig. 5.
|
||||
• When handling the onClick event on the “Save” button in a page view as in Fig. 3,
|
||||
send an HTTP POST request for http://localhost:3001/addnote carrying the new
|
||||
node’s title and content in the body of the request message, and handle the
|
||||
response accordingly.
|
||||
• When handling the onClick event on the “Save” button in a page view as in Fig. 6,
|
||||
send an HTTP PUT request for http://localhost:3001/updatenote/xx where xx should
|
||||
be the _id of the note being updated, and handle the response accordingly.
|
||||
• When handling the onClick event on the “Delete” button in a page view as in Fig. 4
|
||||
or Fig. 5, send an HTTP DELETE request for http://localhost:3001/deletenote/xxx
|
||||
(where xxx is _id of the note to be deleted). If success response is received, update
|
||||
the page view accordingly.
|
||||
• When handling the onKeyUp event (event.key == "Enter") on the search input box in
|
||||
a page view as in Figures 2-6, send an HTTP GET request for
|
||||
http://localhost:3001/searchnotes?searchstr=xxx (where xx is the input search
|
||||
string). When success response is received, update the page view accordingly.
|
||||
App.css (10 marks)
|
||||
Style your page views nicely using CSS rules in App.css.
|
||||
Other marking criteria:
|
||||
(5 marks) Good programming style (avoid redundant code, easy to understand and
|
||||
maintain). You are encouraged to provide a readme.txt file to let us know more about your
|
||||
programs.
|
||||
Submission:
|
||||
You should zip the following files (in the indicated directory structure) into a
|
||||
yourstudentID-a2.zip file
|
||||
NoteService/app.js
|
||||
NoteService /routes/notes.js
|
||||
noteapp/src/App.js
|
||||
noteapp/src/index.js
|
||||
noteapp/src/App.css
|
29
assignment/package.json
Normal file
29
assignment/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "comp3322a-lab6",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"into_backend": "docker compose exec -it backend bash",
|
||||
"into_frontend": "docker compose exec -it frontend bash",
|
||||
"docker_rebuild": "docker compose up -d",
|
||||
"gitUpdate": "git add . && git commit -m'update 03_assignment,' && git push"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/louiscklaw/COMP3322A-lab6.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/louiscklaw/COMP3322A-lab6/issues"
|
||||
},
|
||||
"homepage": "https://github.com/louiscklaw/COMP3322A-lab6#readme",
|
||||
"dependencies": {}
|
||||
}
|
21
assignment/readme.md
Normal file
21
assignment/readme.md
Normal file
@@ -0,0 +1,21 @@
|
||||
### ASSIGNMENT2
|
||||
|
||||
### frontend
|
||||
http://localhost:3000/helloworld
|
||||
|
||||
|
||||
### backend
|
||||
http://localhost:3001/helloworld
|
||||
|
||||
|
||||
### demo
|
||||
https://youtube.com/shorts/GKkpaZy5H1A
|
||||
|
||||
|
||||
### delivery
|
||||
delivery.zip -> need to hand-in
|
||||
source.zip -> password(password) -> source for your reference
|
||||
|
||||
### reply
|
||||
delivery.zip -> need to hand-in
|
||||
source.zip -> password(password) -> source for your reference
|
144
assignment/src/.gitignore
vendored
Normal file
144
assignment/src/.gitignore
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
0
assignment/src/.gitkeep
Normal file
0
assignment/src/.gitkeep
Normal file
223
assignment/src/QUESTION.md
Normal file
223
assignment/src/QUESTION.md
Normal file
@@ -0,0 +1,223 @@
|
||||
In this assignment, we are going to develop a simple single-page iNotes application using the
|
||||
MERN stack (MongoDB, Express.JS, ReactJS, and Node.js). The main workflow of the iNotes
|
||||
application is as follows.
|
||||
|
||||
|
||||
Upon loading, the sketch of the page is shown in Fig. 1:
|
||||
Fig. 1
|
||||
|
||||
|
||||
After a user (e.g., Andy) has logged in, the sketch of the page is in Fig. 2. The user’s
|
||||
icon, user name and a logout button are displayed on the top. A list of this user’s
|
||||
notes are shown on the left panel (node title only) together with a search bar, and
|
||||
the right panel is empty except an icon indicating the creation of a new note (i.e., the
|
||||
“New note icon”). The node titles in the left panel should be listed in reverse
|
||||
chronological order of the last saved time of the notes. The total number of notes
|
||||
should be given in the ( ) on the top of the list.
|
||||
Fig. 2
|
||||
|
||||
After one clicks on the “New note icon” (on any page view where it is shown), a new
|
||||
node creation panel shows in the right panel (Fig. 3). There is a node title input field
|
||||
and a note content input field, into which the user can enter texts. There is a
|
||||
“Cancel” button, clicking which a confirmation box “Are you sure to quit editing the
|
||||
note?” will be popped up: if the user confirms quitting, the page view goes back to
|
||||
the one shown in Fig. 2; otherwise, the current page view remains. There is a “Save”
|
||||
button, clicking which the newly created note is shown on the right panel, with the
|
||||
“Last saved” time and a “Delete” button displayed on top of the note, as shown in
|
||||
Fig. 4; besides, the note title should be listed in the left panel, as the first in the list
|
||||
(as it is the latest), the note title should be highlighted in a different color than the
|
||||
rest of the note titles in the list (since this note’s content is shown in the right panel),
|
||||
and the total number of notes in ( ) on top of the list should be incremented.
|
||||
|
||||
Fig. 3 Fig. 4
|
||||
|
||||
At any time, when one clicks on one note title in the left panel, the note’s content
|
||||
should be displayed in the right panel, as shown in Fig. 5 (which is in fact the same
|
||||
page view as Fig. 4), and the node title in the left panel should be highlighted. On the
|
||||
page view (i.e., Fig. 4 or Fig. 5’s page view), if one clicks into the note title input field
|
||||
or note content input field, the page view changes to the one in Fig. 6, with a
|
||||
“Cancel” button and a “Save” button (indicating a note editing mode). When
|
||||
“Cancel” is clicked, a confirmation box “Are you sure to quit editing the note?” will
|
||||
be popped up: if the user confirms quitting, the page goes back to the previous note
|
||||
view (Fig. 4 or Fig. 5); otherwise, the current page view remains. When “Save” is
|
||||
clicked, a page view as in Fig. 4 or Fig. 5 is shown, except that the “Last saved” time
|
||||
on the top of the right panel should be the updated latest note saved time.
|
||||
|
||||
Fig. 5 Fig. 6
|
||||
|
||||
One can input a search string into the search bar at the top of the left panel and
|
||||
press “Enter” on the keyboard. Then only the notes whose title or content contains
|
||||
the search string will be listed in the left panel, ordered in reserve chronological
|
||||
order of their last saved time, and the number in ( ) shows the number of matched
|
||||
notes. On a page view as in Fig. 3, 4, 5 or 6, the search does not influence the display
|
||||
in the right panel, and if the note whose details are displayed in the right panel
|
||||
matches the search, its title in the searched list in the left panel should be
|
||||
highlighted. For a search on a page view as in Fig. 6, last saved title and content of
|
||||
the note that is being edited are used for matching the search; when the note is
|
||||
saved, if its title and content do not match the search, it will not be displayed in the
|
||||
searched list in the left panel.
|
||||
|
||||
On a page view as in Fig. 4 or Fig. 5, after one clicks the “Delete” button, a
|
||||
confirmation box pops up showing “Confirm to delete this note?” If the user
|
||||
confirms the deletion, the note information will be removed from both the left and
|
||||
right panels. In the left panel, the total note number will be decremented; the right
|
||||
panel will show no note information as in Fig. 2. If the user cancels the deletion, the
|
||||
page view remains unchanged.
|
||||
|
||||
When one clicks the “log out” button on a page view as in Fig. 2, 4 or 5, the page
|
||||
view directly goes back to Fig. 1. When one clicks the “log out” button on a page
|
||||
view as in Fig. 3 or 6, an alert box “Are you sure to quit editing the note and log
|
||||
out?” will be popped up: if the user confirms quitting, the page view goes back to Fig.
|
||||
1; otherwise, the current page view remains.
|
||||
We are going to achieve this web application by implementing code in a backend Express app
|
||||
and a frontend React app.
|
||||
• Express app:
|
||||
app.js
|
||||
./routes/notes.js
|
||||
• React app:
|
||||
./src/App.js
|
||||
./src/index.js
|
||||
./src/App.css
|
||||
Task 1. Backend Web Service
|
||||
We implement the backend web service logic using Express.js. The web service is accessed at
|
||||
http://localhost:3001/xx.
|
||||
Preparations
|
||||
1. Following steps in setup_nodejs_runtime_and_examples_1.pdf, create an Express project
|
||||
named NoteService.
|
||||
2. Following steps in AJAX_JSON_MongoDB_setup_and_examples.pdf, run MongoDB server,
|
||||
and create a database “assignment2” in the database server.
|
||||
3. Insert a few user records to a userList collection in the database in the format as follows.
|
||||
We will assume that user names are all different in this application.
|
||||
db.userList.insert({'name': 'Andy', 'password': '123456', 'icon': 'icons/andy.jpg'})
|
||||
Create a folder “icons” under the public folder in your Express project directory
|
||||
(NoteService). Copy a few icon images to the icons folder. For implementation simplicity, we
|
||||
do not store icon images in MongoDB. Instead, we store them in the harddisk under the
|
||||
NoteService/public/icons/ folder, and store the path of an icon in the userList collection only,
|
||||
using which we can identify the icon image in the icons folder.
|
||||
Insert a number of records to a noteList collection in the database in the format as follows,
|
||||
each corresponding to one note in the app. Here userId should be the value of _id of the
|
||||
record in the userList collection, corresponding to the user who added the note.
|
||||
db.noteList.insert({'userId': 'xxx', 'lastsavedtime': '20:12:10 Tue Nov 15 2022', 'title':
|
||||
'assigment2', 'content': 'an iNotes app based on react'})
|
||||
Implement backend web service logic (NoteService/app.js,
|
||||
NoteService/routes/notes.js)
|
||||
app.js (10 marks)
|
||||
In app.js, load the router module implemented in ./routes/notes.js. Then add the
|
||||
middleware to specify that all requests for http://localhost:3001/ should be handled by this
|
||||
router.
|
||||
Add necessary code for loading the MongoDB database you have created, creating an
|
||||
instance of the database, and passing the database instance for usage of all middlewares.
|
||||
Also load any other modules and add other middlewares which you may need to implement
|
||||
this application.
|
||||
We will let the server run on the port 3001 and launch the Express app using command
|
||||
“node app.js”.
|
||||
./routes/notes.js (22 marks)
|
||||
In notes.js, implement the router module with middlewares to handle the following
|
||||
endpoints:
|
||||
1. HTTP POST requests for http://localhost:3001/signin. The middleware should parse the
|
||||
body of the HTTP POST request and extract the username and password carried in request
|
||||
body. Then it checks whether the username and password match any record in the userList
|
||||
collection in the database. If no, send “Login failure” in the body of the response message. If
|
||||
yes, create a session variable “userId” and store this user’s _id in the session variable.
|
||||
Retrieve name and icon of the current user (according to the value of the “userId” session
|
||||
variable), _id, lastsavedtime and title of all notes of the current user from the respective
|
||||
collections in the MongoDB database. Send all retrieved information as a JSON string to the
|
||||
client if database operations are successful, and the error if failure. You should decide the
|
||||
format of the JSON string and parse it accordingly in the front-end code to be implemented
|
||||
in Task 2.
|
||||
2. HTTP GET requests for http://localhost:3001/logout. The middleware should clear the
|
||||
“userId” session variable and send an empty string back to the user.
|
||||
3. HTTP GET requests for http://localhost:3001/getnote?noteid=xx. Retrieve _id,
|
||||
lastsavedtime, title and content of the note from the noteList collection based on the value
|
||||
of “nodeid” carried in the URL. Send retrieved information as a JSON string in the body of the
|
||||
response message if database operations are successful, and the error if failure. You should
|
||||
decide the format of the JSON string to be included in the response body.
|
||||
4. HTTP POST requests for http://localhost:3001/addnote. The middleware should parse the
|
||||
body of the HTTP POST request and extract the note title and content carried in the request
|
||||
body. Then it saves the new note into the noteList collection together with the _id of the
|
||||
current user (based on the value of “userId” session variable) and the current time on the
|
||||
server as the lastsavedtime. Return the lastsavedtime and _id of the note document in the
|
||||
nodeList collection to the client in JSON if database operations are successful, and the error
|
||||
if failure.
|
||||
5. HTTP PUT requests for http://localhost:3001/savenote/:noteid. The middleware should
|
||||
update the lastsavedtime, title and content of the note in the noteList collection based on
|
||||
the nodeid carried in the URL, the current time on the server and the data contained in the
|
||||
body of the request message. Return the lastsavedtime to the client if success and the error
|
||||
if failure.
|
||||
6. HTTP GET requests for http://localhost:3001/searchnotes?searchstr=xx. The middleware
|
||||
should find in the noteList collection all notes of the current user (based on the value of
|
||||
“userId” session variable) whose title or content contains the searchstr carried in the URL.
|
||||
Send _id, lastsavedtime and title of those notes in JSON to the client if database operations
|
||||
are successful, and the error if failure.
|
||||
7. HTTP DELETE requests for http://localhost:3001/deletenote/:noteid. The middleware,
|
||||
should delete the note from the noteList collection according to the noteid carried in the URL.
|
||||
Return an empty string to the client if success and the error if failure.
|
||||
Task 2 Front-end React App
|
||||
Implement the front-end as a React application. The application is accessed at
|
||||
http://localhost:3000/.
|
||||
Preparations
|
||||
Following steps in React_I_examples.pdf, create a React app named noteapp and install the
|
||||
jQuery module in the React app.
|
||||
Implement the React app (noteapp/src/index.js,
|
||||
noteapp/src/App.js, noteapp/src/App.css)
|
||||
index.js (3 marks)
|
||||
Modify the generated Index.js in the ./src folder of your react app directory, which should
|
||||
render the component you create in App.js in the “root” division in the default index.html,
|
||||
and remove any unnecessary code.
|
||||
App.js (50 marks)
|
||||
App.js should import the jQuery module and link to the style sheet App.css.
|
||||
Design and implement the component structure in App.js, such that the front-end page
|
||||
views and functionalities as illustrated in Figures 1-6 can be achieved.
|
||||
Hints:
|
||||
• You can use conditional rendering to decide if the page view in Fig. 1 or Fig. 2 should
|
||||
be rendered. Suppose the root component you are creating in App.js is iNoteApp. In
|
||||
iNoteApp, you may use a state variable to indicate if the user has logged in or not,
|
||||
and then render the component presenting the respective page view accordingly.
|
||||
Initialize the state variable to indicate that no user has logged in, and update it when
|
||||
a user has successfully logged in and logged out, respectively.
|
||||
• In the component implementing the page view as in Fig. 1, add an event handler for
|
||||
the onClick event on the “Sign in” button. When handling the event, send an HTTP
|
||||
POST request for http://localhost:3001/signin (refer to this website for AJAX
|
||||
cross-origin with cookies:
|
||||
http://promincproductions.com/blog/cross-domain-ajax-request-cookies-cors/).
|
||||
According to the response received from the backend service, remain on the page
|
||||
view and display the “Login failure” message at the top of the page, or render the
|
||||
page view as in Fig. 2. You can limit the number of characters in each note title in the
|
||||
left panel to a small fixed number n, i.e., only the first n characters of the note title is
|
||||
shown and the rest shown as “…”.
|
||||
• When handling the onClick event on the “log out” button in a page view as in Figures
|
||||
2-6, send an HTTP GET request for http://localhost:3001/logout and handle the
|
||||
response accordingly.
|
||||
• When handling the onClick event on a node title in the left panel of a page view as in
|
||||
Figures 2-6, send an HTTP GET request for http://localhost:3001/getnote?nodeid=xx,
|
||||
where xx should be the _id of the note that you store with the note title in the list.
|
||||
Then render a page view as in Fig. 5.
|
||||
• When handling the onClick event on the “Save” button in a page view as in Fig. 3,
|
||||
send an HTTP POST request for http://localhost:3001/addnote carrying the new
|
||||
node’s title and content in the body of the request message, and handle the
|
||||
response accordingly.
|
||||
• When handling the onClick event on the “Save” button in a page view as in Fig. 6,
|
||||
send an HTTP PUT request for http://localhost:3001/updatenote/xx where xx should
|
||||
be the _id of the note being updated, and handle the response accordingly.
|
||||
• When handling the onClick event on the “Delete” button in a page view as in Fig. 4
|
||||
or Fig. 5, send an HTTP DELETE request for http://localhost:3001/deletenote/xxx
|
||||
(where xxx is _id of the note to be deleted). If success response is received, update
|
||||
the page view accordingly.
|
||||
• When handling the onKeyUp event (event.key == "Enter") on the search input box in
|
||||
a page view as in Figures 2-6, send an HTTP GET request for
|
||||
http://localhost:3001/searchnotes?searchstr=xxx (where xx is the input search
|
||||
string). When success response is received, update the page view accordingly.
|
||||
App.css (10 marks)
|
||||
Style your page views nicely using CSS rules in App.css.
|
||||
Other marking criteria:
|
||||
(5 marks) Good programming style (avoid redundant code, easy to understand and
|
||||
maintain). You are encouraged to provide a readme.txt file to let us know more about your
|
||||
programs.
|
||||
Submission:
|
||||
You should zip the following files (in the indicated directory structure) into a
|
||||
yourstudentID-a2.zip file
|
||||
NoteService/app.js
|
||||
NoteService /routes/notes.js
|
||||
noteapp/src/App.js
|
||||
noteapp/src/index.js
|
||||
noteapp/src/App.css
|
2
assignment/src/README.md
Normal file
2
assignment/src/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# LIBRARY USED
|
||||
- https://fontawesome.com/icons/user?s=solid&f=classic
|
65
assignment/src/backend/app.js
Normal file
65
assignment/src/backend/app.js
Normal file
@@ -0,0 +1,65 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var logger = require('morgan');
|
||||
var session = require("express-session");
|
||||
|
||||
var cors = require("cors");
|
||||
|
||||
var MONGO_HOST = "mongo";
|
||||
// var MONGO_HOST = "127.0.0.1";
|
||||
|
||||
// use assignment2
|
||||
var monk = require("monk");
|
||||
var db = monk(`${MONGO_HOST}:27017/assignment2`);
|
||||
|
||||
var notesRouter = require("./routes/notes");
|
||||
|
||||
var app = express();
|
||||
app.use(
|
||||
cors({
|
||||
origin: "http://localhost:3000",
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200,
|
||||
})
|
||||
);
|
||||
|
||||
// config express-session
|
||||
app.use(
|
||||
session({
|
||||
secret: "keyboard cat",
|
||||
cookie: { userId: [] },
|
||||
})
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// Make our db accessible to routers
|
||||
app.use(function (req, res, next) {
|
||||
req.db = db;
|
||||
next();
|
||||
});
|
||||
|
||||
app.options("*", cors());
|
||||
app.use("/", notesRouter);
|
||||
|
||||
// for requests not matching the above routes, create 404 error and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
// set locals, only providing error in development environment
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
// module.exports = app;
|
||||
app.listen(3001);
|
90
assignment/src/backend/bin/www
Normal file
90
assignment/src/backend/bin/www
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('app:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
8
assignment/src/backend/entry.sh
Normal file
8
assignment/src/backend/entry.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
|
||||
yarn -d
|
||||
|
||||
yarn dev
|
53
assignment/src/backend/gen_db.js
Normal file
53
assignment/src/backend/gen_db.js
Normal file
@@ -0,0 +1,53 @@
|
||||
var MongoClient = require("mongodb").MongoClient;
|
||||
|
||||
var topic_name = ["www", "html", "css", "javascript", "nodejs", "jquery", "louiscklaw1", "louiscklaw2", "louiscklaw3"];
|
||||
var topic_status = ["no", "no", "no", "no", "no", "no", "no", "no", "no"];
|
||||
var topic_hour = [2, 4, 4, 6, 10, 6, 1, 2, 3];
|
||||
|
||||
MongoClient.connect("mongodb://mongo:27017/", function (err, result) {
|
||||
if (!err) {
|
||||
console.log("We are connected");
|
||||
var db = result.db("assignment2");
|
||||
|
||||
// db.collection("topicList").remove({});
|
||||
|
||||
// for (let i = 0; i < topic_name.length; i++) {
|
||||
// db.collection("topicList").insert({
|
||||
// name: topic_name[i],
|
||||
// hour: topic_hour[i],
|
||||
// status: topic_status[i],
|
||||
// });
|
||||
// }
|
||||
|
||||
// for (let i = 0; i < 100; i++) {
|
||||
// db.collection("topicList").insert({
|
||||
// name: `topic_name_${i}`,
|
||||
// hour: `topic_hour${i}`,
|
||||
// status: i % 2 ? "yes" : "no",
|
||||
// });
|
||||
// }
|
||||
|
||||
db.collection("users").remove({});
|
||||
db.collection("users").insert({ _id: "u9999", name: `andy`, password: "123456", icon: "icons/andy.jpg" });
|
||||
for (let i = 0; i < 10; i++) {
|
||||
db.collection("users").insert({ _id: i, name: `Andy_${i}`, password: "123456", icon: "icons/andy.jpg" });
|
||||
}
|
||||
|
||||
db.collection("noteList").remove({});
|
||||
db.collection("noteList").insert({
|
||||
userId: "u9999",
|
||||
lastsavedtime: Date.now() + 1 * 10000,
|
||||
title: "assigment2",
|
||||
content: "an iNotes app based on react",
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
db.collection("noteList").insert({
|
||||
userId: "u9999",
|
||||
lastsavedtime: Date.now() + i * 10000,
|
||||
title: `assigment2_${i}`,
|
||||
content: `an iNotes app based on react_${i}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
1305
assignment/src/backend/package-lock.json
generated
Normal file
1305
assignment/src/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
assignment/src/backend/package.json
Normal file
23
assignment/src/backend/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start_old": "node ./bin/www",
|
||||
"start": "node app.js",
|
||||
"dev": "yarn nodemon"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"express-session": "^1.17.3",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"monk": "^7.3.4",
|
||||
"morgan": "~1.9.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"pug": "^3.0.2"
|
||||
}
|
||||
}
|
0
assignment/src/backend/public/icons/.gitkeep
Normal file
0
assignment/src/backend/public/icons/.gitkeep
Normal file
BIN
assignment/src/backend/public/icons/andy.jpg
(Stored with Git LFS)
Normal file
BIN
assignment/src/backend/public/icons/andy.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assignment/src/backend/public/images/logo.png
(Stored with Git LFS)
Normal file
BIN
assignment/src/backend/public/images/logo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
109
assignment/src/backend/public/javascripts/externalJS.js
Normal file
109
assignment/src/backend/public/javascripts/externalJS.js
Normal file
@@ -0,0 +1,109 @@
|
||||
$(document).ready(function() {
|
||||
showAllTopics()
|
||||
});
|
||||
|
||||
|
||||
// step 7.2
|
||||
function showAllTopics() {
|
||||
var table_content = `
|
||||
<tr>
|
||||
<th>Topic Name</th>
|
||||
<th>Study Hour</th>
|
||||
<th>Chosen Status</th>
|
||||
<th>Operation</th>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
$.getJSON("/users/get_table", function (data) {
|
||||
$.each(data, function () {
|
||||
// 1. for each row tag, add the attribute class=“highlight” if the status field of the topic is “yes”
|
||||
|
||||
// 2. the last <td> element contains an <a> element with text “add” or “remove”
|
||||
// (if status field equals to “yes”, text should be “remove”; otherwise, text should be “add”)
|
||||
|
||||
// 3. The table row representations should be all concatenated into the string table_content.
|
||||
table_content += `
|
||||
<tr ${this.status == "yes" ? 'class="highlight"' : ""}>
|
||||
<td>${this.name}</td>
|
||||
<td>${this.hour}</td>
|
||||
<td>${this.status}</td>
|
||||
<td>
|
||||
${
|
||||
this.status == "yes"
|
||||
? `<a href="#" class="operation" rel="${this._id}" >remove</a>`
|
||||
: `<a href="#" class="operation" rel="${this._id}" >add</a>`
|
||||
}
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
// Finally, use $(‘#plan_table’).html() to set HTML content of the table element of id “plan_table” to table_content.
|
||||
$("#plan_table").html(table_content);
|
||||
});
|
||||
}
|
||||
|
||||
// step 8.2
|
||||
function operateTopic(event) {
|
||||
// event.preventDefault() to prevent opening the link “#” when the hyperlink is clicked
|
||||
event.preventDefault();
|
||||
|
||||
// retrieve _id of the topic that you are going to add/remove from the ‘rel’ attribute using $(this).attr(‘rel’)
|
||||
var _id = $(this).attr("rel");
|
||||
|
||||
// use $.ajax() to send a HTTP PUT request for “/users/update_status” with JSON data
|
||||
// {_id: _id field retrieved, op: operation retrieved};
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: `/users/update_status`,
|
||||
data: {
|
||||
_id: _id,
|
||||
// retrieve the operation that your are going to perform using $(this).html().
|
||||
op: $(this).html(),
|
||||
},
|
||||
dataType: "JSON",
|
||||
}).done(function (response) {
|
||||
if (response.msg === "Successfully updated!") {
|
||||
// alert the response message and call showAllTopics() to refresh the topic table.
|
||||
alert("Successfully updated!");
|
||||
|
||||
// call showAllTopics() to refresh the topic table.
|
||||
showAllTopics();
|
||||
} else {
|
||||
alert(response.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#plan_table").on("click", ".operation", operateTopic);
|
||||
|
||||
|
||||
// step 9.2
|
||||
function deleteTopic(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var topic_name = $("#input_name").val();
|
||||
|
||||
// check if the topic_name is valid
|
||||
if ($(`td:contains("${topic_name}")`).length > 0) {
|
||||
// If the length of the list is not 0, use $.ajax() to send a HTTP DELETE request for “/users/delete_topic/:topic_name”;
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: `/users/delete_topic/${topic_name}`,
|
||||
}).done(function (res) {
|
||||
if (res.msg == "Successfully deleted!") {
|
||||
// success delete,
|
||||
// 1. call showAllTopics() to refresh the topic table.
|
||||
// 2. alert response message
|
||||
showAllTopics();
|
||||
alert(res.msg);
|
||||
}
|
||||
// clear after delete operation
|
||||
$("#input_name").val("");
|
||||
});
|
||||
} else {
|
||||
// alert the message “No such topic in the table!”
|
||||
alert("No such topic in the table!");
|
||||
}
|
||||
}
|
||||
|
||||
$("#submit_delete").on('click', deleteTopic)
|
159
assignment/src/backend/public/stylesheets/style.css
Normal file
159
assignment/src/backend/public/stylesheets/style.css
Normal file
@@ -0,0 +1,159 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #163a50;
|
||||
font: 300 100%/120% "Tahoma", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header, nav, .contents, footer {
|
||||
box-sizing: border-box;
|
||||
max-width: 1080px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header, nav, footer {
|
||||
background: #0f2736;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.contents {
|
||||
padding: 1em 2em 2.5em 2em;
|
||||
}
|
||||
|
||||
/* increase line spacing for main title */
|
||||
h1 {
|
||||
line-height: 120%;
|
||||
margin-top: 0.5em;
|
||||
float: left;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
header img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* change color and remove underline for links */
|
||||
footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#plan_table a {
|
||||
color: rgb(0, 47, 255);
|
||||
}
|
||||
|
||||
/* size and margin for logo image */
|
||||
header img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
/* style for footer */
|
||||
footer {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer div {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.contents h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
width: 5em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5em;
|
||||
height: 1.6em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#delete_div p{
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
header *, nav * {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.contents {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.6em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
header,nav {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
header img {
|
||||
display: none;
|
||||
}
|
||||
header h1 {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
nav div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.contents {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contents li{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* style for the table */
|
||||
table, th, td {
|
||||
border: 1.8px solid black;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
margin: auto;
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgb(178, 177, 177);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
#plan_table {
|
||||
margin-bottom: 2em;
|
||||
}
|
159
assignment/src/backend/public/stylesheets/style.js
Normal file
159
assignment/src/backend/public/stylesheets/style.js
Normal file
@@ -0,0 +1,159 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #163a50;
|
||||
font: 300 100%/120% "Tahoma", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
header {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header, nav, .contents, footer {
|
||||
box-sizing: border-box;
|
||||
max-width: 1080px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
header, nav, footer {
|
||||
background: #0f2736;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.contents {
|
||||
padding: 1em 2em 2.5em 2em;
|
||||
}
|
||||
|
||||
/* increase line spacing for main title */
|
||||
h1 {
|
||||
line-height: 120%;
|
||||
margin-top: 0.5em;
|
||||
float: left;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
header img {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* change color and remove underline for links */
|
||||
footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#plan_table a {
|
||||
color: rgb(0, 47, 255);
|
||||
}
|
||||
|
||||
/* size and margin for logo image */
|
||||
header img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
/* style for footer */
|
||||
footer {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer div {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.contents h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
width: 5em;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 5em;
|
||||
height: 1.6em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
#delete_div p{
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
header *, nav * {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.contents {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.6em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1080px) {
|
||||
header,nav {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
header img {
|
||||
display: none;
|
||||
}
|
||||
header h1 {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
nav div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.contents {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contents li{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* style for the table */
|
||||
table, th, td {
|
||||
border: 1.8px solid black;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
margin: auto;
|
||||
width: 400px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgb(178, 177, 177);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
#plan_table {
|
||||
margin-bottom: 2em;
|
||||
}
|
202
assignment/src/backend/routes/notes.js
Normal file
202
assignment/src/backend/routes/notes.js
Normal file
@@ -0,0 +1,202 @@
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
|
||||
const convertToHongKongTime = (date_now) => date_now + 8 * 3600 * 1000;
|
||||
|
||||
// EXPLAIN: http://localhost:3001/signin
|
||||
// EXPLAIN: 1. HTTP POST requests for http://localhost:3001/signin. The middleware should parse the
|
||||
// EXPLAIN: body of the HTTP POST request and extract the username and password carried in request
|
||||
// EXPLAIN: body. Then it checks whether the username and password match any record in the userList
|
||||
// EXPLAIN: collection in the database.
|
||||
// If no, send “Login failure” in the body of the response message.
|
||||
// If yes, create a session variable “userId” and store this user’s _id in the session variable.
|
||||
// EXPLAIN: Retrieve name and icon of the current user (according to the value of the “userId” session
|
||||
// EXPLAIN: variable), _id, lastsavedtime and title of all notes of the current user from the respective
|
||||
// EXPLAIN: collections in the MongoDB database. Send all retrieved information as a JSON string to the
|
||||
// EXPLAIN: client if database operations are successful, and the error if failure. You should decide the
|
||||
// EXPLAIN: format of the JSON string and parse it accordingly in the front-end code to be implemented
|
||||
// EXPLAIN: in Task 2.
|
||||
router.post("/signin", function (req, res, next) {
|
||||
var { db, params, body } = req;
|
||||
var collection = db.get("users");
|
||||
var { username: name, password } = body;
|
||||
|
||||
console.log({ name, password });
|
||||
|
||||
collection.find({ name, password }, {}, (err, docs) => {
|
||||
if (err) res.send({ msg: err });
|
||||
|
||||
if (docs.length > 0) {
|
||||
req.session.userId = docs[0]._id;
|
||||
res.send({ msg: "login success", user_meta: docs[0] });
|
||||
} else {
|
||||
res.send({ msg: "login failure" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// EXPLAIN: 2. HTTP GET requests for http://localhost:3001/logout.
|
||||
// EXPLAIN: The middleware should clear the
|
||||
// EXPLAIN: “userId” session variable and send an empty string back to the user.
|
||||
router.get("/logout", function (req, res, next) {
|
||||
// req.session.userId = "";
|
||||
try {
|
||||
delete req.session.userId;
|
||||
} catch (error) {
|
||||
console.log("error during logout");
|
||||
}
|
||||
|
||||
res.send({ msg: "logout success" });
|
||||
});
|
||||
|
||||
// EXPLAIN: 3. HTTP GET requests for http://localhost:3001/getnote?noteid=xx.
|
||||
// EXPLAIN: Retrieve _id, lastsavedtime, title and content of the note from the noteList collection based on
|
||||
// EXPLAIN: the value of “nodeid” carried in the URL.
|
||||
// EXPLAIN: Send retrieved information as a JSON string in the body of the
|
||||
// EXPLAIN: response message if database operations are successful, and the error if failure.
|
||||
// EXPLAIN: You should decide the format of the JSON string to be included in the response body.
|
||||
router.get("/getnote/:note_id", function (req, res, next) {
|
||||
var { db, params, body, session } = req;
|
||||
var { userId } = session;
|
||||
var notes_collection = db.get("noteList");
|
||||
var { note_id } = params;
|
||||
|
||||
console.log({ userId });
|
||||
|
||||
if (note_id == "*" || note_id == "") {
|
||||
// list notes
|
||||
notes_collection.find({ userId }, { sort: { lastsavedtime: -1 } }, (err, docs) => {
|
||||
if (err) res.send({ msg: err });
|
||||
|
||||
if (docs.length > 0) {
|
||||
res.send(docs);
|
||||
} else {
|
||||
res.send({ msg: "the wanted note not found" });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log({ note_id });
|
||||
notes_collection.find({ _id: note_id, userId: userId }, { sort: { lastsavedtime: -1 } }, (err, docs) => {
|
||||
if (err) res.send({ msg: err });
|
||||
|
||||
if (docs.length > 0) {
|
||||
res.send(docs);
|
||||
} else {
|
||||
res.send({ msg: "the wanted note not found" });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// EXPLAIN: 4. HTTP POST requests for http://localhost:3001/addnote.
|
||||
// EXPLAIN: The middleware should parse the body of the HTTP POST request
|
||||
// EXPLAIN: and extract the note title and content carried in the request body.
|
||||
// EXPLAIN: Then it saves the new note into the noteList collection together with the _id of the
|
||||
// EXPLAIN: current user (based on the value of “userId” session variable) and
|
||||
// EXPLAIN: the current time on the server as the lastsavedtime.
|
||||
|
||||
// EXPLAIN: Return the lastsavedtime and _id of the note document in the nodeList collection to the client in JSON
|
||||
// EXPLAIN: if database operations are successful, and the error if failure.
|
||||
router.post("/addnote", function (req, res, next) {
|
||||
var { db, params, body } = req;
|
||||
var { title, content } = body;
|
||||
var { userId } = req.session;
|
||||
|
||||
var notes_collection = db.get("noteList");
|
||||
var local_time = Date.now() + 8 * 3600 * 1000;
|
||||
|
||||
notes_collection.insert({ title, content, userId, lastsavedtime: convertToHongKongTime(Date.now()) });
|
||||
res.send({ msg: "add note success" });
|
||||
});
|
||||
|
||||
// EXPLAIN: 5. HTTP PUT requests for http://localhost:3001/savenote/:noteid.
|
||||
// EXPLAIN: The middleware should update the lastsavedtime, title and content of the note in the noteList collection
|
||||
// EXPLAIN: based on the nodeid carried in the URL,
|
||||
// EXPLAIN: the current time on the server and the data contained in the body of the request message.
|
||||
// EXPLAIN: Return the lastsavedtime to the client if success and the error if failure.
|
||||
router.put("/savenote/:noteid", (req, res) => {
|
||||
// var db = req.db;
|
||||
// var collection = db.get("noteList");
|
||||
var { db, body, params } = req;
|
||||
var notes_collection = db.get("noteList");
|
||||
var { noteid } = params;
|
||||
var { title, content } = body;
|
||||
|
||||
notes_collection.update(
|
||||
{ _id: noteid },
|
||||
{ $set: { title, content, lastsavedtime: convertToHongKongTime(Date.now()) } },
|
||||
function (err, result) {
|
||||
if (err) console.log({ msg: "error during update note" });
|
||||
// If the update operation is successful, send “Successfully updated!” to the client;
|
||||
// otherwise, send the error in the response.
|
||||
|
||||
res.send({ msg: "updated successfully" });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// EXPLAIN: 6. HTTP GET requests for http://localhost:3001/searchnotes?searchstr=xx.
|
||||
// EXPLAIN: The middleware should find in the noteList collection all notes of the current user (based on the value of
|
||||
// EXPLAIN: “userId” session variable) whose title or content contains the searchstr carried in the URL.
|
||||
// EXPLAIN: Send _id, lastsavedtime and title of those notes in JSON to the client if database operations
|
||||
// EXPLAIN: are successful, and the error if failure.
|
||||
router.get("/searchnotes/:searchstr", async (req, res, next) => {
|
||||
var { db, params } = req;
|
||||
var notes_collection = db.get("noteList");
|
||||
var { searchstr } = params;
|
||||
|
||||
notes_collection.find(
|
||||
{ $or: [{ title: searchstr }, { content: searchstr }] },
|
||||
{ sort: { lastsavedtime: -1 } },
|
||||
(err, docs) => {
|
||||
if (err) res.send({ msg: err });
|
||||
|
||||
if (docs.length > 0) {
|
||||
res.send(docs);
|
||||
} else {
|
||||
res.send({ msg: "the wanted note not found" });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// EXPLAIN: 7. HTTP DELETE requests for http://localhost:3001/deletenote/:noteid.
|
||||
// EXPLAIN: The middleware, should delete the note from the noteList collection according to the noteid carried in the URL.
|
||||
// EXPLAIN: Return an empty string to the client if success and the error if failure.
|
||||
router.delete("/deletenote/:noteid", (req, res) => {
|
||||
var { db, params, body } = req;
|
||||
var { noteid } = params;
|
||||
var notes_collection = db.get("noteList");
|
||||
|
||||
console.log(JSON.stringify({ noteid }));
|
||||
// // uses collection.remove({‘name’: req.params.name}) to remove the topic document from the topicList collection.
|
||||
notes_collection.remove({ _id: noteid }, function (err, result) {
|
||||
if (err) res.send({ msg: err.message });
|
||||
|
||||
// If the remove operation is successful, send the string “Successfully deleted!”
|
||||
// to the client side; otherwise, send the error in the response.
|
||||
res.send({ msg: "Successfully deleted!" });
|
||||
});
|
||||
});
|
||||
|
||||
// EXPLAIN: helloworld endpoint
|
||||
router.get("/helloworld", function (req, res, next) {
|
||||
var { db, params, body, session } = req;
|
||||
var notes_collection = db.get("noteList");
|
||||
|
||||
notes_collection.find(
|
||||
{ _id: "6399bc615d147202753ca3b3", userId: "u9999" },
|
||||
{ sort: { lastsavedtime: -1 } },
|
||||
(err, docs) => {
|
||||
if (err) res.send({ msg: err });
|
||||
|
||||
if (docs.length > 0) {
|
||||
res.send(docs);
|
||||
} else {
|
||||
res.send({ msg: "the wanted note not found" });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = router;
|
1467
assignment/src/backend/yarn.lock
Normal file
1467
assignment/src/backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
3
assignment/src/frontend/README.md
Normal file
3
assignment/src/frontend/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Basic Example
|
||||
|
||||
A simple [create-react-app](CRA-README.md) setup, showcasing one of the lastest React-Bootstrap components!
|
7
assignment/src/frontend/entry.sh
Normal file
7
assignment/src/frontend/entry.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
yarn
|
||||
|
||||
yarn start
|
36985
assignment/src/frontend/package-lock.json
generated
Normal file
36985
assignment/src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
assignment/src/frontend/package.json
Normal file
38
assignment/src/frontend/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "code-sandbox-examples",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"fontawesome-react": "^2.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
12
assignment/src/frontend/public/index.html
Normal file
12
assignment/src/frontend/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>React-Bootstrap CodeSandbox Starter</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
143
assignment/src/frontend/src/App.css
Normal file
143
assignment/src/frontend/src/App.css
Normal file
@@ -0,0 +1,143 @@
|
||||
.username-container{
|
||||
padding-left: 2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.list-group-container {
|
||||
width:80%;
|
||||
}
|
||||
|
||||
.viewnote-body{
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.login-input-container{
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding-left: 2rem
|
||||
}
|
||||
|
||||
.icon-button-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.icon-button-text{
|
||||
padding-left: 0.5rem;
|
||||
|
||||
}
|
||||
|
||||
.viewnote-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.viewnote-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-logout-container{
|
||||
display: block;
|
||||
position: fixed;
|
||||
margin-top: 33vh;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.logout-button-container{
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.note-container{
|
||||
width: 80vw;
|
||||
border-left: 2px solid black;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.add-note-button-container{
|
||||
position: fixed;
|
||||
top: 80vh;
|
||||
left: 90vw;
|
||||
}
|
||||
|
||||
.left-nav-bar-notes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left-nav-bar-container {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
height: calc( 100vh - 200px );
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
||||
}
|
||||
|
||||
.name-nav-bar {
|
||||
|
||||
}
|
||||
|
||||
.top-nav-bar {
|
||||
height: 100px;
|
||||
width: calc( 100vw - 0.001px );
|
||||
border-bottom: 2px solid black ;
|
||||
}
|
||||
|
||||
.home-background {
|
||||
width: 100vw;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.menu-input-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
654
assignment/src/frontend/src/App.js
Normal file
654
assignment/src/frontend/src/App.js
Normal file
@@ -0,0 +1,654 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Container from "react-bootstrap/Container";
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import InputGroup from "react-bootstrap/InputGroup";
|
||||
import ListGroup from "react-bootstrap/ListGroup";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUser, faPlus, faRemove, faSave, faSearch, faSignOut } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import CONSTANTS from "./constants";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
function ConfirmLogout({ setOpenConfirmLogout, setLoginState, fetchNotes, setViewState }) {
|
||||
const handleConfirmLogout = () => {
|
||||
fetch(`//${CONSTANTS.LOGOUT_URL}`)
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setLoginState(CONSTANTS.NOT_LOGGED_IN);
|
||||
setOpenConfirmLogout(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note and log out ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmLogout(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={(e) => {
|
||||
handleConfirmLogout(e);
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmCancelEdit({ setOpenConfirmCancelEdit, fetchNotes, setViewState }) {
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmCancelEdit(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
fetchNotes();
|
||||
setOpenConfirmCancelEdit(false);
|
||||
}}
|
||||
>
|
||||
cancel edit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmDeleteNote({ note_id, setOpenConfirmDeleteNote, fetchNotes, setViewState }) {
|
||||
const handleDeleteClick = () => {
|
||||
fetch(`//${CONSTANTS.DELETE_NOTE_ENDPOINT}/${note_id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
setOpenConfirmDeleteNote(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmDeleteNote(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={(e) => handleDeleteClick(e)}>
|
||||
Confirm delete
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ViewNote = ({ view_note_id, setViewState, setViewNoteId, first_note_id, fetchNotes }) => {
|
||||
var [note_detail, setNoteDetail] = useState("");
|
||||
var [iso_day_string, setIsoDayString] = useState("");
|
||||
var [open_confirm_delete_note, setOpenConfirmDeleteNote] = useState(false);
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setOpenConfirmDeleteNote(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/${view_note_id}`, { credentials: "include" })
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setNoteDetail(res_json[0]);
|
||||
if (res_json[0]?.lastsavedtime && res_json[0]?.lastsavedtime != "") {
|
||||
var d = new Date(res_json[0].lastsavedtime);
|
||||
setIsoDayString(d.toISOString().split(".")[0].replace("T", " "));
|
||||
} else {
|
||||
setIsoDayString("");
|
||||
}
|
||||
});
|
||||
}, [view_note_id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_delete_note ? (
|
||||
<ConfirmDeleteNote
|
||||
note_id={note_detail._id}
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmDeleteNote={setOpenConfirmDeleteNote}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div>Last saved: {iso_day_string || ""}</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleDeleteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div class="icon-button-text">Delete</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="note-title"
|
||||
aria-describedby="note-title"
|
||||
value={note_detail?.title || ""}
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EDIT_NOTE);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={10}
|
||||
value={note_detail?.content || ""}
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EDIT_NOTE);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// EXPLAIN: After one clicks on the “New note icon” (on any page view where it is shown),
|
||||
// EXPLAIN: a new node creation panel shows in the right panel (Fig. 3).
|
||||
// EXPLAIN: There is a node title input field and a note content input field, into which the user can enter texts.
|
||||
// EXPLAIN: There is a “Cancel” button, clicking which a confirmation box
|
||||
|
||||
// EXPLAIN: “Are you sure to quit editing the note?” will be popped up:
|
||||
// EXPLAIN: if the user confirms quitting, the page view goes back to the one shown in Fig. 2;
|
||||
// EXPLAIN: otherwise, the current page view remains.
|
||||
|
||||
// EXPLAIN: There is a “Save” button, clicking which the newly created note is shown on the right panel,
|
||||
// EXPLAIN: with the “Last saved” time and a “Delete” button displayed on top of the note,
|
||||
// EXPLAIN: as shown in Fig. 4; besides, the note title should be listed in the left panel,
|
||||
// EXPLAIN: as the first in the list (as it is the latest), the note title should be highlighted in a different color than the
|
||||
// EXPLAIN: rest of the note titles in the list (since this note’s content is shown in the right panel),
|
||||
// EXPLAIN: and the total number of notes in ( ) on top of the list should be incremented.
|
||||
|
||||
const NewNote = ({ fetchNotes, setViewState }) => {
|
||||
var [note_title, setNoteTitle] = useState("");
|
||||
var [note_content, setNoteContent] = useState("");
|
||||
var [open_confirm_cancel_edit, setOpenConfirmCancelEdit] = useState(false);
|
||||
|
||||
const handleSaveNoteClick = () => {
|
||||
fetch(`//${CONSTANTS.ADD_NEW_NOTE_ENDPOINT}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: note_title, content: note_content }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_cancel_edit ? (
|
||||
<ConfirmCancelEdit
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmCancelEdit={setOpenConfirmCancelEdit}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div class="viewnote-title">
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => setOpenConfirmCancelEdit(true)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faRemove} />
|
||||
</div>
|
||||
<div class="icon-button-text">Cancel</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleSaveNoteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div class="icon-button-text">Save</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Label htmlFor="note-title">Note title</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="new-note-title"
|
||||
aria-describedby="note-title"
|
||||
onChange={(e) => setNoteTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<div>
|
||||
<Form.Label htmlFor="new-note-content">Note Content</Form.Label>
|
||||
<Form.Control as="textarea" rows={10} onChange={(e) => setNoteContent(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditNote = ({ view_note_id, fetchNotes, setViewState }) => {
|
||||
var [note_title, setNoteTitle] = useState("");
|
||||
var [note_content, setNoteContent] = useState("");
|
||||
var [open_confirm_cancel_edit, setOpenConfirmCancelEdit] = useState(false);
|
||||
|
||||
var [note_detail, setNoteDetail] = useState("");
|
||||
|
||||
const handleSaveNoteClick = () => {
|
||||
fetch(`//${CONSTANTS.SAVE_NOTE_ENDPOINT}/${view_note_id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: note_title, content: note_content }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/${view_note_id}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setNoteTitle(res_json[0]?.title);
|
||||
setNoteContent(res_json[0]?.content);
|
||||
});
|
||||
}, [view_note_id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_cancel_edit ? (
|
||||
<ConfirmCancelEdit
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmCancelEdit={setOpenConfirmCancelEdit}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div class="viewnote-title">
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => setOpenConfirmCancelEdit(true)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faRemove} />
|
||||
</div>
|
||||
<div style={{ paddingLeft: "0.5rem" }}>Cancel</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleSaveNoteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div style={{ paddingLeft: "0.5rem" }}>Save</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Label htmlFor="note-title">Note title</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="new-note-title"
|
||||
aria-describedby="note-title"
|
||||
value={note_title}
|
||||
onChange={(e) => setNoteTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<div>
|
||||
<Form.Label htmlFor="new-note-content">Note Content</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={10}
|
||||
value={note_content}
|
||||
onChange={(e) => setNoteContent(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddNoteButton = ({ setViewNoteId, view_state, setViewState }) => {
|
||||
if ([CONSTANTS.VIEW_STATE_EMPTY_NOTE, CONSTANTS.VIEW_STATE_VIEW_NOTE].includes(view_state))
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-circle btn-lg"
|
||||
onClick={(e) => {
|
||||
setViewNoteId("");
|
||||
setViewState(CONSTANTS.VIEW_STATE_ADD_NOTE);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</button>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const Home = ({ user_info, setUserInfo, setLoginState }) => {
|
||||
var [note_list, setNoteList] = useState([]);
|
||||
var [is_loading, setIsLoading] = useState(true);
|
||||
var [search_note, setSearchNote] = useState("");
|
||||
var [view_state, setViewState] = useState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
var [view_note_id, setViewNoteId] = useState("");
|
||||
var [first_note_id, setFirstNoteId] = useState("");
|
||||
var [open_confirm_logout, setOpenConfirmLogout] = useState(false);
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
setOpenConfirmLogout(true);
|
||||
};
|
||||
|
||||
const fetchNotes = () => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/*`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setIsLoading(false);
|
||||
setNoteList(res_json);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetch(`//${CONSTANTS.SEARCH_URL}/${search_note}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setIsLoading(false);
|
||||
setNoteList(res_json);
|
||||
});
|
||||
}, [search_note]);
|
||||
|
||||
useEffect(() => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
fetchNotes();
|
||||
}, []);
|
||||
|
||||
if (is_loading) return <>loading...</>;
|
||||
|
||||
const EmptyNote = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const NoteContent = ({ view_state, view_note_id, setViewNoteId }) => {
|
||||
if (view_state == CONSTANTS.VIEW_STATE_VIEW_NOTE) {
|
||||
return (
|
||||
<ViewNote
|
||||
view_note_id={view_note_id}
|
||||
setViewNoteId={setViewNoteId}
|
||||
first_note_id={first_note_id}
|
||||
fetchNotes={fetchNotes}
|
||||
setViewState={setViewState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (view_state == CONSTANTS.VIEW_STATE_ADD_NOTE)
|
||||
return <NewNote setViewState={setViewState} fetchNotes={fetchNotes} />;
|
||||
if (view_state == CONSTANTS.VIEW_STATE_EDIT_NOTE)
|
||||
return <EditNote view_note_id={view_note_id} setViewState={setViewState} fetchNotes={fetchNotes} />;
|
||||
|
||||
return <EmptyNote />;
|
||||
};
|
||||
|
||||
if (note_list == []) return <>loading notes</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_logout ? (
|
||||
<ConfirmLogout setOpenConfirmLogout={setOpenConfirmLogout} setLoginState={setLoginState} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="home-container">
|
||||
<div class="home-background">iNotes</div>
|
||||
<div class="top-nav-bar">
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: "2rem",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="user-avatar"
|
||||
style={{ backgroundImage: `url("http://${CONSTANTS.BACKEND_HOST}/${user_info.icon}")` }}
|
||||
></div>
|
||||
<div class="username-container">{user_info.name}</div>
|
||||
</div>
|
||||
<div class="logout-button-container">
|
||||
<Button variant="primary" onClick={(e) => handleLogoutClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSignOut} />
|
||||
</div>
|
||||
<div class="icon-button-text">log out</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav-bar-container">
|
||||
<div style={{ width: "20vw", display: "flex", flexDirection: "column", paddingTop: "1rem" }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center", paddingTop: "2rem" }}>
|
||||
<div style={{ width: "80%", display: "flex" }}>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</InputGroup.Text>
|
||||
<Form.Control id="search-note" onChange={(e) => setSearchNote(e.target.value)} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav-bar-notes-container">
|
||||
<div style={{ fontSize: "2rem" }}>Notes ({note_list.length})</div>
|
||||
|
||||
{note_list.length < 1 ? (
|
||||
<>sorry but the list is empty</>
|
||||
) : (
|
||||
<>
|
||||
<ListGroup class="list-group-container">
|
||||
{note_list
|
||||
.filter((n) => {
|
||||
const re = new RegExp(search_note);
|
||||
return n.title?.search(re) > -1;
|
||||
})
|
||||
.map((n, idx) => (
|
||||
<ListGroup.Item
|
||||
key={`list_${idx}`}
|
||||
onClick={(e) => {
|
||||
setViewNoteId(n._id);
|
||||
setViewState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
}}
|
||||
style={{ backgroundColor: n._id == view_note_id ? "cyan" : "" }}
|
||||
>
|
||||
{n.title || "no title"}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-container">
|
||||
<NoteContent setViewNoteId={setViewNoteId} view_state={view_state} view_note_id={view_note_id} />
|
||||
</div>
|
||||
<div class="add-note-button-container">
|
||||
<AddNoteButton setViewNoteId={setViewNoteId} view_state={view_state} setViewState={setViewState} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Login = ({ login_state, setLoginState, setUserInfo }) => {
|
||||
var [debug, setDebug] = useState();
|
||||
var [username, setUsername] = useState("");
|
||||
var [password, setPassword] = useState("");
|
||||
|
||||
const helloworld_alert = (username, password) => {
|
||||
fetch("//localhost:3001/signin", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignIn = (username, password) => {
|
||||
fetch(`//${CONSTANTS.SIGN_IN_URL}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setDebug(JSON.stringify(res_json));
|
||||
if (res_json.msg == CONSTANTS.MSG_LOGIN_SUCCESS) {
|
||||
setLoginState(CONSTANTS.LOGGED_IN);
|
||||
setUserInfo(res_json.user_meta);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
<div class="login-background">
|
||||
<div style={{ fontSize: "3rem" }}>iNotes</div>
|
||||
<div style={{ paddingTop: "5rem" }}>
|
||||
<Form>
|
||||
<Form.Group className="mb-3">
|
||||
<div class="menu-input-field">
|
||||
<Form.Label>Username:</Form.Label>
|
||||
<div class="login-input-container">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<div class="menu-input-field">
|
||||
<Form.Label>Password:</Form.Label>
|
||||
<div class="login-input-container">
|
||||
<Form.Control
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</div>
|
||||
<div style={{ paddingTop: "5rem" }}>
|
||||
<Button variant="primary" onClick={(e) => handleSignIn(username, password)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</div>
|
||||
<div class="icon-button-text">Sign in</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
var [login_state, setLoginState] = useState(CONSTANTS.NOT_LOGGED_IN);
|
||||
var [user_info, setUserInfo] = useState({});
|
||||
|
||||
if (login_state == CONSTANTS.NOT_LOGGED_IN) {
|
||||
return <Login login_state={login_state} setLoginState={setLoginState} setUserInfo={setUserInfo} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Home user_info={user_info} setUserInfo={setUserInfo} setLoginState={setLoginState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
9
assignment/src/frontend/src/App.test.js
Normal file
9
assignment/src/frontend/src/App.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
0
assignment/src/frontend/src/assets/.gitkeep
Normal file
0
assignment/src/frontend/src/assets/.gitkeep
Normal file
BIN
assignment/src/frontend/src/assets/notes.png
(Stored with Git LFS)
Normal file
BIN
assignment/src/frontend/src/assets/notes.png
(Stored with Git LFS)
Normal file
Binary file not shown.
38
assignment/src/frontend/src/constants.js
Normal file
38
assignment/src/frontend/src/constants.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const LOGGED_IN = "logged_in";
|
||||
const NOT_LOGGED_IN = "not_logged_in";
|
||||
|
||||
const MSG_LOGIN_SUCCESS = "login success";
|
||||
|
||||
const BACKEND_HOST = "localhost:3001";
|
||||
|
||||
const VIEW_STATE_ADD_NOTE = "view_state_add_note";
|
||||
const VIEW_STATE_VIEW_NOTE = "view_state_view_note";
|
||||
const VIEW_STATE_EMPTY_NOTE = "view_state_empty_note";
|
||||
const VIEW_STATE_EDIT_NOTE = "view_state_edit_note";
|
||||
|
||||
// ENDPOINTS
|
||||
const ADD_NEW_NOTE_ENDPOINT = `${BACKEND_HOST}/addnote`;
|
||||
const GET_NOTE_ENDPOINT = `${BACKEND_HOST}/getnote`;
|
||||
const DELETE_NOTE_ENDPOINT = `${BACKEND_HOST}/deletenote`;
|
||||
const SAVE_NOTE_ENDPOINT = `${BACKEND_HOST}/savenote`;
|
||||
const LOGOUT_URL = `${BACKEND_HOST}/logout`;
|
||||
const SIGN_IN_URL = `${BACKEND_HOST}/signin`;
|
||||
const SEARCH_URL = `${BACKEND_HOST}/searchnotes`;
|
||||
|
||||
export default {
|
||||
ADD_NEW_NOTE_ENDPOINT,
|
||||
BACKEND_HOST,
|
||||
LOGGED_IN,
|
||||
MSG_LOGIN_SUCCESS,
|
||||
NOT_LOGGED_IN,
|
||||
VIEW_STATE_ADD_NOTE,
|
||||
VIEW_STATE_EMPTY_NOTE,
|
||||
VIEW_STATE_VIEW_NOTE,
|
||||
VIEW_STATE_EDIT_NOTE,
|
||||
GET_NOTE_ENDPOINT,
|
||||
DELETE_NOTE_ENDPOINT,
|
||||
SAVE_NOTE_ENDPOINT,
|
||||
LOGOUT_URL,
|
||||
SIGN_IN_URL,
|
||||
SEARCH_URL,
|
||||
};
|
8
assignment/src/frontend/src/index.js
Normal file
8
assignment/src/frontend/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
// Importing the Bootstrap CSS
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
4
assignment/src/frontend/test.bat
Normal file
4
assignment/src/frontend/test.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
npm i -d
|
||||
|
||||
npm run start
|
11340
assignment/src/frontend/yarn.lock
Normal file
11340
assignment/src/frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
22
assignment/src/gen_db.js
Normal file
22
assignment/src/gen_db.js
Normal file
@@ -0,0 +1,22 @@
|
||||
var MongoClient = require("mongodb").MongoClient;
|
||||
|
||||
var topic_name = ["www", "html", "css", "javascript", "nodejs", "jquery", "louiscklaw1", "louiscklaw2", "louiscklaw3"];
|
||||
var topic_status = ["no", "no", "no", "no", "no", "no", "no", "no", "no"];
|
||||
var topic_hour = [2, 4, 4, 6, 10, 6, 1, 2, 3];
|
||||
|
||||
MongoClient.connect("mongodb://mongo:27017/", function (err, result) {
|
||||
if (!err) {
|
||||
console.log("We are connected");
|
||||
var db = result.db("lab6-db");
|
||||
|
||||
db.collection("topicList").remove({});
|
||||
|
||||
for (let i = 0; i < topic_name.length; i++) {
|
||||
db.collection("topicList").insert({
|
||||
name: topic_name[i],
|
||||
hour: topic_hour[i],
|
||||
status: topic_status[i],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user