├── src
├── index.css
├── components
│ ├── UserLogOut
│ │ ├── UserLogOut.js
│ │ └── UserLogOut.module.css
│ ├── LoginForm
│ │ ├── LogInForm.module.css
│ │ └── LogInForm.js
│ ├── Header.js
│ ├── NavBar.js
│ ├── SignUpForm
│ │ └── SignUpForm.js
│ └── EditEntryForm.js
├── setupTests.js
├── App.test.js
├── reportWebVitals.js
├── index.js
├── pages
│ ├── AuthPage.js
│ ├── EntryIndexPage.js
│ ├── EntryPage.js
│ └── NewEntryPage.js
├── App.css
├── App.js
└── utilities
│ ├── entries-api.js
│ ├── users-api.js
│ └── users-service.js
├── img
├── ERD.png
├── wireframe.png
├── wireframe-new.png
├── wireframe-entry.png
└── wireframe-landing.png
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── manifest.json
└── index.html
├── config
├── ensureLoggedIn.js
├── database.js
└── checkToken.js
├── .gitignore
├── routes
└── api
│ ├── users.js
│ └── entries.js
├── models
├── user.js
└── entry.js
├── package.json
├── server.js
├── controllers
└── api
│ ├── users.js
│ └── entries.js
└── README.md
/src/index.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/UserLogOut/UserLogOut.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/LoginForm/LogInForm.module.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/UserLogOut/UserLogOut.module.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/img/ERD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/img/ERD.png
--------------------------------------------------------------------------------
/img/wireframe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/img/wireframe.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/img/wireframe-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/img/wireframe-new.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/img/wireframe-entry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/img/wireframe-entry.png
--------------------------------------------------------------------------------
/img/wireframe-landing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmslee/final-project/HEAD/img/wireframe-landing.png
--------------------------------------------------------------------------------
/config/ensureLoggedIn.js:
--------------------------------------------------------------------------------
1 | module.exports = function(req, res, next) {
2 | // Status code of 401 is Unauthorized
3 | if (!req.user) return res.status(401).json('Unauthorized');
4 | // A okay
5 | next();
6 | };
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | function Header () {
4 | return (
5 |
6 |
SFX Glossary
7 |
8 | )
9 | }
10 |
11 | export default Header;
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/config/database.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | mongoose.set('strictQuery', true);
4 | mongoose.connect(process.env.DATABASE_URL);
5 |
6 | const db = mongoose.connection;
7 |
8 | db.on('connected', function () {
9 | console.log(`Connected to ${db.name} at ${db.host}:${db.port}`);
10 | });
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/routes/api/users.js:
--------------------------------------------------------------------------------
1 | //* Routing Logic
2 |
3 | const express = require('express');
4 | const router = express.Router();
5 | const usersCtrl = require('../../controllers/api/users');
6 | const ensureLoggedIn = require('../../config/ensureLoggedIn');
7 |
8 |
9 | //* POST
10 | router.post('/', usersCtrl.create);
11 |
12 | router.post('/login', usersCtrl.login);
13 |
14 | router.get('/check-token', ensureLoggedIn, usersCtrl.checkToken);
15 |
16 |
17 |
18 | module.exports = router;
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/NavBar.js:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import { logOut } from "../utilities/users-service";
3 |
4 | function NavBar({ user, setUser }) {
5 | const handleLogOut = () => {
6 | logOut();
7 | setUser(null);
8 | };
9 | return (
10 |
11 | SFX List
12 | |
13 | Create New Entry
14 | |
15 | Welcome, {user.name} {" "}
16 | |
17 |
18 | Logout
19 |
20 |
21 | );
22 | }
23 |
24 | export default NavBar;
25 |
--------------------------------------------------------------------------------
/routes/api/entries.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const entriesCtrl = require('../../controllers/api/entries');
4 | const ensureLoggedIn = require('../../config/ensureLoggedIn');
5 |
6 | //*Index route: get all workouts
7 | router.get('/', entriesCtrl.index)
8 |
9 | //*New route: create new entry
10 | router.post('/new', entriesCtrl.create);
11 |
12 | //*Show route: get a single entry
13 | router.get('/:id', entriesCtrl.show)
14 |
15 | //*Delete route: delete an entry
16 | router.delete('/:id', entriesCtrl.deleteAnEntry)
17 |
18 | //*Edit route: update an entry
19 | router.put('/edit/:id', entriesCtrl.updateEntry)
20 |
21 | module.exports = router;
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 | import { BrowserRouter as Router } from "react-router-dom";
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/src/pages/AuthPage.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import SignUpForm from "../components/SignUpForm/SignUpForm";
4 | import LoginForm from "../components/LoginForm/LogInForm";
5 |
6 | function AuthPage({ setUser }) {
7 | const [showLogin, setShowLogin] = useState(true);
8 |
9 | return (
10 |
11 | Auth Page
12 |
13 | setShowLogin(!showLogin)}>
14 | {showLogin ? "Sign up" : "Sign in"}
15 |
16 |
17 | {showLogin ? (
18 |
19 | ) : (
20 |
21 | )}
22 |
23 | );
24 | }
25 |
26 | export default AuthPage;
27 |
--------------------------------------------------------------------------------
/config/checkToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | module.exports = function(req, res, next) {
4 | // Check for the token being sent in a header or as a query parameter
5 | let token = req.get('Authorization') || req.query.token;
6 | if (token) {
7 | // Remove the 'Bearer ' if it was included in the token header
8 | token = token.replace('Bearer ', '');
9 | // Check if token is valid and not expired
10 | jwt.verify(token, process.env.SECRET, function(err, decoded) {
11 | // If valid token, decoded will be the token's entire payload
12 | // If invalid token, err will be set
13 | req.user = err ? null : decoded.user;
14 | // If your app cares... (optional)
15 | req.exp = err ? null : new Date(decoded.exp * 1000);
16 | return next();
17 | });
18 | } else {
19 | // No token was sent
20 | req.user = null;
21 | return next();
22 | }
23 | };
--------------------------------------------------------------------------------
/src/pages/EntryIndexPage.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | function EntryIndexPage() {
5 | const [entriesData, setEntriesData] = useState(null)
6 |
7 | useEffect(() => {
8 | const fetchEntries = async () => {
9 | try {
10 | const response = await fetch('/api/entries')
11 | const data = await response.json()
12 | console.log(data)
13 | setEntriesData(data)
14 | } catch (error) {
15 | console.error(error)
16 | }
17 | }
18 | fetchEntries()
19 | }, [])
20 |
21 | return (
22 |
23 |
24 | {entriesData && entriesData.map((entry) => (
25 |
26 | {entry.wordRom} ({entry.wordKan})
27 |
28 | ))}
29 |
30 |
31 | )
32 | }
33 |
34 | export default EntryIndexPage;
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: Arial, Helvetica, sans-serif;
3 | height: 100%;
4 | }
5 | header {
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: space-between;
9 | align-items: baseline;
10 | padding: 1rem;
11 | background-color: lightcyan;
12 | }
13 |
14 | nav {
15 | display: flex;
16 | flex-direction: row;
17 | padding: 1rem;
18 | }
19 |
20 |
21 | .index-container {
22 | padding: 1rem;
23 | margin: 1rem;
24 | }
25 |
26 | a {
27 | color: darkblue;
28 | }
29 |
30 | a :hover {
31 | color: gray;
32 | }
33 |
34 | li {
35 | padding: .05rem;
36 | margin: .05rem
37 | }
38 |
39 | .entrypage-container {
40 | display: flex;
41 | flex-direction: row;
42 | }
43 |
44 | .entry-box {
45 | display: flex;
46 | flex-direction: row;
47 | justify-content: space-between;
48 | width: 60%
49 | }
50 |
51 | .entry {
52 | width: 60%
53 | }
54 |
55 | .update-buttons {
56 | padding: 1rem;
57 | margin-right: 12rem;
58 | border-right: groove;
59 | border-color: lightgray;
60 | width: 30%
61 | }
62 |
63 | .update-buttons-box {
64 | margin: 1rem;
65 | align-items: baseline;
66 | }
67 | .edit-box {
68 | width: 40%
69 | }
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const Schema = mongoose.Schema;
4 | const bcrypt = require('bcrypt');
5 |
6 | //* determines how much processing time it will take to perform the hash
7 | const SALT_ROUNDS = 6; // 6 is a reasonable value
8 |
9 | const userSchema = new Schema({
10 | name: { type: String, required: true },
11 | email: {
12 | type: String,
13 | unique: true,
14 | trim: true,
15 | lowercase: true,
16 | required: true,
17 | },
18 | password: {
19 | type: String,
20 | trim: true,
21 | minLength: 3,
22 | required: true,
23 | },
24 | }, {
25 | timestamps: true,
26 | toJSON: function(doc, ret) {
27 | delete ret.password;
28 | return ret;
29 | }
30 | });
31 |
32 | //* Pre Hook
33 | userSchema.pre('save', async function(next) {
34 | // if password was NOT modified continue to the next middleware
35 | if (!this.isModified('password')) return next();
36 |
37 | // update the password with the computed hash
38 | this.password = await bcrypt.hash(this.password, SALT_ROUNDS)
39 | return next();
40 | })
41 |
42 | module.exports = mongoose.model("User", userSchema);
43 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 |
3 | import { Routes, Route } from 'react-router-dom';
4 |
5 | import AuthPage from './pages/AuthPage';
6 | import Header from './components/Header';
7 | import NavBar from './components/NavBar';
8 | import EntryPage from './pages/EntryPage';
9 | import NewEntryPage from './pages/NewEntryPage';
10 | import EntryIndexPage from './pages/EntryIndexPage';
11 |
12 | import { getUser } from './utilities/users-service';
13 |
14 | import './App.css';
15 |
16 | function App() {
17 | const [user, setUser] = useState(getUser());
18 |
19 | return (
20 |
21 | { user ?
22 | <>
23 |
27 |
28 |
29 | }/>
30 | }/>
31 | }/>
32 |
33 |
34 | >
35 | :
36 |
37 | }
38 |
39 | );
40 | }
41 |
42 | export default App;
43 |
--------------------------------------------------------------------------------
/models/entry.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = require('mongoose').Schema;
4 |
5 | //Create schema
6 | //?schema defines structure of doc/type of doc
7 |
8 | const entrySchema = new Schema({
9 | wordRom: {
10 | type: String,
11 | trim: true,
12 | lowercase: true,
13 | required: true
14 | },
15 | wordKan: {
16 | type: String,
17 | trim: true,
18 | required: true
19 | },
20 | translation: {
21 | type: String,
22 | max: 100
23 | },
24 | gloss: {
25 | type: String,
26 | max: 800
27 | },
28 | source: {
29 | type: String,
30 | enum: ['1', '2', '3', '4', '5', '6', '7', '8']
31 | },
32 | chapter: {
33 | type: Number,
34 | //something that restricts the number range
35 | },
36 | pageNo: {
37 | type: Number,
38 | //something that restricts the page possibilities
39 | },
40 | example: {
41 | type: String,
42 | max: 250
43 | },
44 | notes: {
45 | type: String,
46 | max: 1000
47 | }
48 | }, {timestamps: true}
49 | )
50 |
51 | //Create model
52 | //?applies schema to model that will interact with collection of model's name
53 |
54 | module.exports = mongoose.model("Entry", entrySchema);
--------------------------------------------------------------------------------
/src/utilities/entries-api.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = '/api/entries';
2 |
3 | //* Send form data
4 | export function sendForm(entryFormData){
5 | return sendRequest(`${BASE_URL}/new`, 'POST', entryFormData)
6 | }
7 |
8 | //* Display index
9 | export function getEntries () {
10 | return sendRequest(`${BASE_URL}/entries`, 'GET');
11 | }
12 |
13 | //* Delete entry
14 | export function deleteEntry(id) {
15 | return sendRequest(`${BASE_URL}/${id}`, 'DELETE')
16 | }
17 |
18 | //* Update entry
19 | export function updateEntry(id, newData){
20 | return sendRequest(`${BASE_URL}/edit/${id}`, 'PUT', newData)
21 | }
22 |
23 | //* Display entry
24 | export function getEntryById(id){
25 | return sendRequest(`${BASE_URL}/${id}`)
26 | }
27 |
28 | // --- Helper Functions --- //
29 | export default async function sendRequest(url, method = 'GET', payload = null) {
30 | // Fetch takes an optional options object as the 2nd argument
31 | // used to include a data payload, set headers, etc.
32 | const options = { method };
33 | if (payload) {
34 | options.headers = { 'Content-Type': 'application/json' };
35 | options.body = JSON.stringify(payload);
36 | }
37 |
38 | const res = await fetch(url, options);
39 | // res.ok will be false if the status code set to 4xx in the controller action
40 |
41 | if (res.ok) return res.json();
42 | throw new Error('Bad Request');
43 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "final-project",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "axios": "^1.4.0",
10 | "bcrypt": "^5.1.0",
11 | "cors": "^2.8.5",
12 | "dotenv": "^16.0.3",
13 | "express": "^4.18.2",
14 | "jsonwebtoken": "^9.0.0",
15 | "mongoose": "^6.10.5",
16 | "morgan": "^1.10.0",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-router-dom": "^6.10.0",
20 | "react-scripts": "5.0.1",
21 | "serve-favicon": "^2.5.0",
22 | "web-vitals": "^2.1.4"
23 | },
24 | "scripts": {
25 | "start": "node server.js",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject",
29 | "dev": "react-scripts start",
30 | "heroku-prebuild": "npm install"
31 | },
32 | "engines":{
33 | "node": "18.14.0"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | },
53 | "proxy": "http://localhost:3001"
54 | }
55 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config(); // initialize dotenv
2 | require('./config/database'); // connects to db
3 |
4 | const express = require('express');
5 | const path = require('path'); // node module
6 | const favicon = require('serve-favicon');
7 | const logger = require('morgan');
8 | const cors = require('cors');
9 |
10 | const app = express();
11 | // development port: 3001
12 | // in production we'll a PORT number set in the environment variables
13 | const PORT = process.env.PORT || 3001;
14 |
15 |
16 |
17 | //* Config
18 | // Logger middleware
19 | app.use(logger('dev'));
20 | // JSON payload middleware (for data coming from frontend functions)
21 | app.use(express.json());
22 | // Configure both serve-favicon & static middleware
23 | // to serve from the production 'build' folder
24 | app.use(favicon(path.join(__dirname, 'build', 'favicon.ico')));
25 | app.use(express.static(path.join(__dirname, 'build')));
26 | // checks if token was sent and sets a user data on the req (req.user)
27 | app.use(require('./config/checkToken'));
28 |
29 | // * All other routes
30 | app.use('/api/users', require('./routes/api/users'));
31 | app.use('/api/entries', require('./routes/api/entries'));
32 |
33 |
34 |
35 | // Put API routes here, before the "catch all" route
36 | // The following "catch all" route (note the *) is necessary
37 | // to return the index.html on all non-AJAX requests
38 | app.get('/*', (req, res) => {
39 | res.sendFile(path.join(__dirname, 'build', 'index.html'))
40 | })
41 |
42 |
43 | app.listen(PORT, () => {
44 | console.log(`Server is running on port: ${PORT}`);
45 | })
--------------------------------------------------------------------------------
/src/components/LoginForm/LogInForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import {login} from '../../utilities/users-service';
3 |
4 | export default function LoginForm({ setUser }) {
5 |
6 | const [credentials, setCredentials] = useState({
7 | email: '',
8 | password: ''
9 | });
10 |
11 | const [error, setError] = useState('');
12 |
13 | function handleChange(evt) {
14 | setCredentials({ ...credentials, [evt.target.name]: evt.target.value });
15 | setError('');
16 | }
17 |
18 | async function handleSubmit(evt) {
19 | // Prevent form from being submitted to the server
20 | evt.preventDefault();
21 | try {
22 | // The promise returned by the signUp service method
23 | // will resolve to the user object included in the
24 | // payload of the JSON Web Token (JWT)
25 | const user = await login(credentials);
26 | console.log(user);
27 | setUser(user);
28 | } catch {
29 | setError('Log In Failed - Try Again');
30 | }
31 | }
32 |
33 | return (
34 |
52 | );
53 | }
--------------------------------------------------------------------------------
/src/utilities/users-api.js:
--------------------------------------------------------------------------------
1 | // * The users-service.js module will definitely need to make AJAX requests to the Express server.
2 |
3 | import { getToken } from "./users-service";
4 |
5 | //* SignUpForm.jsx <--> users-service.js <--> users-api.js <-Internet-> server.js (Express)
6 |
7 | //* handleSubmit <--> [signUp]-users-service <--> [signUp]-users-api <-Internet-> server.js (Express)
8 |
9 |
10 | const BASE_URL = '/api/users';
11 |
12 | //* SignUp
13 | export function signUp(userData) {
14 | return sendRequest(BASE_URL, 'POST', userData);
15 | }
16 |
17 |
18 | //* Login
19 | export function login(credentials) {
20 | return sendRequest(`${BASE_URL}/login`, 'POST', credentials);
21 | }
22 |
23 | //* Check Token
24 | export function checkToken() {
25 | return sendRequest(`${BASE_URL}/check-token`)
26 | }
27 |
28 | /*--- Helper Functions ---*/
29 |
30 | async function sendRequest(url, method = 'GET', payload = null) {
31 | // Fetch accepts an options object as the 2nd argument
32 | // used to include a data payload, set headers, etc.
33 | const options = { method };
34 | if (payload) {
35 | options.headers = { 'Content-Type': 'application/json' };
36 | options.body = JSON.stringify(payload);
37 | }
38 |
39 | // sends token to backend
40 | const token = getToken();
41 |
42 | if (token) {
43 | options.headers = options.headers || {};
44 | options.headers.Authorization = `Bearer ${token}`;
45 | }
46 |
47 | const res = await fetch(url, options);
48 | // res.ok will be false if the status code set to 4xx in the controller action
49 | if (res.ok) return res.json();
50 | throw new Error('Bad Request');
51 | }
--------------------------------------------------------------------------------
/controllers/api/users.js:
--------------------------------------------------------------------------------
1 | //* Request handler Logic
2 | const User = require('../../models/user');
3 | const jwt = require('jsonwebtoken');
4 | const bcrypt = require('bcrypt');
5 |
6 | //* /*-- Helper Functions --*/
7 | function createJWT(user) {
8 | return jwt.sign({user}, process.env.SECRET, {expiresIn: '24h'});
9 | }
10 |
11 | async function create(req, res) {
12 | // console.log('[From POST handler]', req.body)
13 | try {
14 | //* creating a new user
15 | const user = await User.create(req.body);
16 | console.log(user);
17 |
18 | //* creating a new jwt
19 | const token = createJWT(user);
20 |
21 | res.json(token);
22 |
23 | } catch (error) {
24 | console.log(error);
25 | res.status(400).json(error)
26 | }
27 | }
28 |
29 |
30 | async function login(req, res) {
31 | try {
32 | // find user in db
33 | const user = await User.findOne({ email: req.body.email });
34 | // check if we found an user
35 | if (!user) throw new Error();
36 | // compare the password to hashed password
37 | const match = await bcrypt.compare(req.body.password, user.password);
38 | // check is password matched
39 | if (!match) throw new Error();
40 | // send back a new token with the user data in the payload
41 | res.json( createJWT(user) );
42 | } catch {
43 | res.status(400).json('Bad Credentials');
44 | }
45 | }
46 |
47 |
48 | async function checkToken(req, res) {
49 | console.log(req.user);
50 | res.json(req.exp)
51 | }
52 |
53 |
54 | module.exports = {
55 | create,
56 | login,
57 | checkToken
58 | }
--------------------------------------------------------------------------------
/controllers/api/entries.js:
--------------------------------------------------------------------------------
1 | //*Control functions for different routes
2 |
3 | const Entry = require('../../models/entry');
4 |
5 | // Get all entries
6 | async function index (req, res) {
7 | try {
8 | const entries = await Entry.find({}).sort({wordRom: 1});
9 | res.json(entries)
10 | } catch (e) {
11 | res.status(400).json({msg: e.message});
12 | }
13 | }
14 |
15 | // Get a single workout
16 | async function show (req, res) {
17 | try {
18 | const entry = await Entry.findById(req.params.id);
19 | res.json(entry)
20 | } catch (e) {
21 | res.status(404).json({msg: e.message})
22 | }
23 | }
24 |
25 | // Create an Entry
26 | async function create (req,res) {
27 | // add doc to db
28 | try {
29 | const entry = await Entry.create(req.body);
30 | console.log(entry);
31 | res.json(entry)
32 | } catch (error) {
33 | console.error(error);
34 | }
35 | }
36 |
37 | // Delete an Entry
38 | async function deleteAnEntry (req,res) {
39 | try {
40 | const entry = await Entry.findByIdAndRemove(req.params.id);
41 | res.json(entries)
42 | } catch (error) {
43 | res.status(400).json(error)
44 | }
45 | }
46 |
47 | // Update an Entry
48 | async function updateEntry(req,res) {
49 | console.log(req.body)
50 | try {
51 | const entry = await Entry.findByIdAndUpdate(req.params.id, req.body, {new: true});
52 | res.json(entry)
53 | } catch (error) {
54 | res.status(400).json(error)
55 | }
56 | }
57 |
58 | module.exports = {
59 | create,
60 | index,
61 | show,
62 | deleteAnEntry,
63 | updateEntry
64 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/utilities/users-service.js:
--------------------------------------------------------------------------------
1 | // * We will use a src/utilities/users-service.js module to organize functions used to sign-up, log in, log out, etc.
2 |
3 | //* SignUpForm.jsx <--> users-service.js <--> users-api.js <-Internet-> server.js (Express)
4 |
5 | //* handleSubmit <--> [signUp]-users-service <--> [signUp]-users-api <-Internet-> server.js (Express)
6 |
7 | import * as usersApi from './users-api';
8 |
9 | //* Get Token
10 | export function getToken() {
11 | const token = localStorage.getItem('token');
12 | // if there is no token
13 | if (!token) return null;
14 |
15 | const payload = JSON.parse(atob(token.split('.')[1]));
16 | console.log(payload);
17 |
18 | // if token is expired
19 | if (payload.exp < Date.now() / 1000) {
20 | localStorage.removeItem('token');
21 | return null;
22 | }
23 |
24 | // token is valid
25 | return token;
26 |
27 | }
28 |
29 | //* Get User
30 | export function getUser() {
31 | const token = getToken();
32 | return token ? JSON.parse(atob(token.split('.')[1])).user : null;
33 | }
34 |
35 | //* SignUp
36 | export async function signUp(userData) {
37 | // Delegate the network request code to the users-api.js API module
38 | // which will ultimately return a JSON Web Token (JWT)
39 | // console.log('[From SignUP function]', userData);
40 | const token = await usersApi.signUp(userData);
41 | // saves token to localStorage
42 | localStorage.setItem('token', token);
43 |
44 | return getUser();
45 | }
46 |
47 | //* LogOut
48 | export function logOut() {
49 | localStorage.removeItem('token')
50 | }
51 |
52 | export async function login(credentials) {
53 | const token = await usersApi.login(credentials)
54 | localStorage.setItem('token', token);
55 | return getUser();
56 | }
57 |
58 | export async function checkToken() {
59 | return usersApi.checkToken().then(dateStr => new Date(dateStr))
60 | }
--------------------------------------------------------------------------------
/src/components/SignUpForm/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {signUp} from '../../utilities/users-service';
3 |
4 | function SignUpForm({setUser}) {
5 | const [formData, setFormData] = useState({
6 | name: "",
7 | email: "",
8 | password: "",
9 | confirm: "",
10 | error: "",
11 | });
12 |
13 | const disable = formData.password !== formData.confirm;
14 |
15 | const handleSubmit = async (e) => {
16 | e.preventDefault();
17 |
18 | try {
19 | console.log(formData)
20 | // data to be send to the backend to create a new user
21 | const userData = {
22 | name: formData.name,
23 | email: formData.email,
24 | password: formData.password
25 | }
26 | // returns a token with the user info
27 | const user = await signUp(userData); // user service
28 | setUser(user);
29 |
30 | } catch (error) {
31 | setFormData({...formData, error: "Sign Up Failed - Try Again"})
32 | }
33 | };
34 |
35 | const handleChange = (evt) => {
36 | setFormData({...formData, [evt.target.name]: evt.target.value, error: ''})
37 | };
38 |
39 | return (
40 |
41 |
66 |
67 |
{formData.error}
68 |
69 | );
70 | }
71 |
72 | export default SignUpForm;
73 |
--------------------------------------------------------------------------------
/src/pages/EntryPage.js:
--------------------------------------------------------------------------------
1 | import {useState, useEffect} from 'react';
2 | import {useParams, useNavigate} from 'react-router-dom';
3 | import * as entriesAPI from '../utilities/entries-api';
4 |
5 | import EditEntryForm from '../components/EditEntryForm';
6 |
7 | function EntryPage () {
8 |
9 | const [entry, setEntry] = useState({
10 | wordRom: "",
11 | wordKan: "",
12 | translation: "",
13 | gloss:"",
14 | source: "",
15 | chapter: "",
16 | pageNo: "",
17 | example: "",
18 | notes: "",
19 | })
20 |
21 | const navigate = useNavigate();
22 | const {id} = useParams();
23 |
24 | //*Call single entry
25 | useEffect(() => {
26 | const getEntry = async () => {
27 | try {
28 | const data = await entriesAPI.getEntryById(id);
29 | setEntry(data)
30 | console.log(data)
31 | } catch (error) {
32 | console.error(error)
33 | }
34 | }
35 | getEntry();
36 | },[])
37 |
38 |
39 | //*Delete entry
40 | const handleSubmit = async (e, entry) => {
41 | e.preventDefault();
42 | try {
43 | const deletedEntry = await entriesAPI.deleteEntry(entry._id);
44 | console.log(deletedEntry);
45 | navigate('/entries')
46 | } catch (error) {
47 |
48 | }
49 | }
50 |
51 | const {wordKan, wordRom, translation, gloss, source, chapter, pageNo, example, notes } = entry
52 | return (
53 |
54 |
55 |
56 |
SFX Info
57 |
58 | Written: {entry.wordKan}
59 |
60 | Read: {entry.wordRom}
61 |
62 | Translation: {entry.translation}
63 |
64 | Gloss: {entry.gloss}
65 |
66 | Volume: {entry.source}
67 |
68 | Chapter: {entry.chapter}
69 |
70 | Page: {entry.pageNo}
71 |
72 | Sample usage: {entry.example}
73 |
74 | Notes: {entry.notes}
75 |
76 |
77 |
78 |
79 |
80 | {/**DELETE BUTTON */}
81 | handleSubmit(e, entry)}
85 | >
86 | Delete
87 |
88 | |
89 |
90 | {/**EDIT BUTTON */}
91 | Edit
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | )
101 | }
102 |
103 | export default EntryPage;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SFX Glossary-maker
2 |
3 |
4 | ## Introduction
5 |
6 | SFX Glossary-maker is an SPA inspired by one of the many capabilities of CAT (Computer Assisted Translation) and TM (Translation Memory) software: creating termbases, or a dataset of terms and phrases stored from a specific text/document that can be easily referenced during the translation process. What I've doned is scaled this down immensely for personal use and, specifically, for manga sound effects as well as onomatopoetic expressions.
7 |
8 | [When ready, deployed project link will be available here.]()
9 |
10 | ## Explanation of Technologies Used
11 |
12 | This app is created using the MERN stack: MongoDB for data storage, Express and Node.js for backend build, and React for frontend build. I started by cloning the starter code from Abraham Tavarez's [MERN-instracture-p1](https://github.com/AbeTavarez/MERN-infrastructure-P1.git), which is the basis for in-class code alongs for building a backend with user authentication. Adjustments to the code were made to fit the needs of the app.
13 |
14 | ## Approach
15 |
16 | For a tool intended as a handy reference tool for translation, I tried to minimize "page jumps" while keeping views functional. After logging in, the user is brought to a landing page where all current SFX entries are listed by name. A header keeps the title and navigation in view.
17 |
18 | 
19 |
20 | The "edit" function can be accessed the single entry (show) page. This way, relevant notes and changes be added without a huge break in the process.
21 |
22 | 
23 |
24 | Creating new entries constitutes a break in process, so the form for that is hosted on a separate "page".
25 |
26 | 
27 |
28 | As for data, you can see from the ERD below that there are only two data entities: one for users and one for SFX entries. Each entry has to be created by at least one user, but no user is required to create an entry.
29 |
30 | 
31 |
32 | For more on the nuts and bolts of the building process, please see my [Trello board](https://trello.com/invite/b/uPVx26Wg/ATTI17e9b43ee9a2616a100a830cf0e06df188C689F2/glossary-app).
33 |
34 |
35 | ## Unsolved Problems
36 |
37 | This is short list of unsolved problems:
38 |
39 | - Edit button
40 | - not linked to Edit Form component
41 | - not set to toggle visibility of Edit Form component
42 | - Inconsistency in whether (prefilled) defaultValues in Edit Form are rendered in updates
43 | - Built-in page navigation/refreshes not working consistently
44 | - App is not yet deployed
45 |
46 | ## Future Enhancements
47 |
48 | Some upcoming builds include:
49 |
50 | - Form/User features
51 | - Allowing different users to add, sign, and display their updates on existing entries
52 | - More intuitive interface for editing each entry element
53 | - Rendering features
54 | - Add categories for entries, such as letters of the alphabet, for better display as number of entries grow
55 | - Add sorting capability to allow entries to be filtered and displayed based on different data points
56 |
57 | ## References/Research
58 |
59 | Resources on CAT/TM Tools: [Trados "Translation 101"](https://www.trados.com/solutions/cat-tools/translation-101-what-is-a-cat-tool.html) | [memoq webinar](https://www.memoq.com/resources/webinars/memoq-getting-started-translators) | [Phrase blog guide](https://phrase.com/blog/posts/cat-tools/)
60 |
61 | Key references for build: [Abraham Tavarez's MERN-infractructure app](https://github.com/AbeTavarez/MERN-infrastructure-P1.git) | Per Scholas course materials and past code-alongs | [MERN Tutorial by The Net Ninja](https://youtu.be/98BzS5Oz5E4)
62 |
63 | ## Acknowledgements
64 |
65 | Thank you to my patient and generous instructors and co-learners. It's been uplifting and real to code with you all!
--------------------------------------------------------------------------------
/src/components/EditEntryForm.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react'
2 | import * as entriesAPI from '../utilities/entries-api';
3 | import { useNavigate, useParams } from 'react-router-dom';
4 |
5 | function EditEntryForm ({entry}) {
6 | const navigate = useNavigate();
7 | const [editedEntry, setEditedEntry] = useState({
8 | ...entry
9 | });
10 |
11 | const {id} = useParams()
12 |
13 | //note: all of the data we're passing through here is NEW
14 | const handleEdit = async (e, newEntry) => {
15 | e.preventDefault();
16 | try {
17 | console.log(newEntry)
18 | const editedEntry = await entriesAPI.updateEntry(id, newEntry);
19 | console.log(editedEntry);
20 | setEditedEntry(editedEntry);
21 | navigate(0)
22 | } catch (error) {
23 | console.log(error)
24 | }
25 | }
26 |
27 | // const handleUpdate = (evt) => {
28 | // setEditedEntry({...editedEntry, [evt.target.name]: evt.target.value, error: ''})
29 | // }
30 |
31 | return (
32 |
33 |
113 |
114 | )
115 | }
116 |
117 | export default EditEntryForm;
--------------------------------------------------------------------------------
/src/pages/NewEntryPage.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import * as entriesAPI from '../utilities/entries-api';
3 | import { useNavigate } from 'react-router-dom';
4 |
5 |
6 | function NewEntryPage( ) {
7 | const navigate = useNavigate()
8 | const [entryData, setEntryData] = useState({
9 | wordRom: "",
10 | wordKan: "",
11 | translation: "",
12 | gloss:"",
13 | source: "",
14 | chapter: "",
15 | pageNo: "",
16 | example: "",
17 | notes: "",
18 | });
19 |
20 | //*handles what happens when user clicks submit
21 | const handleSubmit = async (e) => {
22 | e.preventDefault();
23 | try {
24 | const entryFormData = {
25 | wordRom: entryData.wordRom,
26 | wordKan: entryData.wordKan,
27 | translation: entryData.translation,
28 | gloss: entryData.gloss,
29 | source: entryData.source,
30 | chapter: entryData.chapter,
31 | pageNo: entryData.pageNo,
32 | example: entryData.example,
33 | notes: entryData.notes,
34 | }
35 | console.log(entryFormData)
36 |
37 | const entry = await entriesAPI.sendForm(entryFormData)
38 | // setEntryData()
39 | console.log('new entry added', entry)
40 | navigate('/entries')
41 | } catch (error) {
42 | console.error(error)
43 | }
44 | };
45 |
46 | //*handles user inputting values
47 | const handleChange = (evt) => {
48 | setEntryData({ ...entryData, [evt.target.name]: evt.target.value, error: ''})
49 | }
50 |
51 |
52 | return (
53 |
54 |
135 |
136 | )
137 |
138 | }
139 |
140 | export default NewEntryPage;
--------------------------------------------------------------------------------