diff --git a/.eslintignore b/.eslintignore index 7ae9e4b..7895f7e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ models/index.js -node_modules +node_modules \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 4a807a6..e3a35da 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,46 +1,45 @@ -// { -// "env": { -// "browser": true, -// "commonjs": true, -// "node": true, -// "es6": true -// }, -// "rules": { -// "no-duplicate-case":"error", -// "no-empty":"error", -// "no-extra-semi":"error", -// "no-func-assign": "error", -// "no-irregular-whitespace":"error", -// "no-unreachable": "error", -// "curly": "error", -// "dot-notation": "error", -// "eqeqeq": "error", -// "no-empty-function": "error", -// "no-multi-spaces": "error", -// "no-mixed-spaces-and-tabs": "error", -// "no-trailing-spaces": "error", -// "default-case": "error", -// "no-fallthrough": "error", -// "no-unused-vars": "error", -// "no-use-before-define": "error", -// "no-redeclare": "error", -// "camelcase": "error", -// "brace-style": "error", -// "indent": [ -// "error", -// 2 -// ], -// "linebreak-style": [ -// "error", -// "windows" -// ], -// "quotes": [ -// "error", -// "double" -// ], -// "semi": [ -// "error", -// "always" -// ] -// } -// } +{ + "env": { + "browser": true, + "commonjs": true, + "node": true, + "es6": true + }, + "rules": { + "no-duplicate-case":"error", + "no-empty":"error", + "no-extra-semi":"error", + "no-func-assign": "error", + "no-irregular-whitespace":"error", + "no-unreachable": "error", + "curly": "error", + "dot-notation": "error", + "eqeqeq": "error", + "no-empty-function": "error", + "no-multi-spaces": "error", + "no-mixed-spaces-and-tabs": "error", + "no-trailing-spaces": "error", + "default-case": "error", + "no-fallthrough": "error", + "no-use-before-define": "error", + "no-redeclare": "error", + "camelcase": "error", + "brace-style": "error", + "indent": [ + "error", + 2 + ], + // "linebreak-style": [ + // "error", + // "windows" + // ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b512c09..55371e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a2e1b3e..17805cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,4 @@ cache: services: - mysql before_install: - - mysql -e 'CREATE DATABASE IF NOT EXISTS blog_machine;' + - mysql -e 'CREATE DATABASE IF NOT EXISTS blog_machine;' \ No newline at end of file diff --git a/BlogMachine.gif b/BlogMachine.gif new file mode 100644 index 0000000..dda1297 Binary files /dev/null and b/BlogMachine.gif differ diff --git a/README.md b/README.md index 43bd19d..bcdcab4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,119 @@ # BlogMachine -An ap where users can sign into their account and make blog posts. - Heroku Live Link: https://blog-machine.herokuapp.com/ -To get started, first install dependencies: +[![License: NPM](https://img.shields.io/badge/License-NPM%20Package-green.svg)](https://www.npmjs.com/) + + ## Description + +A social media app that lets user create accounts, log in, and post blogs. It also lets users view other member's profiles and other member's blogs, search for blogs, and even filter blogs. + +BlogMachine demo gif: + +![BlogMachine](./BlogMachine.gif) + +# Table of Contents +- [Installation](#installation) +- [Usage](#usage) +- [Contributors](#contributors) +- [Tests](#tests) +- [Layers](#layers) + - [Database](#database) + - [Config](#config) + - [Models](#models) + - [Routes](#routes) + - [Public](#public) +- [License](#license) +- [Questions](#questions) + +## Installation + +The BlogMachine app includes the following dependencies: bcryptjs, express, express-handlebars, express-session, multer, mysql2, passport, passport-local, sequelize, and sequelize-cli. It also utlizes eslint and Travis CI for the tests and for indentation, quotation, and other miscellaneous errors. + +To install dependencies, run the following command: ``` npm i ``` -Then query the [scheema.sql file](./scheema.sql) in MySQL workbench or in the MySQL Shell. +## Usage + +You may use this app live on [Heroku deployed app](https://blog-machine.herokuapp.com/). + +To run the app locally, first query the [scheema.sql file](./scheema.sql) in MySQL workbench or in the MySQL Shell to create the blog-machine database. + +Then install dependencies seen in [Installation](#installation). -Then you should be able to deploy locally by running: +Then run the following command: ``` npm start -``` \ No newline at end of file +``` + +## License +This application is covered by: NPM Package + +## Contributors +[Vincent McGargill](https://github.com/vmcgargill) +[Athena Petrovich](https://github.com/strawberryboar) +[Maynard Peralta](https://github.com/maynperalta) + +## Tests +``` +npm test +``` + +## Layers + +This project is done in a MVC style design. It has 5 layers: database, models, config, routes, and public/views. + +### Database + +The database layers holds the database itself, which stores all of the root information of all 3 tables: users, blogs, and categories. The application accesses and modifies this information by using the sequelize ORM. Most of the queries can be seen in the routes layer when the front end makes an API call. The database layer is very basic and is unseen by the user's eyes as it serves one soul purpose: to store information. Some of this information should also not be seen by the public's eye regardless, such as the hashed version of each user password. + +### Config + +The config layer holds all of the middleware that is used in the routes layer. It includes the local database configuration in config.json for testing purposes. It also holds the multer.js middleware which is used for uploading files. There is also passport.js which is used for authenticating users accounts for when they sign in. There is also the isAuthenticated middleware which verifies if the user is signed in or authenticated. The isAuthenticated middleware is used for HTML pages and API calls that are meant to be used only by users who are logged into an account. If the user is not logged in, then they are redirected to the login page. Finally, there is the isBlogOwner middleware which is specifically used for verifying if a user is the owner of a blog when they are trying to modifiy and delete a blog. The isBlogOwner middleware is a security protocal to make sure no one hacks the website from the front end and updates or deletes another users blog. Otherwise, without this user verifying middleware that checks if the user making these changes is the blog owner, any user could go into the source code in their browser by using inspect element and make changes or even delete a blog that is not their own. You can see this used specifically in the routes layer for updating and deleting blog API calls. + +### Models + +The models layer contains all of the bluprints for the MySQL database tables used in this app. First off we have the user table which contains the following rows: email, password, name, picture, title, bio, website, hobbies, and interests. The email and password are specifically used for signing into the website and authenticating an account. The email can also be used as a point of contact information for the owner of the profile as it will display on their profile. The name is the display name that the user chooses, this may be their real name. The title can be whatever title the user choses for themselves or for their profile, for example a job title or occupation can be a title. The bio is the user's personal biography that they wish to share with other users. The picture is a path link to the user's profile picture that they uploaded. The website is a link to their very own website, a portfolio website would be an example. The hobbies and interests are another optional bit of details that the user may share on the website. All of the datatypes listed in the user table are all strings. Finally, there is also the user ID which is automatically generated by sequelize, and it is used to identify the user, but also used as a foreign key in the blogs table to identify which user posted the blog or which user is allowed to make updates or delete the blog. We can see associations made in the Models layer between blogs and the users AKA the creators of the blogs. Each blog has one user who created it. Each user may have many blogs that they have created, and if a user is deleted then all of the blogs they posted on their account will be deleted or cascaded. A potential added feature for this site would be to add the ability to make certain users site administratiors who can do special things such as edit or delete other user's blogs, or even edit/delete other user profiles to help police the site incase a user goes rogue and does bad things or makes inappropriate posts on the website. Another potential feature wold be to ban users from the website. + +The blog table has 3 rows: the title, the body, and the mood which are all string or text datatypes. The title and the body are pretty self explanitory, they are the title of the blog and the whole body of text of the blog. Then there is the mood row which is a string that the user may select or create themselves which is a description of the mood of the blog or the current mood of the user posting the blog. The blog table also has 2 required foreign keys: the UserId and the CategoryId. The UserId identifies which user posted the blog as explained previously. The CategoryId is the id that identifies which category was selected for the blog. Categories can be created by any user in the category section of the project. Categories are protected: in other words a user cannot delete a category if it already has one or more blog assigned to it. This is intentional to prevent people from deleting blogs made by other users by deleting a specific category. + +Finally, there is the category table which only has 1 row: the name of the category. There is also the ID of the category which is used to identify the selected category and also used to identify a blog category as a foreign key in the blog table. This table is the simplest table in the database. It also has an association with Blogs as it is allowed to have many blogs in the same category. A potential added feature would be to make it so only site administators could create or delete categories. We could also add the feature to edit categories, but for the class assignment this seemed unnecessary because you already have the ability to create and delete categories. + +### Routes + +The routes layer is the layer where all of the interactions with the front end and backend are made. The routes layer uses middleware configurations from the config layer, database queries from the models layer using the sequelize ORM, and it defines both HTML routes and API routes that are all used on the front end layer. There are 8 types of routes all seperated in their own files: general API routes, general HTML routes, blog API routes, blog HTML routes, categories API routes, categories HTML routes, user API routes, and finally user HTML routes. + +The genral API routes are located in the api-routes.js file and they include the login API route, the signup API route, and the logout API route. These API calls are used in the front in for users logging into their accounts, logging out of their accounts, and signing up for a new account. The general HTML routes which is located in the html-routes.js file is essentially the same thing except for it serves/renders HTML handlebar pages and javascript pages to the user when they visit the home page, the login page, or the signup page using those URL paths in the web browser. + +The blogs API routes and the blogs HTML routes are also paired together. The blog API routes located in the blogs-api-routes.js file include all of the blog API calls that are related to getting blog information. There is a general blog API call that accepts filtered queries for a list of blogs and returns and renders a list of those blogs in a neat HTML template using handlebars. There is also an API call for loading blog search suggestions, for creating blogs, updating blogs, deleting blogs, and viewing a specific blog's details which is used specifically for editing blogs in the postblog.js file in the public layer. As you can see, some of these API calls are protected and use the isAuthenticated and isBlogOwner middleware. The blog HTML routes include the blog page which includes the blog filter, the viewblog page which shows the user a specific blog in full detail with the full description. It also has the blog search results page which shows the user their results when they search for a blog, the post blog page, and then the edit blog page. + +The categories API routes and categories HTML routes are paired together and they can be found respectively in the categories-api-routes.js file and the categories-html-routes.js file. This part of the routes layer is much simpler because there is only 1 HTML route and 2 API routes. The HTML route is the route for the viewcategories page where users can create new categories and delete unused categories. The API categories routes include a post category route and a delete category route. They are pretty self explanatory. + +The user API routes and HTML routes can be found in the users-api-routes.js file and the users-html-routes.js file. The API routes seen in this section of the routes include routes to edit a user profile and delete a user profile. This is where we can see the multer.js middleware being used to upload profile pictures from the config layer. There was no need for middleware for checking if the user is the owner of the profile as we can see who is making this request. However, if we wanted to implement a system where we can have site administrators edit or delete another user's profile for site regulation, then we shoulf probably modify these API routes or create entirely new routes that are special for site administrators, along with creating some new middleware. The user HTML routes include the members page which displays all members on the site, the UserProfile page which just checks the user that is signed in and redirects them to their own profile, the view profile route which is used to view a member's profile by their Id, the edit profile page, and the delete profile page. Again, if we were to add a site admin feature we would have to edit some of these routes, or create new routes with new middleware for checking if the user is a website admin. + +### Public + +The public layer includes the views directory and the public directory. The public direcory is a static directory, meaning Express makes it avilable to view for the public in the web browser. It is mostly made up with JavaScript and jQuery button click events, AJAX API calls, dropdown swtch event listeners, and form submit event listeners. These listeners are what makes the website responsive to the user so they can interact with the website, view blogs, post blogs, edit blogs, delete blogs, make changes to their profile, create a profile, delete a profile, create categories, and delete categores. In the public directory you can see 3 sub directories: default, js, stylesheets, and upload. The default directory will hold the default profile picture for when a user signs up for an account and has not uploaded a picture yet. The upload directory will contain all uploaded profile pictures for the website. When a user uploads a new profile picture, it will apear here and can be accessed here by the HTML pages. If a user uploads a new peofile picture, the old profile picture will be removed or deleted here if it is not the default picture. The default picture is in it's own directory to protect it. The js directory holds all of the JavaScript files and is organized in 3 sub directories: blogs, categories, and users. There is also the JavaScript in the root js directory for the error function, the home page, the login page, the signup page, and the navbar stuff. + +The views direcory will hold of the HTML Express Handlebars files. It is also organized into sub-directories: blogs, layouts, partials, and users. In the root of the views directory we can also see general pages such as the viewcategory page, the signup page, login page, search results page, and the home page. The blogs directory includes the handlbar layouts for the blogs filter page, the post blog page, and the userblog page which will have the viewblog partial and include a edit blog button and delete blog button. There is also the users directory which includes the deleteprofile layout, edit profile, the members page which displays all site members, member profile, and user profile. Member profile is specifically used for when one user is viewing another user's profile. The userprofile page is used for when a user is viewing their own prfile that they are signed into as it also includes a edit profilr button and delete profile button. Both of these pages use the viewmember partial. Originally it was desgned for the view member partial to be used as the memberprofile page, but this was later modified so that the edit profile and delete profile buttons could be displayed under the profile details and above the list of user blogs. + +Then their is the layouts directory which includes the main handlebars page. The layout directory is the primary express handlepar layout that is used for all HTML pages in all routes. It includes the header, footer, the navbars, and the javascript includes. All of the content is put inside the body brackets. The purpose of this is so all pages have the same header, footer, navbar, and they also include different javascript files. We have modified it so that based on what HTML route is being called, it renders a specific JavaScript script for that file. Of course the navbar, header, and footer are not actually located in this main.handlebars file, they are actually located in express handlebars patials to keep the handlebar layout more organized. The purpose of this is so that you don't have to sort through a bunch of HTML code to find the navbars and make modifications to it. All you have to do is locate the navbar.handlebars file and that file includes the entire navbar. Then you only have to make modifications to that handlebar file if you want to add or remove a feature. + +As mentioned previously, this app utilizes express handlbars partials. Express handlebars has a feature where in the partials directory, you can create basic HTML templates that can be used and imported into other handlebars files. This is essential for making your HTML more fluent, concise, and less repetitive. That way we can store the generic guts of most of our HTML in the partials directory so it can be used in other handlebar files multiple times. In the partials directory for example, you may see a file called error.handlebars. This file contains a very basic HTML template for an error message that is used on several other handlebar files. You can see it being used in the following fashion: + +``` +{{> error}} +``` + +All you have to do is include the handlbar partial name in curly brackets with an outward facing carrot to let handlbars know to include this partial located in the partial directory. We can see the viewblog partial in this directory which can be used alone for the viewblog feature for any user or it can be seen used in the userblog page as a partial so if the user is signed in and is the profile owner it will display the edit profile and delete profile buttons. That way only profile owners can see the edit profile and delete profile buttons. These feature is also used with the viewmember partial as mentioned before it is used both in the userprofile handlebar and memberprofile handlebar. The only difference is the userprofile will also display an edit profile and delete profile button. The partials directory also has the filterblogs partial which is a generic HTML layout for a list of blogs on a page, and again this partial is used to display lists of blogs on several different pages pages including blogs, home, userprofile, and memberprofile. The soul purpose of the filterblogs partial is to display a list of several blogs while limiting the amount of body displayed on the screen. The filterblogs hides the body so that it does not take up the entire screen for long blog posts, and it still includes that text so when a user does a local search query on the blogs filter pager, they can search for keywords in the body as well as the title, mood, category, or user. The partials directory also has the header, navbar, and footer partials which are only used in the main.handlebars file, but this is done so the code is more organized and easier to read in the main handlebars. It's not entirly necessary to do this because you could just include that all in the main.handlebars file since that file is the only one that uses those partials, but it is a best practice to do so to keep the code more organized and easier to read. + +## Questions +If you have any questions feel free to contact: + +[GitHub](https://github.com/vmcgargill) + +Email: [vincentmcgargill@gmail.com](mailto:vincentmcgargill@gmail.com) \ No newline at end of file diff --git a/config/middleware/isAuthenticated.js b/config/middleware/isAuthenticated.js index d5bd6c9..5fd11e6 100644 --- a/config/middleware/isAuthenticated.js +++ b/config/middleware/isAuthenticated.js @@ -1,4 +1,4 @@ -module.exports = function(req, res, next) { +module.exports = function (req, res, next) { // If the user is logged in, continue with the request to the restricted route if (req.user) { return next(); diff --git a/config/middleware/isBlogOwner.js b/config/middleware/isBlogOwner.js new file mode 100644 index 0000000..2691fe7 --- /dev/null +++ b/config/middleware/isBlogOwner.js @@ -0,0 +1,27 @@ +const db = require("../../models"); + +// This is a middleware function that determines if the signed in user is the blog owner and if they are allowed to edit/delete a blog. +// Only blog owners are allowed to edit and delete their own blogs. +// A potential added feature would be to make certain users site admins. +// Then we could also allow site admins here for viewing the blogs. +module.exports = function (req, res, next) { + let blogId; + + // Determines ing blog ID is query or parameter + if (req.query.blog_id) { + blogId = req.query.blog_id; + } else if (req.params.id) { + blogId = req.params.id; + } + + // If user is blog owner, return next callback. Else if user is not blog owner, redirect them to the index page because they are not allowed to be there + db.Blog.findOne({ + where: { id: blogId } + }).then(function (blog) { + if (blog.UserId === req.user.id) { + return next(); + } else { + res.redirect("/"); + } + }); +}; \ No newline at end of file diff --git a/config/multer 2.js b/config/multer 2.js deleted file mode 100644 index 22db6a8..0000000 --- a/config/multer 2.js +++ /dev/null @@ -1,29 +0,0 @@ -const multer = require("multer"); - -const storage = multer.diskStorage({ - destination: function(req, file, cb) { - cb(null, "./public/upload/") - }, - filename: function(req, file, cb) { - console.log(new Date().toISOString()) - cb(null, new Date().toISOString().replace(/:/g, "c") + file.originalname) - } -}); - -const fileFilter = (req, file, cb) => { - if (file.mimetype === "image/jpeg" || file.mimetype === "image/png" || file.mimetype === "image/gif") { - cb(null, true); - } else { - cb(null, false); - } -} - -const upload = multer({ - storage: storage, - limits: { - fileSize: 5000000 - }, - fileFilter: fileFilter -}) - -module.exports = upload; \ No newline at end of file diff --git a/config/multer.js b/config/multer.js index 22db6a8..aa789ac 100644 --- a/config/multer.js +++ b/config/multer.js @@ -1,29 +1,32 @@ const multer = require("multer"); +// Storage defines the directory that the profile pictures are stared, in this case the uploads directory in the public layer. const storage = multer.diskStorage({ destination: function(req, file, cb) { - cb(null, "./public/upload/") + cb(null, "./public/upload/"); }, filename: function(req, file, cb) { - console.log(new Date().toISOString()) - cb(null, new Date().toISOString().replace(/:/g, "c") + file.originalname) + console.log(new Date().toISOString()); + cb(null, new Date().toISOString().replace(/:/g, "c") + file.originalname); } }); +// Filters files to make sure only images or gifs are uploaded const fileFilter = (req, file, cb) => { if (file.mimetype === "image/jpeg" || file.mimetype === "image/png" || file.mimetype === "image/gif") { cb(null, true); } else { cb(null, false); } -} +}; +// Upload multer function uses the storage function and fileFilter function seen above and determins the filesize to be 5 MB const upload = multer({ storage: storage, limits: { fileSize: 5000000 }, fileFilter: fileFilter -}) +}); module.exports = upload; \ No newline at end of file diff --git a/config/passport.js b/config/passport.js index 8c3289a..716c721 100644 --- a/config/passport.js +++ b/config/passport.js @@ -7,18 +7,17 @@ passport.use(new LocalStrategy( { usernameField: "email" }, - function(email, password, done) { + function (email, password, done) { db.User.findOne({ where: { email: email } - }).then(function(dbUser) { + }).then(function (dbUser) { if (!dbUser) { return done(null, false, { message: "Incorrect email." }); - } - else if (!dbUser.validPassword(password)) { + } else if (!dbUser.validPassword(password)) { return done(null, false, { message: "Incorrect password." }); @@ -28,11 +27,11 @@ passport.use(new LocalStrategy( } )); -passport.serializeUser(function(user, cb) { +passport.serializeUser(function (user, cb) { cb(null, user); }); -passport.deserializeUser(function(obj, cb) { +passport.deserializeUser(function (obj, cb) { cb(null, obj); }); diff --git a/default 2.png b/default 2.png deleted file mode 100644 index 08b9c25..0000000 Binary files a/default 2.png and /dev/null differ diff --git a/default.png b/default.png deleted file mode 100644 index 08b9c25..0000000 Binary files a/default.png and /dev/null differ diff --git a/models/blog.js b/models/blog.js index ace98a9..c3d1635 100644 --- a/models/blog.js +++ b/models/blog.js @@ -1,17 +1,18 @@ -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { + // Defines Blog table with title, body, and mood strings var Blog = sequelize.define("Blog", { title: { type: DataTypes.STRING, allowNull: false, validate: { - len: [0, 50] + len: [1, 50] } }, body: { type: DataTypes.TEXT, allowNull: false, validate: { - len: [0, 100000] + len: [1, 100000] } }, mood: { @@ -23,18 +24,23 @@ module.exports = function(sequelize, DataTypes) { } }); - Blog.associate = function(models) { + // Associates blogs with Users and Categories, creates CategoryId and UserId foreign key rows. + Blog.associate = function (models) { + // If user is deleted, all of their blogs are deleted Blog.belongsTo(models.User, { + onDelete: "CASCADE" + }, { foreignKey: { allowNull: false } }); + // Cannot deleted category that has blogs assigned to it, they are protected. Blog.belongsTo(models.Category, { foreignKey: { allowNull: false } }); - } + }; return Blog; }; diff --git a/models/category.js b/models/category.js index c2f9aa5..b944b31 100644 --- a/models/category.js +++ b/models/category.js @@ -1,14 +1,16 @@ -module.exports = function(sequelize, DataTypes) { - var Category = sequelize.define("Category", { - name: { - type: DataTypes.STRING, - allowNull: false - } - }); - - Category.associate = function(models) { - Category.hasMany(models.Blog); - }; +module.exports = function (sequelize, DataTypes) { + // Created Category table with a name string row + var Category = sequelize.define("Category", { + name: { + type: DataTypes.STRING, + allowNull: false + } + }); - return Category; - }; \ No newline at end of file + // Associates category with the many blogs assigned to 1 category + Category.associate = function (models) { + Category.hasMany(models.Blog); + }; + + return Category; +}; \ No newline at end of file diff --git a/models/user.js b/models/user.js index ec2d88a..a6bdcdf 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,7 @@ const bcrypt = require("bcryptjs"); -module.exports = function(sequelize, DataTypes) { +module.exports = function (sequelize, DataTypes) { + // Defines the user table with all user information const User = sequelize.define("User", { email: { type: DataTypes.STRING, @@ -21,6 +22,8 @@ module.exports = function(sequelize, DataTypes) { type: DataTypes.STRING, allowNull: false }, + // Pictures are locally stored in the public/uploads/ directory instead of stored as a blob on the database. + // An additional feature could be to store pictures in database as blob row instead of as string of the file path. picture: { type: DataTypes.STRING, allowNull: true @@ -62,17 +65,18 @@ module.exports = function(sequelize, DataTypes) { } }); - User.associate = function(models) { - User.hasMany(models.Blog, { - onDelete: "cascade" - }); + // Associates users with the many blogs they own + User.associate = function (models) { + User.hasMany(models.Blog); }; - User.prototype.validPassword = function(password) { + // Makes sure hashed password can be compared to unhashed password + User.prototype.validPassword = function (password) { return bcrypt.compareSync(password, this.password); }; - User.addHook("beforeCreate", function(user) { + // Creates a hook that makes sure password is hashed before added to the database. + User.addHook("beforeCreate", function (user) { user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(10), null); }); return User; diff --git a/package-lock.json b/package-lock.json index f3fc7cc..90a407d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,11 +71,6 @@ } } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, "@types/node": { "version": "14.11.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz", @@ -96,9 +91,9 @@ } }, "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" }, "acorn-jsx": { "version": "5.3.1", @@ -242,11 +237,10 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -1972,9 +1966,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" }, "v8-compile-cache": { "version": "2.1.1", diff --git a/public/default/default 2.png b/public/default/default 2.png deleted file mode 100644 index 08b9c25..0000000 Binary files a/public/default/default 2.png and /dev/null differ diff --git a/public/js/blogs/blog.js b/public/js/blogs/blog.js index e69de29..68d4b97 100644 --- a/public/js/blogs/blog.js +++ b/public/js/blogs/blog.js @@ -0,0 +1,5 @@ +$(document).ready(function() { + if ($("#BlogMood").text().trim() === "") { + $("#BlogMood").text("N/A"); + } +}); \ No newline at end of file diff --git a/public/js/blogs/blogs.js b/public/js/blogs/blogs.js index d267379..a379bf7 100644 --- a/public/js/blogs/blogs.js +++ b/public/js/blogs/blogs.js @@ -1,59 +1,59 @@ -const FilterBlogs = $("#FilterBlogs"); -const DropDowns = $(".DropDowns"); +$(document).ready(function () { + const FilterBlogs = $("#FilterBlogs"); + const DropDowns = $(".DropDowns"); + + // API call that gets the most recent blogs first and displays them on screen + $.get("/api/blogs/?&order=DESC").then(function (data) { + FilterBlogs.append(data); + if ($("#BlogList").text().trim() === "") { + FilterBlogs.text("It looks like there are no blogs posted yet."); + } + }); -$.get("/api/blogs/?&order=DESC").then(function(data) { - FilterBlogs.append(data) -}); -DropDowns.change(function() { + // Dropdown listener that listens for change in dropdown menu, then sends API call to get filters blogs list according to selected dropdowns + // This allows user to filter blogs by multiple attributes. For example, a user could filter another users blogs by sepcific category. + DropDowns.change(function () { FilterBlogs.empty(); - let API = "" + let API = ""; let UserId = $("#users").val(); let age = $("#age").val(); let categoryId = $("#categories").val(); - if (age === "newest") { - API += "/api/blogs/?order=DESC" + API += "/api/blogs/?order=DESC"; } else if (age === "oldest") { - API += "/api/blogs/?order=ASC" + API += "/api/blogs/?order=ASC"; } if (UserId !== "AllUsers") { - API += "&user_id=" + UserId; + API += "&user_id=" + UserId; } if (categoryId !== "AllCategories") { - API += "&category=" + categoryId; + API += "&category=" + categoryId; } - $.get(API).then(function(data) { - FilterBlogs.append(data) + $.get(API).then(function (data) { + FilterBlogs.append(data); + if ($("#BlogList").text().trim() === "") { + FilterBlogs.text("It looks like there are no that match your filter."); + } }); -}); + }); -$("#SearchFilteresBlogs").keyup(function() { - let SearchFilteresBlogs = document.getElementById("SearchFilteresBlogs").value.toUpperCase(); - let BlogList = document.getElementById("BlogList").getElementsByTagName("li"); + // This keyup listener function will filter through blogs locally and search for keyword in title, body, mood, cetegory, or user. + $("#SearchFilteresBlogs").keyup(function () { + let SearchFilteresBlogs = $("#SearchFilteresBlogs").val().toUpperCase(); + let BlogList = document.getElementsByClassName("blogSearch"); for (let i = 0; i < BlogList.length; i++) { - let BlogTitles = BlogList[i].getElementsByTagName("a")[0]; - let BlogDescriptions = BlogList[i].getElementsByTagName("a")[1]; - let BlogAuthors = BlogList[i].getElementsByTagName("a")[2]; - let BlogCategories = BlogList[i].getElementsByTagName("a")[3]; - let BlogMood = BlogList[i].getElementsByTagName("a")[4]; - let titleText = BlogTitles.textContent || BlogTitles.innerText; - let descriptionText = BlogDescriptions.textContent || BlogDescriptions.innerText; - let authorText = BlogAuthors.textContent || BlogAuthors.innerText; - let categoryText = BlogCategories.textContent || BlogCategories.innerText; - let moodText = BlogMood.textContent || BlogMood.innerText; - if (titleText.toUpperCase().indexOf(SearchFilteresBlogs) > -1 || - descriptionText.toUpperCase().indexOf(SearchFilteresBlogs) > -1 || - authorText.toUpperCase().indexOf(SearchFilteresBlogs) > -1 || - categoryText.toUpperCase().indexOf(SearchFilteresBlogs) > -1 || - moodText.toUpperCase().indexOf(SearchFilteresBlogs) > -1) { - BlogList[i].style.display = ""; - } else { - BlogList[i].style.display = "none"; - } + // The innerText property is key here for searching for blogs with the same string as the SearchFilteresBlogs input. + // It looks at the title, body, mood, category, and author of blogs, then it hides blogs that don't match SearchFilteresBlogs value. + if (BlogList[i].innerText.toUpperCase().indexOf(SearchFilteresBlogs) > -1) { + BlogList[i].style.display = ""; + } else { + BlogList[i].style.display = "none"; + } } -}); \ No newline at end of file + }); +}); diff --git a/public/js/blogs/postblog.js b/public/js/blogs/postblog.js index ca08175..258afbb 100644 --- a/public/js/blogs/postblog.js +++ b/public/js/blogs/postblog.js @@ -1,103 +1,105 @@ -$(document).ready(function() { +$(document).ready(function () { - const MoodDropdown = $("#mood"); - const moodInput = $("#moodInput"); - const url = window.location.search; - let Id; - let updating = false; + const MoodDropdown = $("#mood"); + const moodInput = $("#moodInput"); + const url = window.location.search; + let Id; + let updating = false; - MoodDropdown.change(function() { - if (MoodDropdown.val() === "Other") { - moodInput.removeClass("hidden"); - } else { + // Listens for mood dropdown change. If mood dropdown is changed to "Other" then it will display the moodInput elelment so user can enter a custom mood for blog. + MoodDropdown.change(function () { + if (MoodDropdown.val() === "Other") { + moodInput.removeClass("hidden"); + } else { + moodInput.addClass("hidden"); + } + }); + + // If the user is editing an existing blog, then it will get the exisitng blog details and display them in the inputs. + // This makes it easier for editing exisitn blogs, the user can make small changes instead of staring over from scratch + const renderBloginputs = (Id) => { + $.get("/api/blogs/" + Id).then(function (data) { + $("#title").val(data.title); + $("#body").val(data.body); + $("#category").val(data.CategoryId); + // Sets the mood input to whatever value the current blog mood is + $("#moodInput").val(data.mood); + + // This checks what mood the current blog is. If the current mood is null, then the mood dropdown will be slected at None. + if (data.mood === null) { + MoodDropdown.val("None"); + } else { + // Otherwise it will use a for loop to find which mood the blog is and set the dropdown as that value + let MoodOpt = document.getElementsByClassName("MoodOpt"); + for (let index = 0; index < MoodOpt.length; index++) { + const MoodVal = MoodOpt[index].value; + if (MoodVal === data.mood) { + MoodDropdown.val(data.mood); moodInput.addClass("hidden"); + return; + // Or it will select the Other dropdown option and display the mood input. + } else { + MoodDropdown.val("Other"); + moodInput.removeClass("hidden"); + } } + } }); - - const renderBloginputs = (Id) => { - $.get("/api/blogs/" + Id).then(function (data) { - $("#moodInput").val(data.mood); - $("#title").val(data.title); - $("#body").val(data.body); - $("#category").val(data.CategoryId); + }; - - if (data.mood === null) { - MoodDropdown.val("None"); - } else { - let MoodOpt = document.getElementsByClassName("MoodOpt"); - for (let index = 0; index < MoodOpt.length; index++) { - const MoodVal = MoodOpt[index].value; - if (MoodVal === data.mood) { - MoodDropdown.val(data.mood); - moodInput.addClass("hidden"); - return; - } else { - MoodDropdown.val("Other"); - moodInput.removeClass("hidden"); - } - } - } - }); - } + // Checks if blog is existing blog being updated. + if (url.indexOf("?blog_id=") !== -1) { + Id = url.split("=")[1]; + updating = true; + renderBloginputs(Id); + } - // Check if this is a blog being updated - if (url.indexOf("?blog_id=") !== -1) { - Id = url.split("=")[1]; - updating = true; - renderBloginputs(Id) - } + // Submit blog + const submitBlog = (event) => { + event.preventDefault(); + let mood = $("#mood").val(); + const title = $("#title").val().trim(); + const body = $("#body").val().trim(); + const category = $("#category").val(); - function handleLoginErr() { - $("#alert .msg").text("Error: It looks like something went wrong. Please try again."); - $("#alert").fadeIn(500); - } - - // Submit blog - const submitBlog = (event) => { - event.preventDefault(); - let mood = $("#mood").val(); - const title = $("#title").val(); - const body = $("#body").val(); - const category = $("#category").val(); + if (mood === "Other") { + mood = $("#moodInput").val().trim(); + } - if (mood === "Other") { - mood = $("#moodInput").val(); - } - + if (body.trim() === "" || title.trim() === "") { + $("#alert .msg").text(); + handleErr("Title and body field cannot be left empty"); + return; + } - if (body.trim() === "" || title.trim() === "") { - $("#alert .msg").text("Title and body field cannot be left empty"); - $("#alert").fadeIn(500); - return; - } + if (category === null) { + handleErr("Please create a category first before posting a blog. Go to Accounts in the navbar then select Categories from the dropdown to create one."); + return; + } - let BlogData = { - title: title, - body: body, - category: category, - mood: mood - } + let BlogData = { + title: title, + body: body, + category: category, + mood: mood + }; - if (updating === false) { - $.post("/api/blogs", BlogData).then(function(data) { - console.log(data) - window.location.href = "/"; - }).catch(handleLoginErr) - } else if (updating === true) { - $.ajax({ - method: "PUT", - url: "/api/blogs/" + Id, - data: BlogData - }).then(function() { - window.location.href = "/blog/" +Id; - }).catch(handleLoginErr) - } - + if (updating === false) { + $.post("/api/blogs", BlogData).then(function (data) { + window.location.href = "/blog/" + data.id; + }).catch(handleErr); + } else if (updating === true) { + $.ajax({ + method: "PUT", + url: "/api/blogs/" + Id, + data: BlogData + }).then(function () { + window.location.href = "/blog/" + Id; + }).catch(handleErr); } - - // Add on submit event listener - $("#submitForm").on("submit", submitBlog); + }; - }); - \ No newline at end of file + // Add on submit event listener + $("#submitForm").on("submit", submitBlog); + +}); diff --git a/public/js/blogs/userblog.js b/public/js/blogs/userblog.js index fa83022..6fba430 100644 --- a/public/js/blogs/userblog.js +++ b/public/js/blogs/userblog.js @@ -1,17 +1,25 @@ -$("#edit-blog").on("click", function() { +$(document).ready(function () { + if ($("#BlogMood").text().trim() === "") { + $("#BlogMood").text("N/A"); + } + + // Edit Blog Button Event Click Listener + $("#edit-blog").on("click", function () { let id = $(this).val(); window.location.href = "/editBlog/?blog_id=" + id; -}) + }); -$("#delete-blog").on("click", function() { + // Delet Blog Event Click Listener + $("#delete-blog").on("click", function () { let id = $(this).val(); let deleteBlog = confirm("Are you sure you want to delete this blog?"); if (deleteBlog === true) { - $.ajax({ - method: "DELETE", - url: "/api/blogs/" + id - }).then(function() { - window.location.href = "/" - }) + $.ajax({ + method: "DELETE", + url: "/api/blogs/" + id + }).then(function () { + window.location.href = "/"; + }); } -}) \ No newline at end of file + }); +}); diff --git a/public/js/categories/viewCategories.js b/public/js/categories/viewCategories.js index 552cd0b..5dfe0d6 100644 --- a/public/js/categories/viewCategories.js +++ b/public/js/categories/viewCategories.js @@ -1,21 +1,52 @@ -$("#createCategory").on("click", function () { - const createCategoryInput = $("#createCategoryInput").val().trim() +$(document).ready(function () { + + // Create Category Event Click Listener + $("#createCategory").on("click", function () { + const createCategoryInput = $("#createCategoryInput").val().trim(); if (createCategoryInput !== "") { - $.post("/api/categories", {name: createCategoryInput}).then(function() { - location.reload(); - }) + $.post("/api/categories", { name: createCategoryInput }).then(function (data) { + if (data.error) { + handleErr(data.error); + } else { + $("#createCategoryInput").val(""); + $("#createCategoryAlert").fadeIn(500); + $("#createCategoryAlert .msg").text(data.message); + let newCategory = $("