update,
This commit is contained in:
3
james_endl/assignment/src/frontend/README.md
Normal file
3
james_endl/assignment/src/frontend/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Basic Example
|
||||
|
||||
A simple [create-react-app](CRA-README.md) setup, showcasing one of the lastest React-Bootstrap components!
|
7
james_endl/assignment/src/frontend/entry.sh
Normal file
7
james_endl/assignment/src/frontend/entry.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
yarn
|
||||
|
||||
yarn start
|
36985
james_endl/assignment/src/frontend/package-lock.json
generated
Normal file
36985
james_endl/assignment/src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
james_endl/assignment/src/frontend/package.json
Normal file
38
james_endl/assignment/src/frontend/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "code-sandbox-examples",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"fontawesome-react": "^2.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^2.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
12
james_endl/assignment/src/frontend/public/index.html
Normal file
12
james_endl/assignment/src/frontend/public/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>React-Bootstrap CodeSandbox Starter</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
143
james_endl/assignment/src/frontend/src/App.css
Normal file
143
james_endl/assignment/src/frontend/src/App.css
Normal file
@@ -0,0 +1,143 @@
|
||||
.username-container{
|
||||
padding-left: 2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.list-group-container {
|
||||
width:80%;
|
||||
}
|
||||
|
||||
.viewnote-body{
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.login-input-container{
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding-left: 2rem
|
||||
}
|
||||
|
||||
.icon-button-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.icon-button-text{
|
||||
padding-left: 0.5rem;
|
||||
|
||||
}
|
||||
|
||||
.viewnote-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.viewnote-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-logout-container{
|
||||
display: block;
|
||||
position: fixed;
|
||||
margin-top: 33vh;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.logout-button-container{
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.note-container{
|
||||
width: 80vw;
|
||||
border-left: 2px solid black;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.add-note-button-container{
|
||||
position: fixed;
|
||||
top: 80vh;
|
||||
left: 90vw;
|
||||
}
|
||||
|
||||
.left-nav-bar-notes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.left-nav-bar-container {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
height: calc( 100vh - 200px );
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
||||
}
|
||||
|
||||
.name-nav-bar {
|
||||
|
||||
}
|
||||
|
||||
.top-nav-bar {
|
||||
height: 100px;
|
||||
width: calc( 100vw - 0.001px );
|
||||
border-bottom: 2px solid black ;
|
||||
}
|
||||
|
||||
.home-background {
|
||||
width: 100vw;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.menu-input-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
654
james_endl/assignment/src/frontend/src/App.js
Normal file
654
james_endl/assignment/src/frontend/src/App.js
Normal file
@@ -0,0 +1,654 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Container from "react-bootstrap/Container";
|
||||
import Form from "react-bootstrap/Form";
|
||||
|
||||
import InputGroup from "react-bootstrap/InputGroup";
|
||||
import ListGroup from "react-bootstrap/ListGroup";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUser, faPlus, faRemove, faSave, faSearch, faSignOut } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import CONSTANTS from "./constants";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
function ConfirmLogout({ setOpenConfirmLogout, setLoginState, fetchNotes, setViewState }) {
|
||||
const handleConfirmLogout = () => {
|
||||
fetch(`//${CONSTANTS.LOGOUT_URL}`)
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setLoginState(CONSTANTS.NOT_LOGGED_IN);
|
||||
setOpenConfirmLogout(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note and log out ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmLogout(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={(e) => {
|
||||
handleConfirmLogout(e);
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmCancelEdit({ setOpenConfirmCancelEdit, fetchNotes, setViewState }) {
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmCancelEdit(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
fetchNotes();
|
||||
setOpenConfirmCancelEdit(false);
|
||||
}}
|
||||
>
|
||||
cancel edit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmDeleteNote({ note_id, setOpenConfirmDeleteNote, fetchNotes, setViewState }) {
|
||||
const handleDeleteClick = () => {
|
||||
fetch(`//${CONSTANTS.DELETE_NOTE_ENDPOINT}/${note_id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
setOpenConfirmDeleteNote(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal show" style={{ display: "block", position: "fixed", marginTop: "33vh" }}>
|
||||
<Modal.Dialog>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Are you sure to quit editing the note ? </Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={(e) => setOpenConfirmDeleteNote(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={(e) => handleDeleteClick(e)}>
|
||||
Confirm delete
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ViewNote = ({ view_note_id, setViewState, setViewNoteId, first_note_id, fetchNotes }) => {
|
||||
var [note_detail, setNoteDetail] = useState("");
|
||||
var [iso_day_string, setIsoDayString] = useState("");
|
||||
var [open_confirm_delete_note, setOpenConfirmDeleteNote] = useState(false);
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setOpenConfirmDeleteNote(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/${view_note_id}`, { credentials: "include" })
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setNoteDetail(res_json[0]);
|
||||
if (res_json[0]?.lastsavedtime && res_json[0]?.lastsavedtime != "") {
|
||||
var d = new Date(res_json[0].lastsavedtime);
|
||||
setIsoDayString(d.toISOString().split(".")[0].replace("T", " "));
|
||||
} else {
|
||||
setIsoDayString("");
|
||||
}
|
||||
});
|
||||
}, [view_note_id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_delete_note ? (
|
||||
<ConfirmDeleteNote
|
||||
note_id={note_detail._id}
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmDeleteNote={setOpenConfirmDeleteNote}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div>Last saved: {iso_day_string || ""}</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleDeleteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div class="icon-button-text">Delete</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="note-title"
|
||||
aria-describedby="note-title"
|
||||
value={note_detail?.title || ""}
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EDIT_NOTE);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={10}
|
||||
value={note_detail?.content || ""}
|
||||
onClick={(e) => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EDIT_NOTE);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// EXPLAIN: After one clicks on the “New note icon” (on any page view where it is shown),
|
||||
// EXPLAIN: a new node creation panel shows in the right panel (Fig. 3).
|
||||
// EXPLAIN: There is a node title input field and a note content input field, into which the user can enter texts.
|
||||
// EXPLAIN: There is a “Cancel” button, clicking which a confirmation box
|
||||
|
||||
// EXPLAIN: “Are you sure to quit editing the note?” will be popped up:
|
||||
// EXPLAIN: if the user confirms quitting, the page view goes back to the one shown in Fig. 2;
|
||||
// EXPLAIN: otherwise, the current page view remains.
|
||||
|
||||
// EXPLAIN: There is a “Save” button, clicking which the newly created note is shown on the right panel,
|
||||
// EXPLAIN: with the “Last saved” time and a “Delete” button displayed on top of the note,
|
||||
// EXPLAIN: as shown in Fig. 4; besides, the note title should be listed in the left panel,
|
||||
// EXPLAIN: as the first in the list (as it is the latest), the note title should be highlighted in a different color than the
|
||||
// EXPLAIN: rest of the note titles in the list (since this note’s content is shown in the right panel),
|
||||
// EXPLAIN: and the total number of notes in ( ) on top of the list should be incremented.
|
||||
|
||||
const NewNote = ({ fetchNotes, setViewState }) => {
|
||||
var [note_title, setNoteTitle] = useState("");
|
||||
var [note_content, setNoteContent] = useState("");
|
||||
var [open_confirm_cancel_edit, setOpenConfirmCancelEdit] = useState(false);
|
||||
|
||||
const handleSaveNoteClick = () => {
|
||||
fetch(`//${CONSTANTS.ADD_NEW_NOTE_ENDPOINT}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: note_title, content: note_content }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_cancel_edit ? (
|
||||
<ConfirmCancelEdit
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmCancelEdit={setOpenConfirmCancelEdit}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div class="viewnote-title">
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => setOpenConfirmCancelEdit(true)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faRemove} />
|
||||
</div>
|
||||
<div class="icon-button-text">Cancel</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleSaveNoteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div class="icon-button-text">Save</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Label htmlFor="note-title">Note title</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="new-note-title"
|
||||
aria-describedby="note-title"
|
||||
onChange={(e) => setNoteTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<div>
|
||||
<Form.Label htmlFor="new-note-content">Note Content</Form.Label>
|
||||
<Form.Control as="textarea" rows={10} onChange={(e) => setNoteContent(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditNote = ({ view_note_id, fetchNotes, setViewState }) => {
|
||||
var [note_title, setNoteTitle] = useState("");
|
||||
var [note_content, setNoteContent] = useState("");
|
||||
var [open_confirm_cancel_edit, setOpenConfirmCancelEdit] = useState(false);
|
||||
|
||||
var [note_detail, setNoteDetail] = useState("");
|
||||
|
||||
const handleSaveNoteClick = () => {
|
||||
fetch(`//${CONSTANTS.SAVE_NOTE_ENDPOINT}/${view_note_id}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: note_title, content: note_content }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
fetchNotes();
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/${view_note_id}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setNoteTitle(res_json[0]?.title);
|
||||
setNoteContent(res_json[0]?.content);
|
||||
});
|
||||
}, [view_note_id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_cancel_edit ? (
|
||||
<ConfirmCancelEdit
|
||||
setViewState={setViewState}
|
||||
fetchNotes={fetchNotes}
|
||||
setOpenConfirmCancelEdit={setOpenConfirmCancelEdit}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="viewnote-container">
|
||||
<div class="viewnote-body">
|
||||
<div class="viewnote-title">
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => setOpenConfirmCancelEdit(true)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faRemove} />
|
||||
</div>
|
||||
<div style={{ paddingLeft: "0.5rem" }}>Cancel</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<Button variant="primary" onClick={(e) => handleSaveNoteClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSave} />
|
||||
</div>
|
||||
<div style={{ paddingLeft: "0.5rem" }}>Save</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<Form.Label htmlFor="note-title">Note title</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="new-note-title"
|
||||
aria-describedby="note-title"
|
||||
value={note_title}
|
||||
onChange={(e) => setNoteTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<div>
|
||||
<Form.Label htmlFor="new-note-content">Note Content</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={10}
|
||||
value={note_content}
|
||||
onChange={(e) => setNoteContent(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddNoteButton = ({ setViewNoteId, view_state, setViewState }) => {
|
||||
if ([CONSTANTS.VIEW_STATE_EMPTY_NOTE, CONSTANTS.VIEW_STATE_VIEW_NOTE].includes(view_state))
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-circle btn-lg"
|
||||
onClick={(e) => {
|
||||
setViewNoteId("");
|
||||
setViewState(CONSTANTS.VIEW_STATE_ADD_NOTE);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</button>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const Home = ({ user_info, setUserInfo, setLoginState }) => {
|
||||
var [note_list, setNoteList] = useState([]);
|
||||
var [is_loading, setIsLoading] = useState(true);
|
||||
var [search_note, setSearchNote] = useState("");
|
||||
var [view_state, setViewState] = useState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
var [view_note_id, setViewNoteId] = useState("");
|
||||
var [first_note_id, setFirstNoteId] = useState("");
|
||||
var [open_confirm_logout, setOpenConfirmLogout] = useState(false);
|
||||
|
||||
const handleLogoutClick = () => {
|
||||
setOpenConfirmLogout(true);
|
||||
};
|
||||
|
||||
const fetchNotes = () => {
|
||||
fetch(`//${CONSTANTS.GET_NOTE_ENDPOINT}/*`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setIsLoading(false);
|
||||
setNoteList(res_json);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetch(`//${CONSTANTS.SEARCH_URL}/${search_note}`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setIsLoading(false);
|
||||
setNoteList(res_json);
|
||||
});
|
||||
}, [search_note]);
|
||||
|
||||
useEffect(() => {
|
||||
setViewState(CONSTANTS.VIEW_STATE_EMPTY_NOTE);
|
||||
fetchNotes();
|
||||
}, []);
|
||||
|
||||
if (is_loading) return <>loading...</>;
|
||||
|
||||
const EmptyNote = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const NoteContent = ({ view_state, view_note_id, setViewNoteId }) => {
|
||||
if (view_state == CONSTANTS.VIEW_STATE_VIEW_NOTE) {
|
||||
return (
|
||||
<ViewNote
|
||||
view_note_id={view_note_id}
|
||||
setViewNoteId={setViewNoteId}
|
||||
first_note_id={first_note_id}
|
||||
fetchNotes={fetchNotes}
|
||||
setViewState={setViewState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (view_state == CONSTANTS.VIEW_STATE_ADD_NOTE)
|
||||
return <NewNote setViewState={setViewState} fetchNotes={fetchNotes} />;
|
||||
if (view_state == CONSTANTS.VIEW_STATE_EDIT_NOTE)
|
||||
return <EditNote view_note_id={view_note_id} setViewState={setViewState} fetchNotes={fetchNotes} />;
|
||||
|
||||
return <EmptyNote />;
|
||||
};
|
||||
|
||||
if (note_list == []) return <>loading notes</>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{open_confirm_logout ? (
|
||||
<ConfirmLogout setOpenConfirmLogout={setOpenConfirmLogout} setLoginState={setLoginState} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div class="home-container">
|
||||
<div class="home-background">iNotes</div>
|
||||
<div class="top-nav-bar">
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingLeft: "2rem",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="user-avatar"
|
||||
style={{ backgroundImage: `url("http://${CONSTANTS.BACKEND_HOST}/${user_info.icon}")` }}
|
||||
></div>
|
||||
<div class="username-container">{user_info.name}</div>
|
||||
</div>
|
||||
<div class="logout-button-container">
|
||||
<Button variant="primary" onClick={(e) => handleLogoutClick(e)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faSignOut} />
|
||||
</div>
|
||||
<div class="icon-button-text">log out</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav-bar-container">
|
||||
<div style={{ width: "20vw", display: "flex", flexDirection: "column", paddingTop: "1rem" }}>
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center", paddingTop: "2rem" }}>
|
||||
<div style={{ width: "80%", display: "flex" }}>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</InputGroup.Text>
|
||||
<Form.Control id="search-note" onChange={(e) => setSearchNote(e.target.value)} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-nav-bar-notes-container">
|
||||
<div style={{ fontSize: "2rem" }}>Notes ({note_list.length})</div>
|
||||
|
||||
{note_list.length < 1 ? (
|
||||
<>sorry but the list is empty</>
|
||||
) : (
|
||||
<>
|
||||
<ListGroup class="list-group-container">
|
||||
{note_list
|
||||
.filter((n) => {
|
||||
const re = new RegExp(search_note);
|
||||
return n.title?.search(re) > -1;
|
||||
})
|
||||
.map((n, idx) => (
|
||||
<ListGroup.Item
|
||||
key={`list_${idx}`}
|
||||
onClick={(e) => {
|
||||
setViewNoteId(n._id);
|
||||
setViewState(CONSTANTS.VIEW_STATE_VIEW_NOTE);
|
||||
}}
|
||||
style={{ backgroundColor: n._id == view_note_id ? "cyan" : "" }}
|
||||
>
|
||||
{n.title || "no title"}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-container">
|
||||
<NoteContent setViewNoteId={setViewNoteId} view_state={view_state} view_note_id={view_note_id} />
|
||||
</div>
|
||||
<div class="add-note-button-container">
|
||||
<AddNoteButton setViewNoteId={setViewNoteId} view_state={view_state} setViewState={setViewState} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Login = ({ login_state, setLoginState, setUserInfo }) => {
|
||||
var [debug, setDebug] = useState();
|
||||
var [username, setUsername] = useState("");
|
||||
var [password, setPassword] = useState("");
|
||||
|
||||
const helloworld_alert = (username, password) => {
|
||||
fetch("//localhost:3001/signin", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignIn = (username, password) => {
|
||||
fetch(`//${CONSTANTS.SIGN_IN_URL}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res_json) => {
|
||||
setDebug(JSON.stringify(res_json));
|
||||
if (res_json.msg == CONSTANTS.MSG_LOGIN_SUCCESS) {
|
||||
setLoginState(CONSTANTS.LOGGED_IN);
|
||||
setUserInfo(res_json.user_meta);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
<div class="login-background">
|
||||
<div style={{ fontSize: "3rem" }}>iNotes</div>
|
||||
<div style={{ paddingTop: "5rem" }}>
|
||||
<Form>
|
||||
<Form.Group className="mb-3">
|
||||
<div class="menu-input-field">
|
||||
<Form.Label>Username:</Form.Label>
|
||||
<div class="login-input-container">
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<div class="menu-input-field">
|
||||
<Form.Label>Password:</Form.Label>
|
||||
<div class="login-input-container">
|
||||
<Form.Control
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</div>
|
||||
<div style={{ paddingTop: "5rem" }}>
|
||||
<Button variant="primary" onClick={(e) => handleSignIn(username, password)}>
|
||||
<div class="icon-button-container">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</div>
|
||||
<div class="icon-button-text">Sign in</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
var [login_state, setLoginState] = useState(CONSTANTS.NOT_LOGGED_IN);
|
||||
var [user_info, setUserInfo] = useState({});
|
||||
|
||||
if (login_state == CONSTANTS.NOT_LOGGED_IN) {
|
||||
return <Login login_state={login_state} setLoginState={setLoginState} setUserInfo={setUserInfo} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Home user_info={user_info} setUserInfo={setUserInfo} setLoginState={setLoginState} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
9
james_endl/assignment/src/frontend/src/App.test.js
Normal file
9
james_endl/assignment/src/frontend/src/App.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
BIN
james_endl/assignment/src/frontend/src/assets/notes.png
(Stored with Git LFS)
Normal file
BIN
james_endl/assignment/src/frontend/src/assets/notes.png
(Stored with Git LFS)
Normal file
Binary file not shown.
38
james_endl/assignment/src/frontend/src/constants.js
Normal file
38
james_endl/assignment/src/frontend/src/constants.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const LOGGED_IN = "logged_in";
|
||||
const NOT_LOGGED_IN = "not_logged_in";
|
||||
|
||||
const MSG_LOGIN_SUCCESS = "login success";
|
||||
|
||||
const BACKEND_HOST = "localhost:3001";
|
||||
|
||||
const VIEW_STATE_ADD_NOTE = "view_state_add_note";
|
||||
const VIEW_STATE_VIEW_NOTE = "view_state_view_note";
|
||||
const VIEW_STATE_EMPTY_NOTE = "view_state_empty_note";
|
||||
const VIEW_STATE_EDIT_NOTE = "view_state_edit_note";
|
||||
|
||||
// ENDPOINTS
|
||||
const ADD_NEW_NOTE_ENDPOINT = `${BACKEND_HOST}/addnote`;
|
||||
const GET_NOTE_ENDPOINT = `${BACKEND_HOST}/getnote`;
|
||||
const DELETE_NOTE_ENDPOINT = `${BACKEND_HOST}/deletenote`;
|
||||
const SAVE_NOTE_ENDPOINT = `${BACKEND_HOST}/savenote`;
|
||||
const LOGOUT_URL = `${BACKEND_HOST}/logout`;
|
||||
const SIGN_IN_URL = `${BACKEND_HOST}/signin`;
|
||||
const SEARCH_URL = `${BACKEND_HOST}/searchnotes`;
|
||||
|
||||
export default {
|
||||
ADD_NEW_NOTE_ENDPOINT,
|
||||
BACKEND_HOST,
|
||||
LOGGED_IN,
|
||||
MSG_LOGIN_SUCCESS,
|
||||
NOT_LOGGED_IN,
|
||||
VIEW_STATE_ADD_NOTE,
|
||||
VIEW_STATE_EMPTY_NOTE,
|
||||
VIEW_STATE_VIEW_NOTE,
|
||||
VIEW_STATE_EDIT_NOTE,
|
||||
GET_NOTE_ENDPOINT,
|
||||
DELETE_NOTE_ENDPOINT,
|
||||
SAVE_NOTE_ENDPOINT,
|
||||
LOGOUT_URL,
|
||||
SIGN_IN_URL,
|
||||
SEARCH_URL,
|
||||
};
|
8
james_endl/assignment/src/frontend/src/index.js
Normal file
8
james_endl/assignment/src/frontend/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
|
||||
// Importing the Bootstrap CSS
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
4
james_endl/assignment/src/frontend/test.bat
Normal file
4
james_endl/assignment/src/frontend/test.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
npm i -d
|
||||
|
||||
npm run start
|
11340
james_endl/assignment/src/frontend/yarn.lock
Normal file
11340
james_endl/assignment/src/frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user