diff --git a/.eslintrc.json b/.eslintrc.json
index 48e35cb..8f2d3ac 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,7 +3,8 @@
"env": {
"node": true,
"browser": true,
- "es2021": true
+ "es2021": true,
+ "jest": true
},
"plugins": ["import", "react", "jsx-a11y", "css-modules"],
"extends": [
@@ -39,4 +40,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/.gitignore b/.gitignore
index d241c34..c3f62c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,7 @@ package-lock.json
npm-debug.log*
# output from webpack
-dist/
\ No newline at end of file
+dist/
+
+#environment file
+.env
\ No newline at end of file
diff --git a/__tests__/routes.js b/__tests__/routes.js
new file mode 100644
index 0000000..368b611
--- /dev/null
+++ b/__tests__/routes.js
@@ -0,0 +1,324 @@
+const request = require('supertest');
+const mongoose = require('mongoose');
+const Users = require('../server/models/userModel');
+const Snippets = require('../server/models/snippetModel');
+
+require('dotenv').config();
+
+const server = 'http://localhost:3000';
+const mongoURI = process.env.MONGO_URI;
+
+describe('Snippets route', () => {
+ let user;
+ let snippet_id;
+ const username = '__DummyData__';
+ const password = 'codesmith';
+ const snippet = {
+ title: 'FAKE DATA',
+ comments: 'FUN COMMENTS!',
+ storedCode: 'cry()',
+ tags: ['1', '2', '3'],
+ language: 'Klingon',
+ };
+ const newSnippet = {
+ title: 'NEW FAKE DATA',
+ comments: 'MORE FUN COMMENTS!',
+ storedCode: 'cryAgain()',
+ tags: ['4', '5', '6'],
+ language: 'Huttese',
+ };
+ describe('GET', () => {
+ //before all GET test:
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ await mongoose.connect(mongoURI);
+
+ console.log('Creating dummy data!');
+ user = await Users.create({ username, password });
+
+ const fakeSnippet = await Snippets.create(snippet);
+ snippet_id = fakeSnippet._id;
+ user.snippets.push(fakeSnippet._id);
+
+ return user.save();
+ });
+
+ afterAll(async () => {
+ console.log('Deleting dummy data!');
+ await Users.findByIdAndDelete(user._id);
+ await Snippets.findByIdAndDelete(snippet_id);
+
+ console.log('Disconnecting from the database!');
+ return await mongoose.connection.close();
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .get(`/snippets/?userId=${user._id}`)
+ .expect(200)
+ .expect('Content-Type', 'application/json; charset=utf-8');
+ });
+ it('responds with data that has keys: title, comments, storedCode, language', () => {
+ return request(server)
+ .get(`/snippets/?userId=${user._id}`)
+ .expect((res) => {
+ if (!res.body[0].hasOwnProperty('title')) {
+ throw new Error("Expected 'title' key!");
+ }
+ if (!res.body[0].hasOwnProperty('comments')) {
+ throw new Error("Expected 'comments' key!");
+ }
+ if (!res.body[0].hasOwnProperty('language')) {
+ throw new Error("Expected 'language' key!");
+ }
+ if (!res.body[0].hasOwnProperty('storedCode')) {
+ throw new Error("Expected 'storedCode' key!");
+ }
+ });
+ });
+ it('responds with data that has key, tags, and value of an array', () => {
+ return request(server)
+ .get(`/snippets/?userId=${user._id}`)
+ .expect((res) => {
+ if (!res.body[0].hasOwnProperty('tags')) {
+ throw new Error("Expected 'tags' key!");
+ }
+ if (!Array.isArray(res.body[0].tags)) {
+ throw new Error("Expected 'tags' to be an array!");
+ }
+ });
+ });
+ });
+ describe('POST', () => {
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ await mongoose.connect(mongoURI);
+
+ console.log('Creating dummy data!');
+ user = await Users.create({ username, password });
+
+ return user.save();
+ });
+ afterAll(async () => {
+ console.log('Deleting dummy data!');
+
+ await Users.findByIdAndDelete(user._id);
+
+ console.log('Disconnecting from the database!');
+ return await mongoose.connection.close();
+ });
+ afterEach(async () => {
+ user = await Users.findById(user._id);
+ snippet_id = user.snippets.pop();
+ await user.save();
+ return await Snippets.findByIdAndDelete(snippet_id);
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .post(`/snippets/?userId=${user._id}`)
+ .send(snippet)
+ .expect(200)
+ .expect('Content-Type', 'application/json; charset=utf-8');
+ });
+ it('responds with the newly created document', () => {
+ return request(server)
+ .post(`/snippets/?userId=${user._id}`)
+ .send(snippet)
+ .expect((res) => {
+ expect(res.body.title).toBe(snippet.title);
+ expect(res.body.comments).toBe(snippet.comments);
+ expect(res.body.storedCode).toBe(snippet.storedCode);
+ expect(res.body.language).toBe(snippet.language);
+ expect(res.body.tags).toEqual(snippet.tags);
+ });
+ });
+ it("pushes newly created document to user's snippets array", () => {
+ return request(server)
+ .post(`/snippets/?userId=${user._id}`)
+ .send(snippet)
+ .expect(async (res) => {
+ user = await Users.findById(user._id);
+ expect(user.snippets.length).toEqual(1);
+ });
+ });
+ });
+ describe('PUT', () => {
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ await mongoose.connect(mongoURI);
+
+ console.log('Creating dummy data!');
+ user = await Users.create({ username, password });
+
+ const fakeSnippet = await Snippets.create(snippet);
+ snippet_id = fakeSnippet._id;
+
+ return;
+ });
+ afterEach(async () => {
+ return await Snippets.findByIdAndUpdate(snippet_id, snippet);
+ });
+ afterAll(async () => {
+ console.log('Deleting dummy data!');
+ await Snippets.findByIdAndDelete(snippet_id);
+ await Users.findByIdAndDelete(user._id);
+
+ console.log('Disconnecting from the database!');
+ return await mongoose.connection.close();
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .put(`/snippets/?snippetId=${snippet_id}&userId=${user._id}`)
+ .send(newSnippet)
+ .expect(200)
+ .expect('Content-Type', 'application/json; charset=utf-8');
+ });
+ it('responds with unupdated document', () => {
+ return request(server)
+ .put(`/snippets/?snippetId=${snippet_id}&userId=${user._id}`)
+ .send(newSnippet)
+ .expect((res) => {
+ console.log(res.body);
+ expect(res.body.snippet.title).toEqual(snippet.title);
+ expect(res.body.snippet.comments).toEqual(snippet.comments);
+ expect(res.body.snippet.language).toEqual(snippet.language);
+ });
+ });
+ });
+ describe('DELETE', () => {
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ await mongoose.connect(mongoURI);
+
+ console.log('Creating dummy data!');
+ return (user = await Users.create({ username, password }));
+ });
+ beforeEach(async () => {
+ const fakeSnippet = await Snippets.create(snippet);
+ snippet_id = fakeSnippet._id;
+ user.snippets.push(fakeSnippet._id);
+ return user.save();
+ });
+ afterAll(async () => {
+ console.log('Deleting dummy data!');
+ await Users.findByIdAndDelete(user._id);
+
+ console.log('Disconnecting from the database!');
+ return await mongoose.connection.close();
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .delete(`/snippets/?userId=${user._id}&snippetId=${snippet_id}`)
+ .expect(200)
+ .expect('Content-Type', 'application/json; charset=utf-8');
+ });
+ it('responds with the deleted document', () => {
+ return request(server)
+ .delete(`/snippets/?userId=${user._id}&snippetId=${snippet_id}`)
+ .expect((res) => {
+ expect(res.body.snippet.title).toBe(snippet.title);
+ expect(res.body.snippet.comments).toBe(snippet.comments);
+ expect(res.body.snippet.storedCode).toBe(snippet.storedCode);
+ expect(res.body.snippet.language).toBe(snippet.language);
+ expect(res.body.snippet.tags).toEqual(snippet.tags);
+ });
+ });
+ it("removes delete document from user's snippets array", () => {
+ return request(server)
+ .delete(`/snippets/?userId=${user._id}&snippetId=${snippet_id}`)
+ .expect(async () => {
+ user = await Users.findById(user._id);
+ expect(user.snippets.length).toEqual(0);
+ });
+ });
+ });
+});
+
+describe('Authentication route', () => {
+ let user;
+ const username = '__DummyData__';
+ const password = 'codesmith';
+ const languages = ['klingon'];
+ const tags = ['1', '2', '3'];
+ describe('GET', () => {
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ await mongoose.connect(mongoURI);
+
+ console.log('Creating dummy data!');
+ return (user = await Users.create({
+ username,
+ password,
+ languages,
+ tags,
+ }));
+ });
+ afterAll(async () => {
+ console.log('Deleting dummy data!');
+ await Users.findByIdAndDelete(user._id);
+
+ console.log('Disconnecting from database!');
+ return await mongoose.connection.close();
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .get(`/authentication/?_id=${user._id}`)
+ .expect(200)
+ .expect('Content-Type', 'application/json; charset=utf-8');
+ });
+ it('responds with the correct user data', () => {
+ return request(server)
+ .get(`/authentication/?_id=${user._id}`)
+ .expect((res) => {
+ expect(res.body.username).toEqual(username);
+ expect(res.body.languages[0]).toBe(languages[0]);
+ expect(res.body.tags[0]).toBe(tags[0]);
+ });
+ });
+ });
+ describe('POST', () => {
+ beforeAll(async () => {
+ console.log('Connecting to the database!');
+ return await mongoose.connect(mongoURI);
+ });
+ afterEach(async () => {
+ console.log('Deleting dummy data!');
+ return await Users.findByIdAndDelete(user._id);
+ });
+ afterAll(async () => {
+ console.log('Disconnecting from database!');
+ return await mongoose.connection.close();
+ });
+ it('responds with 200 status and json', () => {
+ return request(server)
+ .post('/authentication/signup')
+ .send({ username, password })
+ .expect(201)
+ .expect('Content-Type', 'application/json; charset=utf-8')
+ .expect((res) => {
+ user = res.body;
+ });
+ });
+ it('responds with newly created user document', () => {
+ return request(server)
+ .post('/authentication/signup')
+ .send({ username, password })
+ .expect((res) => {
+ user = res.body;
+ expect(res.body.username).toEqual(username);
+ });
+ });
+ });
+});
+
+describe('Error handling', () => {
+ describe('Invalid route', () => {
+ it('it returns a 404 status and error message', () => {
+ return request(server)
+ .get('/lorem_ipsum')
+ .expect(404)
+ .expect((res) => {
+ expect(res.text).toEqual('Invalid endpoint');
+ });
+ });
+ });
+});
diff --git a/client/App.jsx b/client/App.jsx
index c9b1e1c..1c4a51f 100644
--- a/client/App.jsx
+++ b/client/App.jsx
@@ -1,10 +1,13 @@
import React from 'react';
+
+// importing child components
import MainContainer from './src/containers/MainContainer/MainContainer.jsx';
const App = () => (
-
+
+
);
-export default App;
\ No newline at end of file
+export default App;
diff --git a/client/src/assets/arrow.png b/client/src/assets/arrow.png
index 0302fec..79dbf61 100644
Binary files a/client/src/assets/arrow.png and b/client/src/assets/arrow.png differ
diff --git a/client/src/components/AddSnippet/AddSnippet.jsx b/client/src/components/AddSnippet/AddSnippet.jsx
index efebf01..c1aa27b 100644
--- a/client/src/components/AddSnippet/AddSnippet.jsx
+++ b/client/src/components/AddSnippet/AddSnippet.jsx
@@ -1,21 +1,30 @@
-import CodeMirror from '@uiw/react-codemirror';
-import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
-import { languages } from '@codemirror/language-data';
-import { langs } from '@uiw/codemirror-extensions-langs';
-import styles from './AddSnippet.module.scss';
import React, { useState } from 'react';
+
+// importing child components
import SaveModal from '../../components/AddSnippet/SaveModal.jsx';
import TagInput from '../../components/ui/TagInput/TagInput';
+
+// importing external functionality
+import CodeMirror from '@uiw/react-codemirror';
+import PropTypes from 'prop-types';
+import { langs } from '@uiw/codemirror-extensions-langs';
+
+// importing utils
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
+
+// importing styles
+import styles from './AddSnippet.module.scss';
+
+// importing data
import { LANGUAGES } from '../../data/data.js';
-const AddSnippet = ({ closeModal }) => {
+const AddSnippet = ({ closeModal, getUserData }) => {
const [title, setTitle] = useState('');
const [language, setLanguage] = useState('');
const [comments, setComments] = useState('');
const [storedCode, setStoredCode] = useState('');
- const [tagList, setTags] = useState('');
+ const [tagList, setTags] = useState([]);
const [error, setError] = useState(false);
const [openModal, setOpenModal] = useState(false);
@@ -32,27 +41,25 @@ const AddSnippet = ({ closeModal }) => {
fetch('/snippets', {
method: 'POST',
headers: {
- 'Content-Type': 'application/json',
+ 'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
language: language,
comments: comments,
tags: tagList,
- storedCode: storedCode,
- }),
+ storedCode: storedCode
+ })
})
- .then((data) => data.json())
+ .then((data) => {
+ getUserData();
+ closeModal(false);
+ data.json();
+ })
.catch((err) => {
console.log(err);
console.log('failed saving snippet');
});
-
-
- // setTitle('');
- // setLanguage('');
- // setComments('');
- // setStoredCode('');
}
// wrapper function for setTags to send to TagInput
@@ -72,26 +79,30 @@ const AddSnippet = ({ closeModal }) => {
centered
>
- Add a snippet
+
+ Add a snippet
+
-
-
+
{
setTitle(e.target.value);
}}
>
- {error && Title is required!}
+ {error && Title is required!}
-
-
- {openModal && }
+ {openModal && }
@@ -154,11 +169,15 @@ const AddSnippet = ({ closeModal }) => {
Save
-
);
};
+AddSnippet.propTypes = {
+ closeModal: PropTypes.func,
+ getUserData: PropTypes.func
+};
+
export default AddSnippet;
diff --git a/client/src/components/AddSnippet/SaveModal.jsx b/client/src/components/AddSnippet/SaveModal.jsx
index d5c8e25..43a8625 100644
--- a/client/src/components/AddSnippet/SaveModal.jsx
+++ b/client/src/components/AddSnippet/SaveModal.jsx
@@ -1,13 +1,7 @@
-import React, { useState} from 'react';
-import Modal from 'react-bootstrap/Modal';
+import React from 'react';
function SaveModal() {
-
- return (
-
- You have successfully saved a snippet!
-
- )
+ return You have successfully saved a snippet!
;
}
export default SaveModal;
diff --git a/client/src/components/SnippetDisplay/SnippetDisplay.jsx b/client/src/components/SnippetDisplay/SnippetDisplay.jsx
index 2bba1ca..5c98f9b 100644
--- a/client/src/components/SnippetDisplay/SnippetDisplay.jsx
+++ b/client/src/components/SnippetDisplay/SnippetDisplay.jsx
@@ -1,210 +1,222 @@
-import React, { useState } from 'react';
-// import Snippet from
+import React, { useState, useEffect } from 'react';
+
+// importing child components
+import TagInput from '../../components/ui/TagInput/TagInput';
+
+// importing external functionality
+import PropTypes from 'prop-types';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import CodeMirror from '@uiw/react-codemirror';
import styles from './SnippetDisplay.module.scss';
import { langs } from '@uiw/codemirror-extensions-langs';
-import TagInput from '../../components/ui/TagInput/TagInput';
-import {Card, Button} from 'react-bootstrap';
+
+// importing utils
+import { Card, Button } from 'react-bootstrap';
+import { set } from 'mongoose';
+
const SnippetDisplay = ({ selectedSnippet, getSnippet }) => {
- // indSnippet = this.props
- // create delete method using fetch request
- let snippetTitle = selectedSnippet.title ? selectedSnippet.title : '';
- let snippetLanguage = selectedSnippet.language ? selectedSnippet.language : '';
- let snippetComments = selectedSnippet.comments ? selectedSnippet.comments : '';
- let snippetStoredCode = selectedSnippet.storedCode ? selectedSnippet.storedCode : '';
- let snippetTagList = selectedSnippet.tags ? selectedSnippet.tags : [];
+ const defaultDisplayValues = {
+ title: '',
+ language: '',
+ comments: '',
+ storedCode: '',
+ tags: []
+ };
- // create a state variable for each passed down state and the its setState function
- // const [title, setTitle] = useState(snippetTitle);
- // const [language, setLanguage] = useState(snippetLanguage);
- // const [comments, setComments] = useState(snippetComments);
- // const [storedCode, setStoredCode] = useState(snippetStoredCode);
- // const [tagList, setTags] = useState(snippetTagList);
+ const [copied, setCopied] = useState(false);
const [editButtonState, setEditButtonState] = useState(false);
+ const [currentDisplay, setCurrentDisplay] = useState(defaultDisplayValues);
+
+ useEffect(() => {
+ setCurrentDisplay(selectedSnippet);
+ }, [selectedSnippet, getSnippet]);
+
+ const handleCopy = () => {
+ setCopied(true);
+ setTimeout(() => {
+ setCopied(false);
+ }, 2000);
+ };
- const deleteSnippet = (id) => {
- fetch (`http://localhost:3000/snippets?id=${id}`, {
- method: 'DELETE',
+ const deleteSnippet = (snippetId) => {
+ fetch('/snippets?' + new URLSearchParams({ snippetId }), {
+ method: 'DELETE'
})
.then((response) => {
if (response.ok) {
+ setCurrentDisplay(defaultDisplayValues);
getSnippet();
}
})
.catch((err) => {
- return ({
+ return {
log: `SnippetDisiplay.deleteSnippet: Error: ${err}`,
- status: err.status || 500,
+ status: err.status,
message: 'There was an error deleting snippet.'
- })
- })
- }
+ };
+ });
+ };
- const editSnippet = (id) => {
- // const [oldState, setOldState] = React.useState([]);
- // create an object (eventually will hold the updated state)
- const updatedSnippet = {
- id: id,
- title: snippetTitle,
- comments: snippetComments,
- storedCode: snippetStoredCode,
- tags: snippetTagList,
- language: snippetLanguage
- };
- // within fetch request (post)
- // body: JSON.stringify(created object)
- fetch (`/snippets?id=${id}`, {
+ const editSnippet = (snippetId) => {
+ fetch(`/snippets?${new URLSearchParams({ snippetId })}`, {
method: 'PUT',
- body: JSON.stringify(updatedSnippet)
+ headers: { 'Content-type': 'application/json' },
+ body: JSON.stringify(currentDisplay)
})
.then((response) => {
- response.json();
+ //Are we using this response anywhere? IF not, delete this.
+ return response.json();
+ })
+ .then((data) => {
+ console.log(data);
getSnippet();
})
.catch((err) => {
- return ({
+ //What's happening here? Where is this being returned to?
+ return {
log: `SnippetDisplay.editSnippet: Error: ${err}`,
- status: err.status || 500,
+ status: err.status,
message: 'There was an error editing code snippet.'
- });
+ };
});
};
- // copy code state
- const [copied, setCopied] = useState(false);
-
-
- const checkEdit = () => {
- if (editButtonState === true) {
- return(
-
-
-
Title:
+ const displayContent = (
+
+
+
+
-
{\n console.log(\'Hello World!)\n}'}
- onChange={(e) => snippetStoredCode = (e)}
- >
- setCopied(true)}
- >
-
-
-
+
{
+ if (editButtonState) {
+ setCurrentDisplay({ ...currentDisplay, tags: e });
+ }
+ }}
+ defaultTags={currentDisplay.tags}
+ readOnly={!editButtonState}
+ />
+ {/* {setTags}}> Title: {snippetTagList} */}
+
+
+
{\n console.log(\'Hello World!)\n}'}
+ onChange={(e) => {
+ setCurrentDisplay({ ...currentDisplay, storedCode: e });
+ }}
+ >
+
+
+
+ {copied && copied to clipboard!}
+
+
+ );
+ return (
+
+
+ {displayContent}
+
+
+
-
-
- );
- }
-
- if (editButtonState === false) {
- return (
-
-
-
-
Title: {snippetTitle}
-
Language: {snippetLanguage}
-
Comments: {snippetComments}
-
- {/* {renderTags()}
*/}
-
-
-
{\n console.log(\'Hello World!)\n}'}
- options={{
- readOnly: true,
}}
- onChange={(e) => snippetStoredCode = (e)}
>
- setCopied(true)}
- >
-
-
-
-
+ Save Edit
+
- );
- }
- };
-
- return (
- <>
-
- {checkEdit()}
-
-
-
-
-
- >
+
);
};
+SnippetDisplay.propTypes = {
+ selectedSnippet: PropTypes.object,
+ getSnippet: PropTypes.func
+};
export default SnippetDisplay;
diff --git a/client/src/components/SnippetDisplay/SnippetDisplay.module.scss b/client/src/components/SnippetDisplay/SnippetDisplay.module.scss
index 9044255..4d8e8ce 100644
--- a/client/src/components/SnippetDisplay/SnippetDisplay.module.scss
+++ b/client/src/components/SnippetDisplay/SnippetDisplay.module.scss
@@ -1,10 +1,34 @@
.editor {
margin: 10px;
- width: 1000px;
+ max-width: 100%;
+ min-width: 200px;
+ //width: 100%;
// margin: auto;
padding-bottom: 10px;
}
+.displayRow {
+ display: flex;
+ flex-direction: row;
+ padding-top: 10px !important;
+ padding-bottom: 10px !important;
+ gap: 10px;
+ input {
+ background-color: transparent;
+ color: whitesmoke;
+ border: none;
+ border-bottom: 1.5pt double whitesmoke;
+ }
+ span {
+ margin: 5px;
+ }
+
+ .aspect-entry {
+ display: flex;
+ align-items: baseline;
+ }
+}
+
.title {
width: 185px;
}
@@ -37,22 +61,22 @@
.card {
padding: 15px;
- margin: 50px;
+ margin-left: 50px;
+ margin-right: 50px;
display: flex;
-
}
.addButton {
border: transparent;
- height: 50px;
- width: 270px;
- // padding-left: 7px;
- // padding-right: 7px;
- border-radius: 10px;
- margin-left: 10px;
- margin-right: 10px;
- margin-bottom: 10px;
- font-size: 1.5rem;
- background-color: transparent;
- color: white;
-}
\ No newline at end of file
+ height: 50px;
+ width: 270px;
+ // padding-left: 7px;
+ // padding-right: 7px;
+ border-radius: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ font-size: 1.5rem;
+ background-color: transparent;
+ color: white;
+}
diff --git a/client/src/components/ui/TagInput/TagInput.jsx b/client/src/components/ui/TagInput/TagInput.jsx
index 44f4a78..38cc13a 100644
--- a/client/src/components/ui/TagInput/TagInput.jsx
+++ b/client/src/components/ui/TagInput/TagInput.jsx
@@ -1,46 +1,42 @@
-import React, { useEffect } from 'react';
-import { TAGS } from '../../../data/data';
-import styles from './TagInput.module.scss';
+import React, { useState, useEffect } from 'react';
+import './TagInput.scss';
+
+// importing utils
import { WithContext as ReactTags } from 'react-tag-input';
import PropTypes from 'prop-types';
+// importing data
+import { TAGS } from '../../../data/data';
+
+// importing styles
+
const suggestions = TAGS.map((tag) => {
return {
id: tag,
- text: tag,
+ text: tag
};
});
const KeyCodes = {
comma: 188,
- enter: 13,
+ enter: 13
};
const delimiters = [KeyCodes.comma, KeyCodes.enter];
-const TagInput = (props) => {
-
- const [tags, setTags] = React.useState([]);
+const TagInput = ({ onChange, defaultTags, readOnly }) => {
+ const [tags, setTags] = useState([]);
const handleDelete = (i) => {
setTags(tags.filter((tag, index) => index !== i));
};
- const initialTags = () => {
- const newTagList = [];
- if (props.tags) {
- props.tags.forEach((tag) => newTagList.push({ id: tag, text: tag }));
- setTags(newTagList);
- }
- };
-
- // useEffect(() => {
- // console.log('hello');
- // initialTags();
- // }, [tags]);
-
const handleAddition = (tag) => {
- setTags([...tags, tag]);
+ console.log('Added tag:');
+ console.dir(tag);
+ const newTags = tags.map((el) => el);
+ newTags.push(tag);
+ setTags(newTags);
};
const handleDrag = (tag, currPos, newPos) => {
@@ -57,16 +53,29 @@ const TagInput = (props) => {
console.log('The tag at index ' + index + ' was clicked');
};
+ useEffect(() => {
+ if (defaultTags) {
+ const tagArr = [];
+ defaultTags.forEach((tag) => {
+ tagArr.push({ id: tag, text: tag });
+ });
+ setTags(tagArr);
+ }
+ }, [defaultTags]);
+
useEffect(() => {
const tagStringList = [];
- if (props.onChange) {
+ if (onChange) {
tags.forEach((tag) => tagStringList.push(tag.text));
- props.onChange(tagStringList);
+ onChange(tagStringList);
}
}, [tags]);
return (
{
TagInput.propTypes = {
onChange: PropTypes.func,
- tags: PropTypes.array,
+ defaultTags: PropTypes.array
};
export default TagInput;
diff --git a/client/src/components/ui/TagInput/TagInput.scss b/client/src/components/ui/TagInput/TagInput.scss
new file mode 100644
index 0000000..138de40
--- /dev/null
+++ b/client/src/components/ui/TagInput/TagInput.scss
@@ -0,0 +1,26 @@
+.react-tags-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+
+ .ReactTags_tagInput {
+ display: flex;
+ }
+ .ReactTags_selected {
+ display: flex;
+ white-space: pre-wrap;
+ }
+ button {
+ //display: inline-block;
+ color: white !important;
+ background-color: inherit !important;
+ border: none;
+ }
+
+ .tag-wrapper {
+ white-space: nowrap;
+ margin: 3px;
+ padding: 3px;
+ border: 1pt solid whitesmoke;
+ border-radius: 3px;
+ }
+}
diff --git a/client/src/components/userStart/Login.jsx b/client/src/components/userStart/Login.jsx
new file mode 100644
index 0000000..4a12de1
--- /dev/null
+++ b/client/src/components/userStart/Login.jsx
@@ -0,0 +1,61 @@
+import React, { useState } from 'react';
+
+import styles from './Login.module.scss';
+
+// eslint-disable-next-line react/prop-types
+const Login = ({ handleLogin, handleHaveAccount, style, error }) => {
+
+ return (
+
+
+
USER LOGIN
+
+ //
+ {error &&
wrong username or password!
}
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/client/src/components/userStart/Login.module.scss b/client/src/components/userStart/Login.module.scss
new file mode 100644
index 0000000..796d61f
--- /dev/null
+++ b/client/src/components/userStart/Login.module.scss
@@ -0,0 +1,8 @@
+.red {
+ border: 1.5px solid red;
+}
+
+.p {
+ margin-top: 8px;
+ color: red;
+}
diff --git a/client/src/components/userStart/Signup.jsx b/client/src/components/userStart/Signup.jsx
new file mode 100644
index 0000000..6f7fc05
--- /dev/null
+++ b/client/src/components/userStart/Signup.jsx
@@ -0,0 +1,43 @@
+import React, { useState } from 'react';
+
+// eslint-disable-next-line react/prop-types
+const Signup = ({ handleSigned, strengthMessage, handleStrength }) => {
+ return (
+
+ );
+};
+
+export default Signup;
diff --git a/client/src/containers/MainContainer/MainContainer.jsx b/client/src/containers/MainContainer/MainContainer.jsx
index 2954daf..c5f8f25 100644
--- a/client/src/containers/MainContainer/MainContainer.jsx
+++ b/client/src/containers/MainContainer/MainContainer.jsx
@@ -1,14 +1,141 @@
-import React from 'react';
+import React, { useState } from 'react';
import Sidebar from '../Sidebar/Sidebar.jsx';
import styles from './MainContainer.module.scss';
+import Login from '../../components/userStart/Login.jsx';
+import Signup from '../../components/userStart/Signup.jsx';
+import validator from 'validator';
+const MainContainer = () => {
+ const [login, setLogin] = useState(false);
+ const [haveAccount, setHaveAccount] = useState(true);
+ const [strengthMessage, setStrengthMessage] = useState('');
+
+ //MAY NEED TO REMOVE -- DUPLICATE WITH STRENGTHMESSAGE
+ const [error, setError] = useState(false);
+
+
+ const handleLogin = (e) => {
+ e.preventDefault();
+ const usernameInputValue = document.getElementById('username').value;
+ document.getElementById('username').value = '';
+ const passwordInputValue = document.getElementById('password').value;
+ document.getElementById('password').value = '';
+
+ if(usernameInputValue === '' || passwordInputValue === ''){
+ document.querySelector('.errorMessage').innerHTML = ' Username and password are required';
+ return false;
+ };
+
+
+
+ fetch('http://localhost:3000/authentication/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ // include cookies from cross origin request
+ credentials: 'include',
+ body: JSON.stringify({
+ username: usernameInputValue,
+ password: passwordInputValue,
+ }),
+ })
+ .then((result) => result.json())
+ .then((result) => {
+ console.log('result from login request: ', result);
+ setLogin(result.username);
+ })
+ .catch((err) => {
+
+ //MAY NEED TO REMOVE -- SEE IF TWO MESSAGES APPEAR
+ setError(true);
+ console.log(err);
+
+
+ document.querySelector('.errorMessage').innerHTML = 'Please verify your user information and try again!';
+ return false;
+
+ });
+ };
+ //functino to handle showing the signup page
+ const handleHaveAccount = () => setHaveAccount(false);
+
+ //function to handle password strength check
+ const handleStrength = (input) => {
+ if(validator.isStrongPassword(input, {
+ minLength: 8,
+ minLowercase: 1,
+ minUppercase: 1,
+ minNumbers: 1,
+ minSymbols: 1
+ })){
+ setStrengthMessage('Strong Password');
+ } else {
+ setStrengthMessage('Not A Strong Password');
+ }
+ };
+
+ //function to handle sign-up if username was not already taken
+ const handleSigned = (e) => {
+ e.preventDefault();
+ const nameValue = document.getElementById('user').value;
+ document.getElementById('user').value = '';
+ const passwordValue = document.getElementById('psw').value;
+ document.getElementById('psw').value = '';
+
+ fetch('http://localhost:3000/authentication/signup', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ username: nameValue,
+ password: passwordValue,
+ }),
+ })
+ .then((result) => result.json())
+ .then((result) => {
+ console.log('result from signup request: ', result);
+ setHaveAccount(true);
+ setLogin(result.username);
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+ };
+
+ return login ? (
-const MainContainer = () =>{
- return (
+
+
+
+ welcome, {login}
+
+
- );
-}
+ ) : haveAccount ? (
+
+
+
+
+ ) : (
+
+
+
+ );
+};
-export default MainContainer;
\ No newline at end of file
+export default MainContainer;
diff --git a/client/src/containers/MainContainer/MainContainer.module.scss b/client/src/containers/MainContainer/MainContainer.module.scss
index 38ac063..27f568c 100644
--- a/client/src/containers/MainContainer/MainContainer.module.scss
+++ b/client/src/containers/MainContainer/MainContainer.module.scss
@@ -1,7 +1,35 @@
.container {
- height: 100vh;
+ height: 100%;
width: 100%;
display: flex;
+}
+
+.div {
+ position: absolute;
+ top: 1rem;
+ right: 3rem;
+}
+
+.button {
+ margin-right: 1rem;
+ border: 1px solid transparent;
+ border-radius: 5px;
+}
+
+.h2 {
+ display: inline-block;
+ background: -webkit-linear-gradient(#ffffff, #333);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.span {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 48px;
+ text-transform: uppercase;
+ background: -webkit-linear-gradient(#ffffff, #e2b2b2);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
-}
\ No newline at end of file
diff --git a/client/src/containers/Sidebar/Sidebar.jsx b/client/src/containers/Sidebar/Sidebar.jsx
index 7177f59..97f8046 100644
--- a/client/src/containers/Sidebar/Sidebar.jsx
+++ b/client/src/containers/Sidebar/Sidebar.jsx
@@ -1,46 +1,58 @@
import React, { useState, useEffect } from 'react';
+
+// importing child components
import SnippetDisplay from '../../components/SnippetDisplay/SnippetDisplay.jsx';
import AddSnippet from '../../components/AddSnippet/AddSnippet.jsx';
-import styles from './Sidebar.module.scss';
import SnippetsRadioList from './SnippetsRadioList/SnippetsRadioList.jsx';
+import TagsList from './TagsList/TagsList.jsx';
+
+// importing utils
import { Card, Spinner } from 'react-bootstrap';
+
+// importing styles
+import styles from './Sidebar.module.scss';
+
+// importing assets
import arrow from '../../assets/arrow.png';
import img from '../../assets/star nose mole.jpeg';
-const Sidebar = () => {
+const Sidebar = ({ handleLogin }) => {
+ //Snippets and selected snippet
const [snippets, setSnippets] = useState([]);
const [selectedSnippet, setSelectedSnippet] = useState({});
+
+ //Tags and selected tags
+ const [userTags, setUserTags] = useState([]);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [filteredSnippets, setFilteredSnippets] = useState([]);
+
const [openModal, setOpenModal] = useState(false);
const [collapse, setCollapse] = useState(false);
const [loading, setLoading] = useState(true);
+ const [displayType, setDisplayType] = useState('snippets');
- // getSnippet func
- const getSnippet = () => {
- setLoading(true);
- fetch('http://localhost:3000/snippets')
- .then((res) => res.json())
- .then((res) => {
- console.log('res', res);
+ useEffect(() => {
+ getUserData();
+ }, []);
- // moved setSnippets to outside of for loop so we arent re-rendering each time a snippet is added to state
- const newSnippetArray = [];
- for (const snippet of res.snippets) newSnippetArray.push(snippet);
+ useEffect(() => {
+ filterSnippetsByTags();
+ }, [snippets, selectedTags]);
+ //Get all snippets stored under user's account
- setSnippets(newSnippetArray);
+ const getUserData = () => {
+ setLoading(true);
+ fetch('/snippets')
+ .then((res) => res.json())
+ .then((data) => {
+ //As structured in snippets route, should receive an array of snippet objects
+ setSnippets(data.snippets);
+ setUserTags([...data.tagsLangs.tags, ...data.tagsLangs.languages]);
setLoading(false);
})
- .catch((error) => console.log('Get request failed', error));
- };
-
- // renderTags function
- const renderTabs = () => {
- const tabs = [];
-
- for (let i = 0; i < snippets.length; i++) {
- tabs.push();
- }
-
- return tabs;
+ .catch((error) =>
+ console.log('Failed to complete request for snippets: ', error)
+ );
};
// wrapper to send to our snippets radio list for updating selected snippet. probably not 100% needed, but want to be able to console log from Sidebar
@@ -48,57 +60,157 @@ const Sidebar = () => {
setSelectedSnippet(e);
};
- // get data from backend at first page load
- useEffect(() => getSnippet(), []);
+ const selectDeselectTag = (tagValue) => {
+ const newTagList = new Set(selectedTags);
+ if (!newTagList.has(tagValue)) {
+ newTagList.add(tagValue);
+ } else {
+ newTagList.delete(tagValue);
+ }
+
+ setSelectedTags(Array.from(newTagList));
+ };
const toggleSidebar = () => {
setCollapse(() => !collapse);
};
+ const toggleDisplayType = (event) => {
+ setDisplayType(event.target.value);
+ };
+
+ const filterSnippetsByTags = () => {
+ const snippetSubset = snippets.filter((sn) => {
+ for (let i = 0; i < [...sn.tags, sn.lanuage].length; i++) {
+ if (selectedTags.includes([...sn.tags, sn.lanuage][i])) return true;
+ }
+ return false;
+ });
+
+ setFilteredSnippets(snippetSubset);
+ };
+
+ const snippetsDisplay = (
+
+ {/* Animation while app is fetching data from DB */}
+
+ {loading && (
+
+
+
+ )}
+
+
+
+ );
+
+ const tagsDisplay = (
+
+ {/* Animation while app is fetching data from DB */}
+
+ {loading && (
+
+
+
+ )}
+
+
+
+ );
+
return (
- <>
-
+
+ {/*----- SIDE BAR -----*/}
+
- Code Snippets
+ {/* Changes the collapse state, which will render/unrender the sidebar*/}
+
+
+
+
-
-
- {/* render our snippet list, pass down snippets and function to update selectedSnippet */}
- {loading && (
-
-
-
- )}
-
-
-
-
- Click me to add a new snippet!
+
+ {/* Renders the list of snippets fetched from DB */}
+
+ {displayType === 'snippets' ? snippetsDisplay : tagsDisplay}
+
-
- {openModal && }
+
+ {/*----- ADD SNIPPET MODAL -----*/}
+
+ {openModal && (
+
+ )}
+
+ {/*----- SNIPPET DISPLAY -----*/}
+
{
{snippets && (
)}
- >
+
);
};
diff --git a/client/src/containers/Sidebar/Sidebar.module.scss b/client/src/containers/Sidebar/Sidebar.module.scss
index b3215b1..555bb9d 100644
--- a/client/src/containers/Sidebar/Sidebar.module.scss
+++ b/client/src/containers/Sidebar/Sidebar.module.scss
@@ -9,19 +9,60 @@
// border-color: black;
// }
+.tagsSnippetsDisplayHolder {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ max-height: 75vh;
+}
+
+.tagsSnippetsDisplayBox {
+ padding: 8px 16px;
+ overflow-y: auto;
+ max-height: 50%;
+}
+
+.displayTypeSelector {
+ display: flex;
+ justify-content: space-around;
+ text-align: center;
+ gap: 10px;
+ width: 100%;
+}
+
+.displayTypeButtonActive {
+ background-color: transparent;
+ box-shadow: none;
+ width: 100%;
+ color: whitesmoke !important;
+ border: none;
+ border-bottom: 1.5pt solid whitesmoke !important;
+}
+
+.displayTypeButtonInactive {
+ background-color: transparent;
+ box-shadow: none;
+ width: 100%;
+ color: rgb(124, 124, 124) !important;
+ border: none !important;
+}
+
+.cardBodyContent {
+ //height: min-content;
+ overflow: hidden;
+}
.sidebar {
width: 300px;
- height: 100vh !important;
+ //height: inherit;
+ //max-height: 75vh;
position: absolute;
- left: -300px;
+ left: -250px;
top: 0;
-
+
transition: 200ms ease-in;
background-color: rgb(10, 25, 41);
- padding-top: 28px;
-
display: flex;
flex-direction: column;
justify-content: space-evenly;
@@ -31,12 +72,12 @@
z-index: 20;
right: -50px;
top: 35px;
- transform: scale(.5);
+ transform: scale(0.5);
background: none;
border: none;
.arrow {
-
+ color: whitesmoke;
}
.arrowOpen {
@@ -45,7 +86,9 @@
}
.cardBody {
-
+ padding: 8px 16px;
+ overflow-y: auto;
+ max-height: 75vh;
}
}
@@ -58,11 +101,13 @@
// z-index: -20;
left: -250px;
transition: 200ms ease-in;
+ width: 100%;
}
.snippetDisplayOpen {
left: 0;
transition: 200ms ease-in;
+ width: 100%;
}
.logo {
@@ -97,17 +142,16 @@
.addButton {
border: transparent;
- height: 50px;
- width: 270px;
- // padding-left: 7px;
- // padding-right: 7px;
- border-radius: 10px;
- margin-left: 10px;
- margin-right: 10px;
- margin-bottom: 10px;
- font-size: 1.5rem;
- background-color: transparent;
- color: white;
+ height: 50px;
+ // padding-left: 7px;
+ // padding-right: 7px;
+ border-radius: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ font-size: 1.5rem;
+ background-color: transparent;
+ color: white;
}
.img {
@@ -122,4 +166,4 @@
margin-bottom: 110px;
margin-left: 3px;
font-size: 21px;
-}
\ No newline at end of file
+}
diff --git a/client/src/containers/Sidebar/SnippetsRadioList/SnippetsRadioList.jsx b/client/src/containers/Sidebar/SnippetsRadioList/SnippetsRadioList.jsx
index 5fae115..d5b93f1 100644
--- a/client/src/containers/Sidebar/SnippetsRadioList/SnippetsRadioList.jsx
+++ b/client/src/containers/Sidebar/SnippetsRadioList/SnippetsRadioList.jsx
@@ -1,80 +1,63 @@
+import React, { Fragment, useEffect } from 'react';
+
+// importing utils
import PropTypes from 'prop-types';
+
+// importing styles
import styles from './SnippetsRadioList.module.scss';
-import { Fragment } from 'react';
-function SnippetsRadioList(props) {
+function SnippetsRadioList({ snippets, setSelectedSnippet }) {
// call passed in function on changed button, should find a way to do this without having to iterate through snippet array. store the snippet on the input itself somehow?
const onChangeValue = (e) => {
- if (!props.onChange) return;
- for (const el of props.snippets) {
- if (el.id !== undefined && el.id.toString() === e.target.value)
- props.onChange(el);
- }
+ if (!setSelectedSnippet) return;
+ setSelectedSnippet(buttonMapper[e.target.id]);
};
- const toggleButtons = [];
+ const buttonMapper = {};
+ const buttonList = [];
- if (props.snippets) {
- props.snippets.forEach((el, i) => {
- const currButton =
- i === 0 ? (
-
-
-
-
-
- ) : (
-
-
-
-
-
- );
+ if (snippets) {
+ snippets.forEach((el, i) => {
+ const currButton = (
+
+
+
+
+
+ );
- toggleButtons.push(currButton);
+ buttonMapper[`snippet-input-${i}`] = el;
+ buttonList.push(currButton);
});
}
return (
- <>
+
onChangeValue(e)}
>
- {toggleButtons}
+ {buttonList}
- >
+
);
}
SnippetsRadioList.propTypes = {
snippets: PropTypes.array,
- onChange: PropTypes.func,
+ setSelectedSnippet: PropTypes.func
};
export default SnippetsRadioList;
diff --git a/client/src/containers/Sidebar/TagsList/TagsList.jsx b/client/src/containers/Sidebar/TagsList/TagsList.jsx
new file mode 100644
index 0000000..5522660
--- /dev/null
+++ b/client/src/containers/Sidebar/TagsList/TagsList.jsx
@@ -0,0 +1,41 @@
+import React, { useEffect } from 'react';
+
+// importing utils
+import PropTypes from 'prop-types';
+
+// importing styles
+import styles from './TagsList.module.scss';
+
+function TagsList({ allTags, selectedTags, selectDeselectTag }) {
+ const buttonList = [];
+
+ const onTagClick = (e) => {
+ selectDeselectTag(e.target.value);
+ };
+
+ if (allTags) {
+ allTags.forEach((el) => {
+ const currButton = (
+
+ );
+ buttonList.push(currButton);
+ });
+ }
+
+ return {buttonList}
;
+}
+TagsList.propTypes = {
+ allTags: PropTypes.array,
+ selectedTags: PropTypes.array,
+ selectDeselectTag: PropTypes.func
+};
+export default TagsList;
diff --git a/client/src/containers/Sidebar/TagsList/TagsList.module.scss b/client/src/containers/Sidebar/TagsList/TagsList.module.scss
new file mode 100644
index 0000000..17a0433
--- /dev/null
+++ b/client/src/containers/Sidebar/TagsList/TagsList.module.scss
@@ -0,0 +1,16 @@
+.activeTag {
+ border: none;
+ background-color: whitesmoke;
+ color: black;
+ border-radius: 10px;
+}
+
+.inactiveTag {
+ border: 1pt solid whitesmoke;
+ background-color: black;
+ color: whitesmoke;
+ border-radius: 10px;
+}
+
+.tagsListDisplay {
+}
diff --git a/client/src/scss/_global.scss b/client/src/scss/_global.scss
index 0e76d3f..7abf2ad 100644
--- a/client/src/scss/_global.scss
+++ b/client/src/scss/_global.scss
@@ -1,15 +1,120 @@
+*::-webkit-scrollbar {
+ color: #232024;
+}
+*::-webkit-scrollbar-track {
+ background: transparentize(#232024, 1);
+}
+
+*::-webkit-scrollbar-corner {
+ background: transparentize(#232024, 1);
+}
+
+*::-webkit-scrollbar-thumb {
+ background: whitesmoke;
+}
+
body {
+ background: url(https://wallpaperaccess.com/full/266479.png) no-repeat center
+ center fixed;
+ -webkit-background-size: cover;
+ -moz-background-size: cover;
+ -o-background-size: cover;
+ background-size: cover;
+}
+
+#right {
+ background: radial-gradient(150px 40px at 195px bottom, #666, #222);
+
+ color: rgb(186, 127, 241);
+}
+
+#card {
+ background: radial-gradient(150px 40px at 195px bottom, #666, #222);
+ color: white;
+ margin-left: 20px;
+}
+
+.login {
+ display: flex;
+ flex-direction: column;
+ min-height: 500px;
+ width: 60%;
+ margin: auto;
+ margin-top: 100px;
+ justify-content: center;
+ align-items: center;
+}
+
+.errorMessage {
+ margin-top: 240px;
+ position: fixed;
color: red;
+ font-size: 17px;
}
+.form {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-top: 10px;
+ height: 200px;
+}
-// this might be a bug. react tag library wasnt going inline
-.ReactTags__selected {
+#secondDiv {
+ margin-top: 15px;
+}
+#go {
+ margin-top: 20px;
+ width: 80px;
+ border: 1px solid;
+ background-color: rgb(4, 99, 129);
+ border-radius: 10px;
+ color: white;
+}
+#submit {
+ margin-top: 20px;
+ width: 100px;
+ border: 1px solid green;
+ background-color: green;
+ border-radius: 10px;
+ color: white;
+}
+
+#loginBox {
+ background-color: rgb(18, 64, 84);
+ opacity: 0.9;
+ background: -webkit-linear-gradient(#eee, #333);
+ height: 400px;
+ color: black;
+ font-size: 16px;
+ font-weight: bold;
+ width: 600px;
+ border: 2px solid rgb(42, 41, 41);
display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ border-radius: 12px;
+ border-style: double;
+}
+
+#bottomMessage {
+ font-size: 20px;
+ background: -webkit-linear-gradient(#eee, #333);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
}
-.cm-editor {outline: none !important}
+#headerTitle {
+ font-size: 72px;
+ background: -webkit-linear-gradient(#eee, #333);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
-.entireSnippetDisplay {
- z-index: 50;
-}
\ No newline at end of file
+.signup {
+ width: 30%;
+ color: lightgrey;
+ margin: auto;
+ margin-top: 200px;
+}
diff --git a/package.json b/package.json
index f13e347..02bfcd2 100644
--- a/package.json
+++ b/package.json
@@ -14,21 +14,30 @@
"dependencies": {
"@uiw/codemirror-extensions-langs": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
+ "bcrypt": "^5.1.0",
"bootstrap": "^5.2.3",
"codemirror": "^6.0.1",
+ "cookie-parser": "^1.4.6",
"cors": "^2.8.5",
+ "dotenv": "^16.0.3",
"express": "^4.18.2",
+ "express-openid-connect": "^2.16.0",
+ "jsonwebtoken": "^9.0.0",
"mongodb": "^5.5.0",
"mongoose": "^7.1.1",
+ "passport": "^0.6.0",
+ "passport-jwt": "^4.0.1",
+ "passport-local": "^1.0.0",
"react": "^18.2.0",
- "react-copy-to-clipboard": "^5.1.0",
"react-bootstrap": "^2.7.4",
+ "react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^14.0.5",
"react-dnd-html5-backend": "^14.1.0",
"react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-tag-input": "^6.8.1",
- "sass": "^1.62.1"
+ "sass": "^1.62.1",
+ "validator": "^13.9.0"
},
"devDependencies": {
"@babel/core": "^7.21.8",
@@ -50,6 +59,7 @@
"nodemon": "^2.0.22",
"sass-loader": "^13.2.2",
"style-loader": "^3.3.2",
+ "supertest": "^6.3.3",
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1",
"webpack-dev-server": "^4.15.0"
diff --git a/server/authConfig/jwt.config.js b/server/authConfig/jwt.config.js
new file mode 100644
index 0000000..3cb8157
--- /dev/null
+++ b/server/authConfig/jwt.config.js
@@ -0,0 +1,43 @@
+const passport = require('passport');
+const User = require('../models/userModel.js');
+
+const JwtStrategy = require('passport-jwt').Strategy,
+ ExtractJwt = require('passport-jwt').ExtractJwt;
+
+// load env variables
+require('dotenv').config();
+const secret = process.env.JWT_SECRET;
+
+// Passport JWT config
+const options = {};
+options.jwtFromRequest = (req) => {
+ let token = null;
+ if (req && req.cookies) {
+ token = req.cookies['token'];
+ }
+ return token;
+};
+options.secretOrKey = secret;
+
+// new JWT strategy
+const jwtStrategy = new JwtStrategy(options,
+ async (payload, done) => {
+ try{
+ // find user based on JWT payload
+ const user = await User.findById(payload.userId);
+ if (user) {
+ // if user is found, return user
+ return done(null, user);
+ } else {
+ // if user is not found, return false
+ return done(null, false);
+ // or create a new account
+ }
+ } catch (err) {
+ // if error occurs pass to done callback
+ return done(err);
+ }
+ }
+);
+
+module.exports = jwtStrategy;
\ No newline at end of file
diff --git a/server/authConfig/passport.js b/server/authConfig/passport.js
new file mode 100644
index 0000000..4d06a1a
--- /dev/null
+++ b/server/authConfig/passport.js
@@ -0,0 +1,52 @@
+const bcrypt = require('bcrypt');
+const passport = require('passport');
+const User = require('../models/userModel.js');
+const LocalStrategy = require('passport-local').Strategy;
+const jwt = require('jsonwebtoken');
+const jwtStrategy = require('./jwt.config');
+
+
+passport.use(jwtStrategy);
+passport.use(new LocalStrategy({
+ usernameField: 'username', // field name for username in req body
+ passwordField: 'password', // field name for password in req body
+}, async (username, password, done) => {
+ try {
+ const user = await User.findOne({ username });
+
+ if (!user) {
+ // User not found
+ return done(null, false, {message: 'Incorrect username or password'});
+ }
+
+ // unhash stored password with bcrypt and compare to input password
+ const passwordMatch = await bcrypt.compare(password, user.password);
+
+ if (!passwordMatch) {
+ // incorrect password
+ return done(null, false, {message: 'Incorrect username or password'});
+ }
+
+ // Auth successful, return the authenticated user
+ return done(null, user);
+ } catch (err) {
+ // Error occured during the auth process
+ return done(`Error occured during the auth process ${err}`);
+ }
+}
+));
+
+// Serialize the user object into a session
+passport.serializeUser((user, done) => {
+ done(null, user.id);
+});
+
+// Deserialize the user object from a session
+passport.deserializeUser(async (id, done) => {
+ try {
+ const user = await User.findById(id);
+ done(null, user);
+ } catch (err) {
+ done(err);
+ }
+});
diff --git a/server/controllers/authenticationController.js b/server/controllers/authenticationController.js
new file mode 100644
index 0000000..11a2c43
--- /dev/null
+++ b/server/controllers/authenticationController.js
@@ -0,0 +1,34 @@
+const passport = require('../authConfig/passport.js');
+const User = require('../models/userModel.js');
+const bcrypt = require('bcrypt');
+const authenticationController = {};
+
+authenticationController.signUp = async (req, res, next) => {
+ const { username, password } = req.body;
+ try {
+ const existingUser = await User.findOne({ username });
+
+ if (existingUser) {
+ return next({
+ log: 'Error occured in authenticationController.signUp',
+ status: 400,
+ message: 'Username already exists, please select another',
+ });
+ }
+
+ // password encryption
+ const salt = await bcrypt.genSalt(10);
+ const hashedPassword = await bcrypt.hash(password, salt);
+
+ const newUser = await User.create({
+ username: req.body.username,
+ password: hashedPassword,
+ });
+ res.locals.newUser = newUser;
+ return next();
+ } catch (err) {
+ return next(err);
+ }
+};
+
+module.exports = authenticationController;
diff --git a/server/controllers/snippetsController.js b/server/controllers/snippetsController.js
index 864cf42..5d6ff8b 100644
--- a/server/controllers/snippetsController.js
+++ b/server/controllers/snippetsController.js
@@ -1,13 +1,30 @@
const User = require('../models/userModel.js');
-
+const Snippet = require('../models/snippetModel.js');
const snippetsController = {};
-snippetsController.getSnippets = (req, res, next) => {
- const userId = '645fee9104d1f0acef95a002';
+//Error creator method specific to this controller
+const createError = (method, log, status, message = log) => {
+ return {
+ log: `Error occurred in snippetsController.${method}: ${log}`,
+ status,
+ message: { err: message }
+ };
+};
+
+//Retrieves all snippets associated with a user by looking up user (by ID) and referencing all snippets in the associated list
+//NOTE: WE SHOULD REALLY SEPARATE OUT STUFF LIKE THIS INTO A SEPARATE USER ROUTE AND USER CONTROLLER
+snippetsController.getSnippetsByUser = (req, res, next) => {
+ console.log('In get snippets...');
+ const userId = req.user._id.toString();
+ console.log('Logging ID...');
+ console.log(userId);
- User.findOne({ _id: userId })
+ User.findById(userId)
+ .populate('snippets')
+ .exec()
.then((user) => {
- res.locals.allSnippets = user;
+ res.locals.allSnippets = user.snippets;
+ res.locals.userTagsLangs = { tags: user.tags, languages: user.languages };
return next();
})
.catch((err) => {
@@ -16,121 +33,203 @@ snippetsController.getSnippets = (req, res, next) => {
});
};
+//Creates snippet under Snippets collection
snippetsController.createSnippet = (req, res, next) => {
const { title, comments, storedCode, tags, language } = req.body;
const snippet = { title, comments, storedCode, tags, language };
- const userId = '645fee9104d1f0acef95a002';
+ Snippet.create(snippet)
+ .then((doc) => {
+ res.locals.newSnippet = doc;
+ return next();
+ })
+ .catch((err) => {
+ return next(
+ createError('createSnippet', `Error creating new snippet: ${err}`, 500)
+ );
+ });
+};
+//Associates snippet with a particular user
+snippetsController.saveSnippetToUser = (req, res, next) => {
+ const userId = req.user._id.toString();
User.findById(userId)
.then((user) => {
- // Increment the lastId and assign it to the new snippet
- const newSnippetId = user.lastId + 1;
- user.lastId = newSnippetId;
-
- // Create the new snippet object with the assigned ID
- const newSnippet = {
- id: newSnippetId,
- ...snippet,
- };
-
- // Push the new snippet to the snippets array
- user.snippets.push(newSnippet);
-
- const [tags, languages] = recalcTagsAndLang(user);
- user.tags = tags;
- user.languages = languages;
-
- // Save the updated user document
- return user.save().then((updatedUser) => {
- res.locals.createdSnippet = newSnippet;
- next();
- });
+ user.snippets.push(res.locals.newSnippet._id);
+ user
+ .save()
+ .then((r) => {
+ res.locals.updatedUserRecord = r;
+ res.locals.changeFlag = true;
+ return next();
+ })
+ .catch((err) => {
+ return next(
+ createError(
+ 'saveSnippetToUser',
+ `Encountered error when saving new snippet to user: ${err}`,
+ 500
+ )
+ );
+ });
})
- .catch((error) => {
- console.error('Creating a snippet has failed:', error);
- next(error);
+ .catch((err) => {
+ return next(
+ createError(
+ 'saveSnippetToUser',
+ `Encountered error when retrieving user record: ${err}`,
+ 500
+ )
+ );
});
};
-
+
+//Updates snippet with provided properties
snippetsController.updateSnippet = (req, res, next) => {
- const { id, title, comments, storedCode, tags, language } = req.body;
- const updatedSnippet = { id, title, comments, storedCode, tags, language };
- const userId = '645fee9104d1f0acef95a002';
-
- User.findOneAndUpdate(
- { _id: userId, 'snippets.id': updatedSnippet.id },
- {
- $set: { 'snippets.$': updatedSnippet },
- },
- { new: true }
+ const { snippetId } = req.query;
+ const { title, comments, storedCode, tags, language } = req.body;
+ const updatedSnippet = { title, comments, storedCode, tags, language };
+
+ for (const key in updatedSnippet) {
+ if (!updatedSnippet[key]) {
+ delete updatedSnippet[key];
+ }
+ }
+
+ //Need to work out how best to update user tags, languages under this new approach
+
+ Snippet.findByIdAndUpdate(
+ snippetId,
+ { ...updatedSnippet },
+ { new: false, upsert: true }
)
- .then((updatedUser) => {
- const [tags, languages] = recalcTagsAndLang(updatedUser);
- updatedUser.tags = tags;
- updatedUser.languages = languages;
- return updatedUser.save();
- })
- .then((savedUser) => {
- res.locals.updatedSnippet = updatedSnippet;
- next();
+ .exec()
+ .then((result) => {
+ //Compare tags, languages in original and update to enable user refresh
+ res.locals.updatedSnippet = result;
+ let removedTags, addedTags, oldLang, newLang;
+
+ if (result.tags !== tags || result.language !== language) {
+ res.locals.changeFlag = true;
+ }
+
+ return next();
})
.catch((err) => {
- console.log('Updating the snippet has failed:', err);
- next(err);
+ return next(
+ createError(
+ '.updateSnippet',
+ `Encountered error while updating snippet: ${err}`,
+ 500
+ )
+ );
});
};
+//Deletes snippet with provided ID and removes from users with associated ID
snippetsController.deleteSnippet = (req, res, next) => {
- const { id } = req.query;
- const userId = '645fee9104d1f0acef95a002';
+ const userId = req.user._id.toString();
+ const { snippetId } = req.query;
+ Snippet.findByIdAndDelete(snippetId)
+ .exec()
+ .then((result) => {
+ res.locals.deletedSnippet = result;
+ })
+ .then(() => {
+ User.findById(userId)
+ .exec()
+ .then((user) => {
+ user.snippets = user.snippets.filter((el) => el != snippetId);
+ user
+ .save()
+ .then((updatedUser) => {
+ res.locals.changeFlag = true;
+ res.locals.updatedUserRecord = updatedUser;
+ return next();
+ })
+ .catch((err) => {
+ return next(
+ createError(
+ 'deleteSnippet',
+ `Encountered error while saving updated user snippets list: ${err}`,
+ 500
+ )
+ );
+ });
+ })
+ .catch((err) => {
+ return next(
+ createError(
+ 'deleteSnippet',
+ `Encountered error while retrieving user with provided ID: ${err}`,
+ 500
+ )
+ );
+ });
+ })
+ .catch((err) => {
+ return next(
+ createError(
+ 'deleteSnippet',
+ `Encountered error while attempting to delete snippet with provided ID: ${err}`,
+ 500
+ )
+ );
+ });
+};
- User.findOne({ _id: userId })
+snippetsController.recalcTagsAndLang = (req, res, next) => {
+ if (!res.locals.changeFlag) {
+ return next();
+ }
+
+ const userId = req.user._id.toString();
+ const tagList = new Set();
+ const languageList = new Set();
+ console.log(userId);
+ User.findById(userId)
+ .populate('snippets')
+ .exec()
.then((user) => {
- const deletedSnippet = user.snippets.find((snippet) => {
- return `${snippet.id}` === id;
+ console.log(user);
+ user.snippets.forEach((snippet) => {
+ snippet.tags.forEach((tag) => {
+ if (!tagList.has(tag)) {
+ tagList.add(tag);
+ }
+ });
+
+ if (!languageList.has(snippet.language)) {
+ languageList.add(snippet.language);
+ }
});
- // Remove the snippet from the user's snippets array
- user.snippets = user.snippets.filter((snippet) => `${snippet.id}` !== id);
-
- //recalculate the tags and languages.
- const [tags, languages] = recalcTagsAndLang(user);
- user.tags = tags;
- user.languages = languages;
-
- // Save the updated user document
- return user.save().then(() => {
- res.locals.deletedSnippet = deletedSnippet;
- next();
- });
+ user.tags = Array.from(tagList);
+ user.languages = Array.from(languageList);
+ user
+ .save()
+ .then((usr) => {
+ res.locals.updatedUserRecord = usr;
+ return next();
+ })
+ .catch((err) => {
+ return next(
+ createError(
+ '.recalcTagsAndLang',
+ `Error saving new tags, languages to user: ${err}`,
+ 500
+ )
+ );
+ });
})
- .catch((error) => {
- console.error('Error deleting snippet:', error);
- next(error);
+ .catch((err) => {
+ return next(
+ createError(
+ '.recalcTagsAndLanguages',
+ `Error locating user for update: ${err}`,
+ 500
+ )
+ );
});
};
-// helper function to re-calculate taglist/language counts?
-const recalcTagsAndLang = function (user) {
- const tagList = {};
- const languageList = {};
-
- for (const snippet of user.snippets) {
- if (Array.isArray(snippet.tags)) {
- for (const tag of snippet.tags) {
- if (!tagList[tag]) {
- tagList[tag] = [];
- }
- tagList[tag].push(snippet);
- }
-
- if (!languageList[snippet.language]) {
- languageList[snippet.language] = [];
- }
- languageList[snippet.language].push(snippet);
- }
- }
- //return something here.
- return [tagList, languageList];
-};
module.exports = snippetsController;
diff --git a/server/models/snippetModel.js b/server/models/snippetModel.js
new file mode 100644
index 0000000..a3a1a39
--- /dev/null
+++ b/server/models/snippetModel.js
@@ -0,0 +1,12 @@
+const mongoose = require('mongoose');
+const Schema = mongoose.Schema;
+
+const snippetSchema = new Schema({
+ title: { type: String, required: true },
+ comments: { type: String },
+ storedCode: { type: String },
+ tags: [String],
+ language: { type: String }
+});
+
+module.exports = mongoose.model('Snippet', snippetSchema);
diff --git a/client/src/components/ui/TagInput/TagInput.module.scss b/server/models/tagModel.js
similarity index 100%
rename from client/src/components/ui/TagInput/TagInput.module.scss
rename to server/models/tagModel.js
diff --git a/server/models/userModel.js b/server/models/userModel.js
index a29070c..912910d 100644
--- a/server/models/userModel.js
+++ b/server/models/userModel.js
@@ -2,34 +2,19 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
- username: { type: String, required: true },
+ username: { type: String, required: true, unique: true },
password: { type: String, required: true },
- tags: {
- type: Object,
- default: {},
- },
- languages: {
- type: Object,
- default: {},
- },
-
- lastId: { type: Number, default: 0 },
-
+ tags: { type: [String], default: [] },
+ languages: { type: [String], default: [] },
snippets: {
type: [
{
- id: { type: Number, required: true },
- type: Object,
- title: { type: String, required: true },
- comments: { type: String },
- storedCode: { type: String },
-
- tags: [String],
- language: { type: String },
- },
+ type: Schema.Types.ObjectId,
+ ref: 'Snippet'
+ }
],
- default: [],
- },
+ default: []
+ }
});
module.exports = mongoose.model('User', userSchema);
diff --git a/server/routes/authenticationRouter.js b/server/routes/authenticationRouter.js
new file mode 100644
index 0000000..f78606f
--- /dev/null
+++ b/server/routes/authenticationRouter.js
@@ -0,0 +1,43 @@
+const express = require('express');
+const router = express.Router();
+const passport = require('passport');
+const jwt = require('jsonwebtoken');
+
+const authenticationController = require('../controllers/authenticationController');
+
+require('dotenv').config();
+const secret = process.env.JWT_SECRET;
+
+router.post('/signup', authenticationController.signUp, (req, res) => {
+ return res.status(201).json({ username: res.locals.newUser.username });
+});
+
+router.post('/login',
+ passport.authenticate('local', { session: false }),
+ (req, res) => {
+ console.log(req.user);
+ const token = jwt.sign({ userId: req.user.id }, secret, {
+ expiresIn: '1d',
+ });
+ res.cookie('token', token, {
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Expires in 30 days
+ httpOnly: true,
+ });
+ res.cookie('userId', req.user.id, {
+ expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Expires in 30 days
+ httpOnly: true,
+ });
+ return res.status(202).json({ username: req.user.username });
+ }
+);
+
+router.get(
+ '/protected',
+ passport.authenticate('jwt', { session: false }),
+ (req, res) => {
+ console.log('at protected router, SUCCESS!');
+ res.send('Protected route accessed!');
+ }
+);
+
+module.exports = router;
diff --git a/server/routes/snippets.js b/server/routes/snippets.js
deleted file mode 100644
index 4e1b5c2..0000000
--- a/server/routes/snippets.js
+++ /dev/null
@@ -1,25 +0,0 @@
-const express = require('express');
-
-const snippetsController = require('../controllers/snippetsController');
-
-const router = express.Router();
-
-router.get('/', snippetsController.getSnippets, (req, res) =>
- res.status(200).json(res.locals.allSnippets)
-);
-
-router.post('/', snippetsController.createSnippet, (req, res) =>
- res.status(200).json(res.locals.createdSnippet)
-);
-
-router.put('/', snippetsController.updateSnippet, (req, res) =>
- res.status(200).json(res.locals.updatedSnippet)
-);
-
-router.delete('/', snippetsController.deleteSnippet, (req, res) =>
- res.status(200).json(res.locals.deletedSnippet)
-);
-
-router.use((req, res) => res.status(404).send('Invalid endpoint'));
-
-module.exports = router;
diff --git a/server/routes/snippetsRouter.js b/server/routes/snippetsRouter.js
new file mode 100644
index 0000000..c23a306
--- /dev/null
+++ b/server/routes/snippetsRouter.js
@@ -0,0 +1,57 @@
+const express = require('express');
+const passport = require('passport');
+
+const snippetsController = require('../controllers/snippetsController');
+
+const router = express.Router();
+
+router.get(
+ '/',
+ passport.authenticate('jwt', { session: false }),
+ snippetsController.getSnippetsByUser,
+ (req, res) => {
+ return res
+ .status(200)
+ .json({
+ snippets: res.locals.allSnippets,
+ tagsLangs: res.locals.userTagsLangs
+ });
+ }
+);
+
+router.post(
+ '/',
+ passport.authenticate('jwt', { session: false }),
+ snippetsController.createSnippet,
+ snippetsController.saveSnippetToUser,
+ snippetsController.recalcTagsAndLang,
+ (req, res) => res.status(200).json(res.locals.newSnippet)
+);
+
+router.put(
+ '/',
+ passport.authenticate('jwt', { session: false }),
+ snippetsController.updateSnippet,
+ snippetsController.recalcTagsAndLang,
+ (req, res) =>
+ res.status(200).json({
+ snippet: res.locals.updatedSnippet,
+ userData: res.locals.updatedUserRecord
+ })
+);
+
+router.delete(
+ '/',
+ passport.authenticate('jwt', { session: false }),
+ snippetsController.deleteSnippet,
+ snippetsController.recalcTagsAndLang,
+ (req, res) =>
+ res.status(200).json({
+ snippet: res.locals.deletedSnippet,
+ userData: res.locals.updatedUserRecord
+ })
+);
+
+router.use((req, res) => res.status(404).send('Invalid endpoint'));
+
+module.exports = router;
diff --git a/server/server.js b/server/server.js
index 855cf32..c72030b 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,37 +1,55 @@
-const path = require('path');
const express = require('express');
-const app = express();
const mongoose = require('mongoose');
const cors = require('cors');
+const passport = require('passport');
+const cookieParser = require('cookie-parser');
-const port = process.env.PORT || 3000;
+const snippetsRouter = require('./routes/snippetsRouter');
+const authenticationRouter = require('./routes/authenticationRouter');
-const mongoURI =
- 'mongodb+srv://paaoul:Melikeit1@scratchcluster.igf2bag.mongodb.net/';
-mongoose.connect(mongoURI);
+require('dotenv').config();
+
+//Create express app and set constants
+const app = express();
+const port = process.env.PORT || 3000;
-const snippetsRouter = require('./routes/snippets');
+//Get mongoURI and connect to DB
+const mongoURI = process.env.MONGO_URI;
+mongoose.connect(mongoURI);
-app.use(cors());
+// initialize passport
+app.use(passport.initialize());
+// parse incoming cookies to authentication endpoints and store them on req.cookies object
+app.use(cookieParser());
+// allow cookies to be included in CORS request
+app.use(cors({
+ origin: 'http://localhost:8080',
+ credentials: true
+}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+
+//Point relevant requests to snippet and authentication routers
+
app.use('/snippets', snippetsRouter);
+app.use('/authentication', authenticationRouter);
+//Handle requests to invalid endpoints and middleware errors
app.use((req, res) => res.status(404).send('Invalid endpoint'));
-
app.use((err, req, res, next) => {
const defaultErr = {
log: 'Express error handler caught unknown middleware error',
status: 500,
- message: { err: 'An error occurred' },
+ message: { err: 'An error occurred' }
};
const errorObj = Object.assign({}, defaultErr, err);
console.log(errorObj.log);
return res.status(errorObj.status).json(errorObj.message);
});
+//Get 'er goin'
app.listen(port, () => {
console.log(`Server listening on port ${port}...`);
});
diff --git a/webpack.config.js b/webpack.config.js
index 57743ec..aa87539 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -14,7 +14,7 @@ module.exports = {
rules: [
{
test: /\.(png|jp(e*)g|svg|gif)$/,
- type: 'asset/resource',
+ type: 'asset/resource'
},
{
test: /\.jsx?/,
@@ -24,27 +24,25 @@ module.exports = {
options: {
presets: [
'@babel/preset-env',
- ['@babel/preset-react', { runtime: 'automatic' }],
- ],
- },
- },
+ ['@babel/preset-react', { runtime: 'automatic' }]
+ ]
+ }
+ }
},
{
test: /\.s?css/,
- use: [
- 'style-loader', 'css-loader', 'sass-loader'
- ]
+ use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
- template:'./public/index.html'
+ template: './public/index.html'
})
],
resolve: {
- extensions: ['.js', '.jsx', '.scss'],
+ extensions: ['.js', '.jsx', '.scss']
},
devServer: {
static: {
@@ -52,8 +50,8 @@ module.exports = {
directory: path.resolve(__dirname, 'dist')
},
proxy: {
- '/snippets': 'http://localhost:3000'
+ '/': 'http://localhost:3000'
}
},
devtool: 'eval-source-map'
-};
\ No newline at end of file
+};