├── 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 | 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 | 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 | 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 |
24 |
25 | 26 |
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 |
35 |
36 | 37 |
38 |

39 | 40 | 41 |

42 |

43 | 44 | 45 |

46 | 47 |
48 | 49 |
50 |

 {error}

51 |
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 | 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 |
42 |
43 |

44 | 45 | 46 |

47 | 48 |

49 | 50 | 51 |

52 | 53 |

54 | 55 | 56 |

57 | 58 |

59 | 60 | 61 |

62 | 63 | 64 |
65 |
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 | 88 |   |   89 | 90 | {/**EDIT BUTTON */} 91 | 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 | ![Landing Page Wireframe Image](./img/wireframe-landing.png) 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 | ![Single Entry Page Wireframe Image](./img/wireframe-entry.png) 23 | 24 | Creating new entries constitutes a break in process, so the form for that is hosted on a separate "page". 25 | 26 | ![Create Entry Page Wireframe Image](./img/wireframe-new.png) 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 | ![ERD image](./img/ERD.png) 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 |
handleEdit(e, editedEntry)}> 34 |

Edit Entry Form

35 | 36 | setEditedEntry({...editedEntry, wordRom: e.target.value})} 41 | required 42 | /> 43 |
44 | 45 | setEditedEntry({...editedEntry, wordKan: e.target.value})} 50 | required 51 | /> 52 |
53 | 54 | setEditedEntry({...editedEntry, translation: e.target.value})} 59 | /> 60 |
61 | 62 | setEditedEntry({...editedEntry, gloss: e.target.value})} 67 | /> 68 |
69 | 70 | setEditedEntry({...editedEntry, source: e.target.value})} 75 | /> 76 |
77 | 78 | setEditedEntry({...editedEntry, chapter: e.target.value})} 83 | /> 84 |
85 | 86 | setEditedEntry({...editedEntry, pageNo: e.target.value})} 91 | /> 92 |
93 | 94 | setEditedEntry({...editedEntry, example: e.target.value})} 99 | /> 100 |
101 | 102 | setEditedEntry({...editedEntry, notes: e.target.value})} 107 | /> 108 |
109 | 110 | 111 | 112 |
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 |
55 |

Add New SFX Entry

56 | 57 | 64 |
65 | 66 | 73 |
74 | 75 | 81 |
82 | 83 | 89 |
90 | 91 | 98 |
99 | 100 | 106 |
107 | 108 | 114 |
115 | 116 | 122 |
123 | 124 | 130 |
131 | 132 | 133 | 134 |
135 |
136 | ) 137 | 138 | } 139 | 140 | export default NewEntryPage; --------------------------------------------------------------------------------