Browse Source

fix: merge conflicts

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 3 years ago
parent
commit
49e5f647cb

+ 55 - 128
backend/logic/actions/reports.js

@@ -11,29 +11,6 @@ const SongsModule = moduleManager.modules.songs;
 const CacheModule = moduleManager.modules.cache;
 const ActivitiesModule = moduleManager.modules.activities;
 
-const reportableIssues = [
-	{
-		name: "Video",
-		reasons: ["Doesn't exist", "It's private", "It's not available in my country"]
-	},
-	{
-		name: "Title",
-		reasons: ["Incorrect", "Inappropriate"]
-	},
-	{
-		name: "Duration",
-		reasons: ["Skips too soon", "Skips too late", "Starts too soon", "Skips too late"]
-	},
-	{
-		name: "Artists",
-		reasons: ["Incorrect", "Inappropriate"]
-	},
-	{
-		name: "Thumbnail",
-		reasons: ["Incorrect", "Inappropriate", "Doesn't exist"]
-	}
-];
-
 CacheModule.runJob("SUB", {
 	channel: "report.resolve",
 	cb: reportId => {
@@ -62,25 +39,17 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	index: isAdminRequired(async function index(session, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
-			[
-				next => {
-					reportModel.find({ resolved: false }).sort({ released: "desc" }).exec(next);
-				}
-			],
+			[next => reportModel.find({ resolved: false }).sort({ released: "desc" }).exec(next)],
 			async (err, reports) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log("ERROR", "REPORTS_INDEX", `Indexing reports failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
+
 				this.log("SUCCESS", "REPORTS_INDEX", "Indexing reports successful.");
 				return cb({ status: "success", data: { reports } });
 			}
@@ -95,29 +64,18 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	findOne: isAdminRequired(async function findOne(session, reportId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
-		async.waterfall(
-			[
-				next => {
-					reportModel.findOne({ _id: reportId }).exec(next);
-				}
-			],
-			async (err, report) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
-					return cb({ status: "error", message: err });
-				}
-				this.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
-				return cb({ status: "success", data: { report } });
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
+		async.waterfall([next => reportModel.findOne({ _id: reportId }).exec(next)], async (err, report) => {
+			if (err) {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
+				return cb({ status: "error", message: err });
 			}
-		);
+
+			this.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
+			return cb({ status: "success", data: { report } });
+		});
 	}),
 
 	/**
@@ -128,13 +86,8 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	getReportsForSong: isAdminRequired(async function getReportsForSong(session, songId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -147,7 +100,7 @@ export default {
 				(_reports, next) => {
 					const reports = [];
 					for (let i = 0; i < _reports.length; i += 1) {
-						data.push(_reports[i]._id);
+						reports.push(_reports[i]._id);
 					}
 					next(null, reports);
 				}
@@ -158,6 +111,7 @@ export default {
 					this.log("ERROR", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
+
 				this.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
 				return cb({ status: "success", data: { reports } });
 			}
@@ -165,20 +119,15 @@ export default {
 	}),
 
 	/**
-	 * Resolves a report
+	 * Resolves a reported issue
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
 	 * @param {Function} cb - gets called with the result
 	 */
 	resolve: isAdminRequired(async function resolve(session, reportId, cb) {
-		const reportModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "report"
-			},
-			this
-		);
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -204,11 +153,14 @@ export default {
 					);
 					return cb({ status: "error", message: err });
 				}
+
 				CacheModule.runJob("PUB", {
 					channel: "report.resolve",
 					value: reportId
 				});
+
 				this.log("SUCCESS", "REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
+
 				return cb({
 					status: "success",
 					message: "Successfully resolved Report"
@@ -221,18 +173,23 @@ export default {
 	 * Creates a new report
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {object} data - the object of the report data
+	 * @param {object} report - the object of the report data
+	 * @param {string} report.youtubeId - the youtube id of the song that is being reported
+	 * @param {Array} report.issues - all issues reported (custom or defined)
 	 * @param {Function} cb - gets called with the result
 	 */
-	create: isLoginRequired(async function create(session, data, cb) {
+	create: isLoginRequired(async function create(session, report, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 
+		const { youtubeId } = report;
+
+		// properties for every report issue that is saved to db
+		const template = {};
+
 		async.waterfall(
 			[
-				next => {
-					songModel.findOne({ youtubeId: data.youtubeId }).exec(next);
-				},
+				next => songModel.findOne({ youtubeId }).exec(next),
 
 				(song, next) => {
 					if (!song) return next("Song not found.");
@@ -245,53 +202,32 @@ export default {
 				(song, next) => {
 					if (!song) return next("Song not found.");
 
-					delete data.youtubeId;
-					data.song = {
+					template.song = {
 						_id: song._id,
 						youtubeId: song.youtubeId
 					};
 
-					for (let z = 0; z < data.issues.length; z += 1) {
-						if (reportableIssues.filter(issue => issue.name === data.issues[z].name).length > 0) {
-							for (let r = 0; r < reportableIssues.length; r += 1) {
-								if (
-									reportableIssues[r].reasons.every(
-										reason => data.issues[z].reasons.indexOf(reason) < -1
-									)
-								) {
-									return cb({
-										status: "error",
-										message: "Invalid data"
-									});
-								}
-							}
-						} else
-							return cb({
-								status: "error",
-								message: "Invalid data"
-							});
-					}
-
 					return next(null, { title: song.title, artists: song.artists, thumbnail: song.thumbnail });
 				},
 
 				(song, next) => {
-					const issues = [];
-
-					for (let r = 0; r < data.issues.length; r += 1) {
-						if (!data.issues[r].reasons.length <= 0) issues.push(data.issues[r]);
-					}
+					template.createdBy = session.userId;
+					template.createdAt = Date.now();
 
-					data.issues = issues;
+					return async.each(
+						report.issues,
+						(issue, next) => {
+							reportModel.create({ ...issue, ...template }, (err, value) => {
+								CacheModule.runJob("PUB", {
+									channel: "report.create",
+									value
+								});
 
-					next(null, song);
-				},
-
-				(song, next) => {
-					data.createdBy = session.userId;
-					data.createdAt = Date.now();
-
-					reportModel.create(data, (err, report) => next(err, report, song));
+								return next(err);
+							});
+						},
+						err => next(err, report, song)
+					);
 				}
 			],
 			async (err, report, song) => {
@@ -300,31 +236,22 @@ export default {
 					this.log(
 						"ERROR",
 						"REPORTS_CREATE",
-						`Creating report for "${data.song._id}" failed by user "${session.userId}". "${err}"`
+						`Creating report for "${template.song._id}" failed by user "${session.userId}". "${err}"`
 					);
 					return cb({ status: "error", message: err });
 				}
 
-				CacheModule.runJob("PUB", {
-					channel: "report.create",
-					value: report
-				});
-
 				ActivitiesModule.runJob("ADD_ACTIVITY", {
-					userId: report.createdBy,
+					userId: template.createdBy,
 					type: "song__report",
 					payload: {
 						message: `Reported song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
-						youtubeId: data.song.youtubeId,
+						youtubeId: template.song.youtubeId,
 						thumbnail: song.thumbnail
 					}
 				});
 
-				this.log(
-					"SUCCESS",
-					"REPORTS_CREATE",
-					`User "${session.userId}" created report for "${data.youtubeId}".`
-				);
+				this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${youtubeId}".`);
 
 				return cb({
 					status: "success",

+ 1 - 10
backend/logic/db/index.js

@@ -11,7 +11,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	playlist: 4,
 	punishment: 1,
 	queueSong: 1,
-	report: 2,
+	report: 3,
 	song: 5,
 	station: 5,
 	user: 3
@@ -262,15 +262,6 @@ class _DBModule extends CoreClass {
 
 					this.schemas.playlist.index({ createdFor: 1, type: 1 }, { unique: true });
 
-					// Report
-					this.schemas.report
-						.path("description")
-						.validate(
-							description =>
-								!description || (isLength(description, 0, 400) && regex.ascii.test(description)),
-							"Invalid description."
-						);
-
 					if (config.get("skipDbDocumentsVersionCheck")) resolve();
 					else {
 						this.runJob("CHECK_DOCUMENT_VERSIONS", {}, null, -1)

+ 3 - 8
backend/logic/db/schemas/report.js

@@ -4,14 +4,9 @@ export default {
 		_id: { type: String, required: true },
 		youtubeId: { type: String, required: true }
 	},
-	description: { type: String },
-	issues: [
-		{
-			name: String,
-			reasons: Array
-		}
-	],
+	category: { type: String, enum: ["custom", "video", "title", "duration", "artists", "thumbnail"], required: true },
+	info: { type: String, required: true },
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 2, required: true }
+	documentVersion: { type: Number, default: 3, required: true }
 };

+ 153 - 88
frontend/package-lock.json

@@ -2833,6 +2833,7 @@
       "version": "3.0.11",
       "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.0.11.tgz",
       "integrity": "sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==",
+      "dev": true,
       "requires": {
         "@babel/parser": "^7.12.0",
         "@babel/types": "^7.12.0",
@@ -2844,17 +2845,20 @@
         "@babel/helper-validator-identifier": {
           "version": "7.14.0",
           "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
-          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A=="
+          "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
+          "dev": true
         },
         "@babel/parser": {
           "version": "7.14.4",
           "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.4.tgz",
-          "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA=="
+          "integrity": "sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA==",
+          "dev": true
         },
         "@babel/types": {
           "version": "7.14.4",
           "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.4.tgz",
           "integrity": "sha512-lCj4aIs0xUefJFQnwwQv2Bxg7Omd6bgquZ6LGC+gGMh6/s5qDVfjuCMlDmYQ15SLsWHd9n+X3E75lKIhl5Lkiw==",
+          "dev": true,
           "requires": {
             "@babel/helper-validator-identifier": "^7.14.0",
             "to-fast-properties": "^2.0.0"
@@ -2866,6 +2870,7 @@
       "version": "3.0.11",
       "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz",
       "integrity": "sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw==",
+      "dev": true,
       "requires": {
         "@vue/compiler-core": "3.0.11",
         "@vue/shared": "3.0.11"
@@ -2956,36 +2961,58 @@
       "integrity": "sha512-PtHmAxFmCyCElV7uTWMrXj+fefwn4lCfTtPo9fPw0SK8/7e3UaFl8IL7lnugJmNFfeKQyuTkSoGvTq1uDaRF6Q=="
     },
     "@vue/reactivity": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.0.11.tgz",
-      "integrity": "sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.1.tgz",
+      "integrity": "sha512-DsH5woNVCcPK1M0RRYVgJEU1GJDU2ASOKpAqW3ppHk+XjoFLCbqc/26RTCgTpJYd9z8VN+79Q1u7/QqgQPbuLQ==",
       "requires": {
-        "@vue/shared": "3.0.11"
+        "@vue/shared": "3.1.1"
+      },
+      "dependencies": {
+        "@vue/shared": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz",
+          "integrity": "sha512-g+4pzAw7PYSjARtLBoDq6DmcblX8i9KJHSCnyM5VDDFFifUaUT9iHbFpOF/KOizQ9f7QAqU2JH3Y6aXjzUMhVA=="
+        }
       }
     },
     "@vue/runtime-core": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.0.11.tgz",
-      "integrity": "sha512-87XPNwHfz9JkmOlayBeCCfMh9PT2NBnv795DSbi//C/RaAnc/bGZgECjmkD7oXJ526BZbgk9QZBPdFT8KMxkAg==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.1.1.tgz",
+      "integrity": "sha512-GboqR02txOtkd9F3Ysd8ltPL68vTCqIx2p/J52/gFtpgb5FG9hvOAPEwFUqxeEJRu7ResvQnmdOHiEycGPCLhQ==",
       "requires": {
-        "@vue/reactivity": "3.0.11",
-        "@vue/shared": "3.0.11"
+        "@vue/reactivity": "3.1.1",
+        "@vue/shared": "3.1.1"
+      },
+      "dependencies": {
+        "@vue/shared": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz",
+          "integrity": "sha512-g+4pzAw7PYSjARtLBoDq6DmcblX8i9KJHSCnyM5VDDFFifUaUT9iHbFpOF/KOizQ9f7QAqU2JH3Y6aXjzUMhVA=="
+        }
       }
     },
     "@vue/runtime-dom": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.0.11.tgz",
-      "integrity": "sha512-jm3FVQESY3y2hKZ2wlkcmFDDyqaPyU3p1IdAX92zTNeCH7I8zZ37PtlE1b9NlCtzV53WjB4TZAYh9yDCMIEumA==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.1.1.tgz",
+      "integrity": "sha512-o57n/199e/BBAmLRMSXmD2r12Old/h/gf6BgL0RON1NT2pwm6MWaMY4Ul55eyq+FsDILz4jR/UgoPQ9vYB8xcw==",
       "requires": {
-        "@vue/runtime-core": "3.0.11",
-        "@vue/shared": "3.0.11",
+        "@vue/runtime-core": "3.1.1",
+        "@vue/shared": "3.1.1",
         "csstype": "^2.6.8"
+      },
+      "dependencies": {
+        "@vue/shared": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz",
+          "integrity": "sha512-g+4pzAw7PYSjARtLBoDq6DmcblX8i9KJHSCnyM5VDDFFifUaUT9iHbFpOF/KOizQ9f7QAqU2JH3Y6aXjzUMhVA=="
+        }
       }
     },
     "@vue/shared": {
       "version": "3.0.11",
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.0.11.tgz",
-      "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA=="
+      "integrity": "sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==",
+      "dev": true
     },
     "@webassemblyjs/ast": {
       "version": "1.11.0",
@@ -5162,20 +5189,21 @@
       }
     },
     "css-select": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
-      "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
+      "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
       "requires": {
         "boolbase": "^1.0.0",
-        "css-what": "^3.2.1",
-        "domutils": "^1.7.0",
-        "nth-check": "^1.0.2"
+        "css-what": "^5.0.0",
+        "domhandler": "^4.2.0",
+        "domutils": "^2.6.0",
+        "nth-check": "^2.0.0"
       }
     },
     "css-what": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
-      "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ=="
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
+      "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg=="
     },
     "cssesc": {
       "version": "3.0.0",
@@ -5399,32 +5427,26 @@
       }
     },
     "dom-serializer": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
-      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
+      "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
       "requires": {
         "domelementtype": "^2.0.1",
+        "domhandler": "^4.2.0",
         "entities": "^2.0.0"
-      },
-      "dependencies": {
-        "domelementtype": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
-          "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w=="
-        }
       }
     },
     "domelementtype": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
-      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
+      "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
     },
     "domhandler": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
-      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
+      "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
       "requires": {
-        "domelementtype": "1"
+        "domelementtype": "^2.2.0"
       }
     },
     "dompurify": {
@@ -5433,12 +5455,13 @@
       "integrity": "sha512-9H0UL59EkDLgY3dUFjLV6IEUaHm5qp3mxSqWw7Yyx4Zhk2Jn2cmLe+CNPP3xy13zl8Bqg+0NehQzkdMoVhGRww=="
     },
     "domutils": {
-      "version": "1.7.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
-      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
+      "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
       "requires": {
-        "dom-serializer": "0",
-        "domelementtype": "1"
+        "dom-serializer": "^1.0.1",
+        "domelementtype": "^2.2.0",
+        "domhandler": "^4.2.0"
       }
     },
     "dot-case": {
@@ -6716,9 +6739,9 @@
       }
     },
     "glob-parent": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
-      "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
       "dev": true,
       "requires": {
         "is-glob": "^4.0.1"
@@ -6975,23 +6998,14 @@
       }
     },
     "htmlparser2": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
-      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
-      "requires": {
-        "domelementtype": "^1.3.1",
-        "domhandler": "^2.3.0",
-        "domutils": "^1.5.1",
-        "entities": "^1.1.1",
-        "inherits": "^2.0.1",
-        "readable-stream": "^3.1.1"
-      },
-      "dependencies": {
-        "entities": {
-          "version": "1.1.2",
-          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
-          "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
-        }
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+      "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+      "requires": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.0.0",
+        "domutils": "^2.5.2",
+        "entities": "^2.0.0"
       }
     },
     "http-deceiver": {
@@ -7146,7 +7160,8 @@
     "inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
     },
     "inquirer": {
       "version": "7.1.0",
@@ -8437,11 +8452,11 @@
       }
     },
     "nth-check": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
-      "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
+      "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
       "requires": {
-        "boolbase": "~1.0.0"
+        "boolbase": "^1.0.0"
       }
     },
     "number-is-nan": {
@@ -9297,6 +9312,7 @@
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
       "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "dev": true,
       "requires": {
         "inherits": "^2.0.3",
         "string_decoder": "^1.1.1",
@@ -9469,15 +9485,15 @@
       "dev": true
     },
     "renderkid": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz",
-      "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==",
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz",
+      "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==",
       "requires": {
-        "css-select": "^2.0.2",
-        "dom-converter": "^0.2",
-        "htmlparser2": "^3.10.1",
-        "lodash": "^4.17.20",
-        "strip-ansi": "^3.0.0"
+        "css-select": "^4.1.3",
+        "dom-converter": "^0.2.0",
+        "htmlparser2": "^6.1.0",
+        "lodash": "^4.17.21",
+        "strip-ansi": "^3.0.1"
       }
     },
     "repeat-element": {
@@ -10468,6 +10484,7 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
       "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
       "requires": {
         "safe-buffer": "~5.2.0"
       }
@@ -11038,7 +11055,8 @@
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
     },
     "utila": {
       "version": "0.4.0",
@@ -11091,13 +11109,60 @@
       }
     },
     "vue": {
-      "version": "3.0.11",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.0.11.tgz",
-      "integrity": "sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.1.1.tgz",
+      "integrity": "sha512-j9fj3PNPMxo2eqOKYjMuss9XBS8ZtmczLY3kPvjcp9d3DbhyNqLYbaMQH18+1pDIzzVvQCQBvIf774LsjjqSKA==",
       "requires": {
-        "@vue/compiler-dom": "3.0.11",
-        "@vue/runtime-dom": "3.0.11",
-        "@vue/shared": "3.0.11"
+        "@vue/compiler-dom": "3.1.1",
+        "@vue/runtime-dom": "3.1.1",
+        "@vue/shared": "3.1.1"
+      },
+      "dependencies": {
+        "@babel/helper-validator-identifier": {
+          "version": "7.14.5",
+          "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz",
+          "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg=="
+        },
+        "@babel/parser": {
+          "version": "7.14.5",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz",
+          "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg=="
+        },
+        "@babel/types": {
+          "version": "7.14.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
+          "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==",
+          "requires": {
+            "@babel/helper-validator-identifier": "^7.14.5",
+            "to-fast-properties": "^2.0.0"
+          }
+        },
+        "@vue/compiler-core": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.1.1.tgz",
+          "integrity": "sha512-Z1RO3T6AEtAUFf2EqqovFm3ohAeTvFzRtB0qUENW2nEerJfdlk13/LS1a0EgsqlzxmYfR/S/S/gW9PLbFZZxkA==",
+          "requires": {
+            "@babel/parser": "^7.12.0",
+            "@babel/types": "^7.12.0",
+            "@vue/shared": "3.1.1",
+            "estree-walker": "^2.0.1",
+            "source-map": "^0.6.1"
+          }
+        },
+        "@vue/compiler-dom": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.1.1.tgz",
+          "integrity": "sha512-nobRIo0t5ibzg+q8nC31m+aJhbq8FbWUoKvk6h3Vs1EqTDJaj6lBTcVTq5or8AYht7FbSpdAJ81isbJ1rWNX7A==",
+          "requires": {
+            "@vue/compiler-core": "3.1.1",
+            "@vue/shared": "3.1.1"
+          }
+        },
+        "@vue/shared": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.1.tgz",
+          "integrity": "sha512-g+4pzAw7PYSjARtLBoDq6DmcblX8i9KJHSCnyM5VDDFFifUaUT9iHbFpOF/KOizQ9f7QAqU2JH3Y6aXjzUMhVA=="
+        }
       }
     },
     "vue-content-loader": {

+ 1 - 1
frontend/package.json

@@ -49,7 +49,7 @@
     "html-webpack-plugin": "^5.3.1",
     "marked": "^2.0.3",
     "toasters": "^2.3.1",
-    "vue": "^3.0.11",
+    "vue": "^3.1.1",
     "vue-content-loader": "^2.0.0",
     "vue-loader": "^16.2.0",
     "vue-router": "^4.0.8",

+ 59 - 0
frontend/src/App.vue

@@ -1176,4 +1176,63 @@ h4.section-title {
 		font-style: italic;
 	}
 }
+.checkbox-control {
+	display: flex;
+	flex-direction: row;
+	align-items: center;
+
+	p {
+		margin-left: 10px;
+	}
+
+	.switch {
+		position: relative;
+		display: inline-block;
+		flex-shrink: 0;
+		width: 40px;
+		height: 24px;
+	}
+
+	.switch input {
+		opacity: 0;
+		width: 0;
+		height: 0;
+	}
+
+	.slider {
+		position: absolute;
+		cursor: pointer;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: #ccc;
+		transition: 0.2s;
+		border-radius: 34px;
+	}
+
+	.slider:before {
+		position: absolute;
+		content: "";
+		height: 16px;
+		width: 16px;
+		left: 4px;
+		bottom: 4px;
+		background-color: white;
+		transition: 0.2s;
+		border-radius: 50%;
+	}
+
+	input:checked + .slider {
+		background-color: var(--primary-color);
+	}
+
+	input:focus + .slider {
+		box-shadow: 0 0 1px var(--primary-color);
+	}
+
+	input:checked + .slider:before {
+		transform: translateX(16px);
+	}
+}
 </style>

+ 3 - 3
frontend/src/components/Queue.vue

@@ -89,7 +89,7 @@
 			@click="openModal('manageStation') & showManageStationTab('search')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Add Song To Queue </span>
+			<span> Add Song To Queue </span>
 		</button>
 		<button
 			class="button is-primary tab-actionable-button"
@@ -99,7 +99,7 @@
 			@click="openModal('requestSong')"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Request Song </span>
+			<span> Request Song </span>
 		</button>
 		<button
 			class="button is-primary tab-actionable-button disabled"
@@ -115,7 +115,7 @@
 			v-tippy="{ theme: 'info' }"
 		>
 			<i class="material-icons icon-with-button">queue</i>
-			<span class="optional-desktop-only-text"> Add Song To Queue </span>
+			<span> Add Song To Queue </span>
 		</button>
 		<div
 			id="queue-locked"

+ 5 - 5
frontend/src/components/modals/ManageStation/index.vue

@@ -53,7 +53,7 @@
 									<i class="material-icons icon-with-button"
 										>play_arrow</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Resume Station
 									</span>
 								</button>
@@ -65,7 +65,7 @@
 									<i class="material-icons icon-with-button"
 										>pause</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Pause Station
 									</span>
 								</button>
@@ -78,7 +78,7 @@
 									<i class="material-icons icon-with-button"
 										>skip_next</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Force Skip
 									</span>
 								</button>
@@ -91,7 +91,7 @@
 									<i class="material-icons icon-with-button"
 										>settings</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Manage Station
 									</span>
 								</button> -->
@@ -191,7 +191,7 @@
 				@click="openModal('requestSong')"
 			>
 				<i class="material-icons icon-with-button">queue</i>
-				<span class="optional-desktop-only-text"> Request Song </span>
+				<span> Request Song </span>
 			</button>
 			<div v-if="isOwnerOrAdmin()" class="right">
 				<confirm @confirm="clearAndRefillStationQueue()">

+ 316 - 67
frontend/src/components/modals/Report.vue

@@ -3,33 +3,134 @@
 		<template #body>
 			<div class="edit-report-wrapper">
 				<song-item
-					:song="localSong"
+					:song="song"
 					:disabled-actions="['report']"
 					header="Selected Song.."
 				/>
+
 				<div class="columns is-multiline">
 					<div
-						v-for="issue in issues"
+						v-for="issue in predefinedIssues"
 						class="column is-half"
-						:key="issue.name"
+						:key="issue.category"
 					>
-						<label class="label">{{ issue.name }}</label>
+						<label class="label">{{ issue.category }}</label>
+
 						<p
 							v-for="reason in issue.reasons"
-							class="control"
-							:key="reason"
+							class="control checkbox-control"
+							:key="reason.reason"
 						>
-							<label class="checkbox">
-								<input
-									type="checkbox"
-									@click="toggleIssue(issue.name, reason)"
-								/>
-								{{ reason }}
-							</label>
+							<span class="align-horizontally">
+								<span>
+									<label class="switch">
+										<input
+											type="checkbox"
+											:id="reason.reason"
+											v-model="reason.enabled"
+										/>
+										<span class="slider round"></span>
+									</label>
+
+									<label :for="reason.reason">
+										<span></span>
+										<p>{{ reason.reason }}</p>
+									</label>
+								</span>
+
+								<i
+									class="material-icons"
+									content="Provide More Info"
+									v-tippy
+									@click="reason.showInfo = !reason.showInfo"
+								>
+									info
+								</i>
+							</span>
+
+							<input
+								type="text"
+								class="input"
+								v-model="reason.info"
+								v-if="reason.showInfo"
+								placeholder="Provide more information..."
+								@keyup="reason.enabled = true"
+							/>
 						</p>
 					</div>
-					<div class="column">
-						<label class="label">Other</label>
+					<!-- allow for multiple custom issues with plus/add button and then a input textbox -->
+					<!-- do away with textbox -->
+
+					<div class="column is-half">
+						<div id="custom-issues">
+							<div id="custom-issues-title">
+								<label class="label">Issues not listed</label>
+
+								<button
+									class="button tab-actionable-button "
+									content="Add an issue that isn't listed"
+									v-tippy="{ theme: 'info' }"
+									@click="customIssues.push('')"
+								>
+									<i class="material-icons icon-with-button"
+										>add</i
+									>
+									<span>
+										Add Custom Issue
+									</span>
+								</button>
+							</div>
+
+							<div
+								class="custom-issue control is-grouped input-with-button"
+								v-for="(issue, index) in customIssues"
+								:key="index"
+							>
+								<p class="control is-expanded">
+									<input
+										type="text"
+										class="input"
+										v-model="customIssues[index]"
+										placeholder="Provide information..."
+									/>
+								</p>
+								<p class="control">
+									<button
+										class="button is-danger"
+										content="Remove custom issue"
+										v-tippy="{ theme: 'info' }"
+										@click="customIssues.splice(index, 1)"
+									>
+										<i class="material-icons">
+											delete
+										</i>
+									</button>
+								</p>
+							</div>
+
+							<p
+								id="no-issues-listed"
+								v-if="customIssues.length <= 0"
+							>
+								<em>
+									Add any issues that aren't listed above.
+								</em>
+							</p>
+						</div>
+					</div>
+
+					<!--
+						<div class="column">
+						<p class="content-box-optional-helper">
+							<a href="#" @click="changeToLoginModal()">
+								Issue isn't listed?
+							</a>
+						</p>
+
+						<br />
+
+		
+
 						<textarea
 							v-model="report.description"
 							class="textarea"
@@ -40,6 +141,7 @@
 							{{ charactersRemaining }}
 						</div>
 					</div>
+					-->
 				</div>
 			</div>
 		</template>
@@ -66,49 +168,122 @@ export default {
 	components: { Modal, SongItem },
 	data() {
 		return {
-			localSong: null,
-			report: {
-				resolved: false,
-				youtubeId: "",
-				description: "",
-				issues: [
-					{ name: "Video", reasons: [] },
-					{ name: "Title", reasons: [] },
-					{ name: "Duration", reasons: [] },
-					{ name: "Artists", reasons: [] },
-					{ name: "Thumbnail", reasons: [] }
-				]
-			},
-			issues: [
+			customIssues: [],
+			predefinedIssues: [
 				{
-					name: "Video",
+					category: "video",
 					reasons: [
-						"Doesn't exist",
-						"It's private",
-						"It's not available in my country",
-						"Unofficial"
+						{
+							enabled: false,
+							reason: "Doesn't exist",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "It's private",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "It's not available in my country",
+							info: "United States",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Unofficial",
+							info: "",
+							showInfo: false
+						}
 					]
 				},
 				{
-					name: "Title",
-					reasons: ["Incorrect", "Inappropriate"]
+					category: "title",
+					reasons: [
+						{
+							enabled: false,
+							reason: "Incorrect",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Inappropriate",
+							info: "",
+							showInfo: false
+						}
+					]
 				},
 				{
-					name: "Duration",
+					category: "duration",
 					reasons: [
-						"Skips too soon",
-						"Skips too late",
-						"Starts too soon",
-						"Starts too late"
+						{
+							enabled: false,
+							reason: "Skips too soon",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Skips too late",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Starts too soon",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Starts too late",
+							info: "",
+							showInfo: false
+						}
 					]
 				},
 				{
-					name: "Artists",
-					reasons: ["Incorrect", "Inappropriate"]
+					category: "artists",
+					reasons: [
+						{
+							enabled: false,
+							reason: "Incorrect",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Inappropriate",
+							info: "",
+							showInfo: false
+						}
+					]
 				},
 				{
-					name: "Thumbnail",
-					reasons: ["Incorrect", "Inappropriate", "Doesn't exist"]
+					category: "thumbnail",
+					reasons: [
+						{
+							enabled: false,
+							reason: "Incorrect",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Inappropriate",
+							info: "",
+							showInfo: false
+						},
+						{
+							enabled: false,
+							reason: "Doesn't exist",
+							info: "",
+							showInfo: false
+						}
+					]
 				}
 			]
 		};
@@ -124,31 +299,40 @@ export default {
 			socket: "websockets/getSocket"
 		})
 	},
-	mounted() {
-		if (this.song !== null) {
-			this.localSong = this.song;
-			this.report.youtubeId = this.song.youtubeId;
-			this.reportSong(null);
-		}
-	},
 	methods: {
 		create() {
-			this.socket.dispatch("reports.create", this.report, res => {
-				new Toast(res.message);
-				if (res.status === "success") this.closeModal("report");
-			});
-		},
-		toggleIssue(name, reason) {
-			for (let z = 0; z < this.report.issues.length; z += 1) {
-				if (this.report.issues[z].name === name) {
-					if (this.report.issues[z].reasons.indexOf(reason) > -1) {
-						this.report.issues[z].reasons.splice(
-							this.report.issues[z].reasons.indexOf(reason),
-							1
-						);
-					} else this.report.issues[z].reasons.push(reason);
+			const issues = [];
+
+			// any predefined issues that are enabled
+			this.predefinedIssues.forEach(category =>
+				category.reasons.forEach(reason => {
+					if (reason.enabled) {
+						const info =
+							reason.info === ""
+								? reason.reason
+								: `${reason.reason} - ${reason.info}`;
+
+						issues.push({ category: category.category, info });
+					}
+				})
+			);
+
+			// any custom issues
+			this.customIssues.forEach(issue =>
+				issues.push({ category: "custom", info: issue })
+			);
+
+			this.socket.dispatch(
+				"reports.create",
+				{
+					issues,
+					youtubeId: this.song.youtubeId
+				},
+				res => {
+					new Toast(res.message);
+					if (res.status === "success") this.closeModal("report");
 				}
-			}
+			);
 		},
 		...mapActions("modals/report", ["reportSong"]),
 		...mapActions("modalVisibility", ["closeModal"])
@@ -185,9 +369,74 @@ export default {
 	}
 }
 
+.label {
+	text-transform: capitalize;
+}
+
 .columns {
 	margin-left: unset;
 	margin-right: unset;
 	margin-top: 20px;
+
+	.control {
+		display: flex;
+		flex-direction: column;
+
+		span.align-horizontally {
+			width: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: space-between;
+
+			span {
+				display: flex;
+			}
+		}
+
+		i {
+			cursor: pointer;
+		}
+
+		input[type="text"] {
+			height: initial;
+			margin: 10px 0;
+		}
+	}
+}
+
+#custom-issues {
+	height: 100%;
+
+	#custom-issues-title {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin-bottom: 15px;
+
+		button {
+			padding: 3px 5px;
+			height: initial;
+		}
+
+		label {
+			margin: 0;
+		}
+	}
+
+	#no-issues-listed {
+		display: flex;
+		height: calc(100% - 32px - 15px);
+		align-items: center;
+		justify-content: center;
+	}
+
+	.custom-issue {
+		flex-direction: row;
+
+		input {
+			height: 36px;
+			margin: 0;
+		}
+	}
 }
 </style>

+ 8 - 21
frontend/src/components/modals/ViewReport.vue

@@ -39,30 +39,17 @@
 						}}
 					</span>
 					<br />
-					<span v-if="report.description">
-						<strong>Description:</strong>
-						{{ report.description }}
+					<span>
+						<strong>Category:</strong>
+						{{ report.category }}
+					</span>
+					<br />
+					<span>
+						<strong>Info:</strong>
+						{{ report.info }}
 					</span>
 				</div>
 			</article>
-			<table v-if="report.issues.length > 0" class="table is-narrow">
-				<thead>
-					<tr>
-						<td>Issue</td>
-						<td>Reasons</td>
-					</tr>
-				</thead>
-				<tbody>
-					<tr v-for="issue in report.issues" :key="issue.name">
-						<td>
-							<span>{{ issue.name }}</span>
-						</td>
-						<td>
-							<span>{{ issue.reasons }}</span>
-						</td>
-					</tr>
-				</tbody>
-			</table>
 		</template>
 		<template #footer v-if="report && report._id">
 			<a class="button is-primary" href="#" @click="resolve(report._id)">

+ 6 - 2
frontend/src/pages/Admin/tabs/Reports.vue

@@ -8,7 +8,8 @@
 						<td>Song ID</td>
 						<td>Author</td>
 						<td>Time of report</td>
-						<td>Description</td>
+						<td>Category</td>
+						<td>Info</td>
 						<td>Options</td>
 					</tr>
 				</thead>
@@ -41,7 +42,10 @@
 							>
 						</td>
 						<td>
-							<span>{{ report.description }}</span>
+							<span>{{ report.category }}</span>
+						</td>
+						<td>
+							<span>{{ report.info }}</span>
 						</td>
 						<td>
 							<a

+ 0 - 62
frontend/src/pages/Settings/Tabs/Preferences.vue

@@ -191,65 +191,3 @@ export default {
 	}
 };
 </script>
-
-<style lang="scss" scoped>
-.checkbox-control {
-	display: flex;
-	flex-direction: row;
-	align-items: center;
-
-	p {
-		margin-left: 10px;
-	}
-
-	.switch {
-		position: relative;
-		display: inline-block;
-		flex-shrink: 0;
-		width: 40px;
-		height: 24px;
-	}
-
-	.switch input {
-		opacity: 0;
-		width: 0;
-		height: 0;
-	}
-
-	.slider {
-		position: absolute;
-		cursor: pointer;
-		top: 0;
-		left: 0;
-		right: 0;
-		bottom: 0;
-		background-color: #ccc;
-		transition: 0.2s;
-		border-radius: 34px;
-	}
-
-	.slider:before {
-		position: absolute;
-		content: "";
-		height: 16px;
-		width: 16px;
-		left: 4px;
-		bottom: 4px;
-		background-color: white;
-		transition: 0.2s;
-		border-radius: 50%;
-	}
-
-	input:checked + .slider {
-		background-color: var(--primary-color);
-	}
-
-	input:focus + .slider {
-		box-shadow: 0 0 1px var(--primary-color);
-	}
-
-	input:checked + .slider:before {
-		transform: translateX(16px);
-	}
-}
-</style>

+ 1 - 1
frontend/src/pages/Station/Sidebar/Playlists.vue

@@ -99,7 +99,7 @@
 			@click="openModal('createPlaylist')"
 		>
 			<i class="material-icons icon-with-button">create</i>
-			<span class="optional-desktop-only-text"> Create Playlist </span>
+			<span> Create Playlist </span>
 		</a>
 	</div>
 </template>

+ 1 - 1
frontend/src/pages/Station/Sidebar/Users.vue

@@ -58,7 +58,7 @@
 			@click="copyToClipboard()"
 		>
 			<i class="material-icons icon-with-button">share</i>
-			<span class="optional-desktop-only-text">
+			<span>
 				Share (copy to clipboard)
 			</span>
 		</button>

+ 4 - 4
frontend/src/pages/Station/index.vue

@@ -119,7 +119,7 @@
 									<i class="material-icons icon-with-button"
 										>play_arrow</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Resume Station
 									</span>
 								</button>
@@ -131,7 +131,7 @@
 									<i class="material-icons icon-with-button"
 										>pause</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Pause Station
 									</span>
 								</button>
@@ -144,7 +144,7 @@
 									<i class="material-icons icon-with-button"
 										>skip_next</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Force Skip
 									</span>
 								</button>
@@ -157,7 +157,7 @@
 									<i class="material-icons icon-with-button"
 										>settings</i
 									>
-									<span class="optional-desktop-only-text">
+									<span>
 										Manage Station
 									</span>
 								</button>