This commit is contained in:
louiscklaw
2025-02-01 02:02:22 +08:00
commit cb49efbeca
213 changed files with 144159 additions and 0 deletions

View 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

Binary file not shown.

282
assignment/doc/TEST.md Normal file
View 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. 1](./fig1.png)
Fig. 2
![Fig. 2](./fig2.png)
Fig. 3
![Fig. 3](./fig3.png)
Fig. 4
![Fig. 4](./fig4.png)
Fig. 5
![Fig. 5](./fig5.png)
Fig. 6
![Fig. 6](./fig6.png)
-----
## TEST1
### After a user (e.g., Andy) has logged in, the sketch of the page is in Fig. 2.
-The users icon,
-user name and a
-logout button are displayed on the top.
-A list of this users 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 notes `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 notes `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. 5s 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 users `_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 nodes `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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
assignment/doc/fig6.png (Stored with Git LFS) Normal file

Binary file not shown.

View 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
View 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 users
icon, user name and a logout button are displayed on the top. A list of this users
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 notes 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 notes 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. 5s 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 users _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
nodes 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
View 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
View 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
View 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
View File

223
assignment/src/QUESTION.md Normal file
View 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 users
icon, user name and a logout button are displayed on the top. A list of this users
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 notes 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 notes 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. 5s 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 users _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
nodes 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
View File

@@ -0,0 +1,2 @@
# LIBRARY USED
- https://fontawesome.com/icons/user?s=solid&f=classic

View 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);

View 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);
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -ex
yarn -d
yarn dev

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

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

Binary file not shown.

View 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)

View 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;
}

View 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;
}

View 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 users _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;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Basic Example
A simple [create-react-app](CRA-README.md) setup, showcasing one of the lastest React-Bootstrap components!

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -ex
yarn
yarn start

36985
assignment/src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
]
}
}

View 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>

View 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;
}

View 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 notes 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;

View 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);
});

BIN
assignment/src/frontend/src/assets/notes.png (Stored with Git LFS) Normal file

Binary file not shown.

View 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,
};

View 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"));

View File

@@ -0,0 +1,4 @@
npm i -d
npm run start

File diff suppressed because it is too large Load Diff

22
assignment/src/gen_db.js Normal file
View 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],
});
}
}
});