Prechádzať zdrojové kódy

feat(News): added markdown/preview side-by-side and updated news schema

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 rokov pred
rodič
commit
4afc27bb2d

+ 86 - 57
backend/logic/actions/news.js

@@ -41,7 +41,7 @@ CacheModule.runJob("SUB", {
 
 export default {
 	/**
-	 * Gets all news items
+	 * Gets all news items that are published
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {Function} cb - gets called with the result
@@ -51,7 +51,7 @@ export default {
 		async.waterfall(
 			[
 				next => {
-					newsModel.find({}).sort({ createdAt: "desc" }).exec(next);
+					newsModel.find({ status: "published" }).sort({ createdAt: "desc" }).exec(next);
 				}
 			],
 			async (err, news) => {
@@ -72,7 +72,7 @@ export default {
 	 * Gets a news item by id
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {string} newsId - the news id
+	 * @param {string} newsId - the news item id
 	 * @param {Function} cb - gets called with the result
 	 */
 	async getNewsFromId(session, newsId, cb) {
@@ -87,11 +87,11 @@ export default {
 			async (err, news) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "GET_NEWS_FROM_ID", `Getting news failed. "${err}"`);
+					this.log("ERROR", "GET_NEWS_FROM_ID", `Getting news item ${newsId} failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
 
-				this.log("SUCCESS", "GET_NEWS_FROM_ID", `Got news successful.`, false);
+				this.log("SUCCESS", "GET_NEWS_FROM_ID", `Got news item ${newsId} successfully.`, false);
 
 				return cb({ status: "success", data: { news } });
 			}
@@ -120,8 +120,11 @@ export default {
 					this.log("ERROR", "NEWS_CREATE", `Creating news failed. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
+
 				CacheModule.runJob("PUB", { channel: "news.create", value: news });
+
 				this.log("SUCCESS", "NEWS_CREATE", `Creating news successful.`);
+
 				return cb({
 					status: "success",
 					message: "Successfully created News"
@@ -138,84 +141,110 @@ export default {
 	 */
 	async newest(session, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
-		async.waterfall(
-			[
-				next => {
-					newsModel.findOne({}).sort({ createdAt: "desc" }).exec(next);
-				}
-			],
-			async (err, news) => {
-				if (err) {
-					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log("ERROR", "NEWS_NEWEST", `Getting the latest news failed. "${err}"`);
-					return cb({ status: "error", message: err });
-				}
-
-				this.log("SUCCESS", "NEWS_NEWEST", `Successfully got the latest news.`, false);
-				return cb({ status: "success", data: { news } });
+		async.waterfall([next => newsModel.findOne({}).sort({ createdAt: "desc" }).exec(next)], async (err, news) => {
+			if (err) {
+				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+				this.log("ERROR", "NEWS_NEWEST", `Getting the latest news failed. "${err}"`);
+				return cb({ status: "error", message: err });
 			}
-		);
+
+			this.log("SUCCESS", "NEWS_NEWEST", `Successfully got the latest news.`, false);
+			return cb({ status: "success", data: { news } });
+		});
 	},
 
 	/**
 	 * Removes a news item
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {object} newsId - the id of the news object we want to remove
+	 * @param {object} newsId - the id of the news item we want to remove
 	 * @param {Function} cb - gets called with the result
 	 */
 	remove: isAdminRequired(async function remove(session, newsId, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
 
-		newsModel.deleteOne({ _id: newsId }, async err => {
-			if (err) {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-				this.log(
-					"ERROR",
-					"NEWS_REMOVE",
-					`Removing news "${newsId}" failed for user "${session.userId}". "${err}"`
-				);
-				return cb({ status: "error", message: err });
-			}
+		async.waterfall(
+			[
+				next => {
+					if (!newsId) return next("Please provide a news item id to update.");
+					return next();
+				},
 
-			CacheModule.runJob("PUB", { channel: "news.remove", value: newsId });
+				next => {
+					newsModel.deleteOne({ _id: newsId }, err => next(err));
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"NEWS_REMOVE",
+						`Removing news "${newsId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
 
-			this.log("SUCCESS", "NEWS_REMOVE", `Removing news "${newsId}" successful by user "${session.userId}".`);
+				CacheModule.runJob("PUB", { channel: "news.remove", value: newsId });
 
-			return cb({
-				status: "success",
-				message: "Successfully removed News"
-			});
-		});
+				this.log("SUCCESS", "NEWS_REMOVE", `Removing news "${newsId}" successful by user "${session.userId}".`);
+
+				return cb({
+					status: "success",
+					message: "Successfully removed News"
+				});
+			}
+		);
 	}),
 
 	/**
-	 * Removes a news item
+	 * Updates a news item
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
-	 * @param {string} _id - the news id
-	 * @param {object} news - the news object
+	 * @param {string} newsId - the id of the news item
+	 * @param {object} item - the news item object
+	 * @param {string} item.status - the status of the news e.g. published
+	 * @param {string} item.title - taken from a level-1 heading at the top of the markdown
+	 * @param {string} item.markdown - the markdown that forms the content of the news
 	 * @param {Function} cb - gets called with the result
 	 */
-	// TODO Fix this
-	update: isAdminRequired(async function update(session, _id, news, cb) {
+	update: isAdminRequired(async function update(session, newsId, item, cb) {
 		const newsModel = await DBModule.runJob("GET_MODEL", { modelName: "news" }, this);
-		newsModel.updateOne({ _id }, news, { upsert: true }, async err => {
-			if (err) {
-				err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+		async.waterfall(
+			[
+				next => {
+					if (!newsId) return next("Please provide a news item id to update.");
+					return next();
+				},
+
+				next => {
+					newsModel.updateOne({ _id: newsId }, item, { upsert: true }, err => next(err));
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"NEWS_UPDATE",
+						`Updating news item "${newsId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+
+				CacheModule.runJob("PUB", { channel: "news.update", value: { ...item, _id: newsId } });
+
 				this.log(
-					"ERROR",
+					"SUCCESS",
 					"NEWS_UPDATE",
-					`Updating news "${_id}" failed for user "${session.userId}". "${err}"`
+					`Updating news item "${newsId}" successful for user "${session.userId}".`
 				);
-				return cb({ status: "error", message: err });
+				return cb({
+					status: "success",
+					message: "Successfully updated news item"
+				});
 			}
-			CacheModule.runJob("PUB", { channel: "news.update", value: news });
-			this.log("SUCCESS", "NEWS_UPDATE", `Updating news "${_id}" successful for user "${session.userId}".`);
-			return cb({
-				status: "success",
-				message: "Successfully updated News"
-			});
-		});
+		);
 	})
 };

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

@@ -7,7 +7,7 @@ import CoreClass from "../../core";
 
 const REQUIRED_DOCUMENT_VERSIONS = {
 	activity: 2,
-	news: 1,
+	news: 2,
 	playlist: 3,
 	punishment: 1,
 	queueSong: 1,

+ 3 - 6
backend/logic/db/schemas/news.js

@@ -1,11 +1,8 @@
 export default {
 	title: { type: String, required: true },
-	description: { type: String, required: true },
-	bugs: [{ type: String }],
-	features: [{ type: String }],
-	improvements: [{ type: String }],
-	upcoming: [{ type: String }],
+	markdown: { type: String, required: true },
+	status: { type: String, enum: ["draft", "published", "archived"], required: true, default: "published" },
 	createdBy: { type: String, required: true },
 	createdAt: { type: Number, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 1, required: true }
+	documentVersion: { type: Number, default: 2, required: true }
 };

+ 5 - 0
frontend/package-lock.json

@@ -7645,6 +7645,11 @@
         "object-visit": "^1.0.0"
       }
     },
+    "marked": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz",
+      "integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA=="
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",

+ 1 - 0
frontend/package.json

@@ -47,6 +47,7 @@
     "date-fns": "^2.19.0",
     "eslint-config-airbnb-base": "^13.2.0",
     "html-webpack-plugin": "^5.3.1",
+    "marked": "^2.0.3",
     "toasters": "^2.3.0",
     "vue": "^2.6.12",
     "vue-content-loader": "^0.2.3",

+ 5 - 0
frontend/src/components/Modal.vue

@@ -88,6 +88,10 @@ p {
 }
 
 .modal-card-foot {
+	*:not(:last-child) {
+		margin-right: 10px;
+	}
+
 	& > div {
 		display: flex;
 		flex-grow: 1;
@@ -95,6 +99,7 @@ p {
 			margin-left: 10px;
 		}
 	}
+
 	.right {
 		margin-left: auto;
 		justify-content: flex-end;

+ 132 - 316
frontend/src/components/modals/EditNews.vue

@@ -1,215 +1,142 @@
 <template>
-	<modal title="Edit News">
-		<div slot="body" v-if="news && news._id">
-			<label class="label">Title</label>
-			<p class="control">
-				<input
-					v-model="news.title"
-					class="input"
-					type="text"
-					placeholder="News Title"
-					autofocus
-				/>
-			</p>
-			<label class="label">Description</label>
-			<p class="control">
-				<input
-					v-model="news.description"
-					class="input"
-					type="text"
-					placeholder="News Description"
-				/>
-			</p>
-			<div class="columns">
-				<div class="column">
-					<label class="label">Bugs</label>
-					<p class="control has-addons">
-						<input
-							ref="edit-bugs"
-							class="input"
-							type="text"
-							placeholder="Bug"
-							@keyup.enter="add('bugs')"
-						/>
-						<a class="button is-info" href="#" @click="add('bugs')"
-							>Add</a
-						>
-					</p>
-					<span
-						v-for="bug in news.bugs"
-						class="tag is-info"
-						:key="bug"
-					>
-						{{ bug }}
-						<button
-							class="delete is-info"
-							@click="remove('bugs', index)"
-						/>
-					</span>
-				</div>
-				<div class="column">
-					<label class="label">Features</label>
-					<p class="control has-addons">
-						<input
-							ref="edit-features"
-							class="input"
-							type="text"
-							placeholder="Feature"
-							@keyup.enter="add('features')"
-						/>
-						<a
-							class="button is-info"
-							href="#"
-							@click="add('features')"
-							>Add</a
-						>
-					</p>
-					<span
-						v-for="feature in news.features"
-						class="tag is-info"
-						:key="feature"
-					>
-						{{ feature }}
-						<button
-							class="delete is-info"
-							@click="remove('features', index)"
-						/>
-					</span>
-				</div>
-			</div>
-
-			<div class="columns">
+	<modal :title="newsId ? 'Edit News' : 'Create News'">
+		<div slot="body">
+			<div id="markdown-editor-and-preview">
 				<div class="column">
-					<label class="label">Improvements</label>
-					<p class="control has-addons">
-						<input
-							ref="edit-improvements"
-							class="input"
-							type="text"
-							placeholder="Improvement"
-							@keyup.enter="add('improvements')"
-						/>
-						<a
-							class="button is-info"
-							href="#"
-							@click="add('improvements')"
-							>Add</a
-						>
-					</p>
-					<span
-						v-for="improvement in news.improvements"
-						class="tag is-info"
-						:key="improvement"
-					>
-						{{ improvement }}
-						<button
-							class="delete is-info"
-							@click="remove('improvements', index)"
-						/>
-					</span>
+					<p><strong>Markdown</strong></p>
+					<textarea v-model="markdown"></textarea>
 				</div>
 				<div class="column">
-					<label class="label">Upcoming</label>
-					<p class="control has-addons">
-						<input
-							ref="edit-upcoming"
-							class="input"
-							type="text"
-							placeholder="Upcoming"
-							@keyup.enter="add('upcoming')"
-						/>
-						<a
-							class="button is-info"
-							href="#"
-							@click="add('upcoming')"
-							>Add</a
-						>
-					</p>
-					<span
-						v-for="upcoming in news.upcoming"
-						class="tag is-info"
-						:key="upcoming"
-					>
-						{{ upcoming }}
-						<button
-							class="delete is-info"
-							@click="remove('upcoming', index)"
-						/>
-					</span>
+					<p><strong>Preview</strong></p>
+					<div id="preview" v-html="marked(markdown)"></div>
 				</div>
 			</div>
 		</div>
 		<div slot="footer">
-			<button class="button is-success" @click="updateNews(false)">
-				<i class="material-icons save-changes">done</i>
-				<span>&nbsp;Save</span>
-			</button>
-			<button class="button is-success" @click="updateNews(true)">
-				<i class="material-icons save-changes">done</i>
-				<span>&nbsp;Save and close</span>
-			</button>
+			<p class="control select">
+				<select v-model="status">
+					<option value="draft">Draft</option>
+					<option value="published" selected>Published</option>
+				</select>
+			</p>
+
+			<save-button
+				ref="saveButton"
+				v-if="newsId"
+				@clicked="newsId ? update(false) : create(false)"
+			/>
+
+			<save-button
+				ref="saveAndCloseButton"
+				type="save-and-close"
+				@clicked="newsId ? update(true) : create(true)"
+			/>
 		</div>
 	</modal>
 </template>
 
 <script>
 import { mapActions, mapGetters, mapState } from "vuex";
-
+import marked from "marked";
 import Toast from "toasters";
 
+import SaveButton from "../SaveButton.vue";
 import Modal from "../Modal.vue";
 
 export default {
-	components: { Modal },
+	components: { Modal, SaveButton },
 	props: {
 		newsId: { type: String, default: "" },
 		sector: { type: String, default: "admin" }
 	},
+	data() {
+		return {
+			markdown: "# Example\n## Subheading goes here",
+			status: "published"
+		};
+	},
 	computed: {
-		...mapState("modals/editNews", {
-			news: state => state.news
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
+		...mapState("modals/editNews", { news: state => state.news }),
+		...mapGetters({ socket: "websockets/getSocket" })
 	},
 	mounted() {
-		this.socket.dispatch(`news.getNewsFromId`, this.newsId, res => {
-			if (res.status === "success") {
-				const { news } = res.data;
-				this.editNews(news);
-			} else {
-				new Toast("News with that ID not found");
-				this.closeModal("editNews");
-			}
-		});
+		if (this.newsId) {
+			this.socket.dispatch(`news.getNewsFromId`, this.newsId, res => {
+				if (res.status === "success") {
+					const { markdown, status } = res.data.news;
+					this.markdown = markdown;
+					this.status = status;
+				} else {
+					new Toast("News with that ID not found.");
+					this.closeModal("editNews");
+				}
+			});
+		}
 	},
 	methods: {
-		add(type) {
-			const change = this.$refs[`edit-${type}`].value.trim();
-
-			if (this.news[type].indexOf(change) !== -1)
-				return new Toast(`Tag already exists`);
-
-			if (change) this.addChange({ type, change });
-			else new Toast(`${type} cannot be empty`);
+		marked,
+		getTitle() {
+			let title = "";
+			const preview = document.getElementById("preview");
+
+			// validate existence of h1 for the page title
+			if (preview.childNodes[0].tagName !== "H1") {
+				for (
+					let node = 0;
+					node < preview.childNodes.length;
+					node += 1
+				) {
+					if (preview.childNodes[node].tagName) {
+						if (preview.childNodes[node].tagName === "H1")
+							title = preview.childNodes[node].innerText;
+
+						break;
+					}
+				}
+			} else title = preview.childNodes[0].innerText;
 
-			this.$refs[`edit-${type}`].value = "";
-			return true;
+			return title;
 		},
-		remove(type, index) {
-			this.removeChange({ type, index });
+		create(close) {
+			const title = this.getTitle();
+			if (!title)
+				return new Toast(
+					"Please provide a title (heading level 1) at the top of the document."
+				);
+
+			return this.socket.dispatch(
+				"news.create",
+				{
+					title,
+					markdown: this.markdown,
+					status: this.status
+				},
+				res => {
+					new Toast(res.message);
+					if (res.status === "success" && close)
+						this.closeModal("editNews");
+				}
+			);
 		},
-		updateNews(close) {
-			this.socket.dispatch(
+		update(close) {
+			const title = this.getTitle();
+			if (!title)
+				return new Toast(
+					"Please provide a title (heading level 1) at the top of the document."
+				);
+
+			return this.socket.dispatch(
 				"news.update",
-				this.news._id,
-				this.news,
+				this.newsId,
+				{
+					title,
+					markdown: this.markdown,
+					status: this.status
+				},
 				res => {
 					new Toast(res.message);
-					if (res.status === "success") {
-						if (close) this.closeModal("editNews");
-					}
+					if (res.status === "success" && close)
+						this.closeModal("editNews");
 				}
 			);
 		},
@@ -224,148 +151,37 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-input[type="range"] {
-	-webkit-appearance: none;
-	width: 100%;
-	margin: 7.3px 0;
-}
-
-input[type="range"]:focus {
-	outline: none;
-}
-
-input[type="range"]::-webkit-slider-runnable-track {
-	width: 100%;
-	height: 5.2px;
-	cursor: pointer;
-	box-shadow: 0;
-	background: var(--light-grey-3);
-	border-radius: 0;
-	border: 0;
-}
-
-input[type="range"]::-webkit-slider-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 19px;
-	width: 19px;
-	border-radius: 15px;
-	background: var(--primary-color);
-	cursor: pointer;
-	-webkit-appearance: none;
-	margin-top: -6.5px;
-}
-
-input[type="range"]::-moz-range-track {
-	width: 100%;
-	height: 5.2px;
-	cursor: pointer;
-	box-shadow: 0;
-	background: var(--light-grey-3);
-	border-radius: 0;
-	border: 0;
-}
-
-input[type="range"]::-moz-range-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 19px;
-	width: 19px;
-	border-radius: 15px;
-	background: var(--primary-color);
-	cursor: pointer;
-	-webkit-appearance: none;
-	margin-top: -6.5px;
-}
-
-input[type="range"]::-ms-track {
-	width: 100%;
-	height: 5.2px;
-	cursor: pointer;
-	box-shadow: 0;
-	background: var(--light-grey-3);
-	border-radius: 1.3px;
-}
-
-input[type="range"]::-ms-fill-lower {
-	background: var(--light-grey-3);
-	border: 0;
-	border-radius: 0;
-	box-shadow: 0;
-}
-
-input[type="range"]::-ms-fill-upper {
-	background: var(--light-grey-3);
-	border: 0;
-	border-radius: 0;
-	box-shadow: 0;
-}
-
-input[type="range"]::-ms-thumb {
-	box-shadow: 0;
-	border: 0;
-	height: 15px;
-	width: 15px;
-	border-radius: 15px;
-	background: var(--primary-color);
-	cursor: pointer;
-	-webkit-appearance: none;
-	margin-top: 1.5px;
-}
-
-.controls {
-	display: flex;
-	flex-direction: column;
-	align-items: center;
-}
-
-.artist-genres {
-	display: flex;
-	justify-content: space-between;
-}
-
-#volumeSlider {
-	margin-bottom: 15px;
-}
-
-.has-text-centered {
-	padding: 10px;
-}
-
-.thumbnail-preview {
-	display: flex;
-	margin: 0 auto 25px auto;
-	max-width: 200px;
-	width: 100%;
-}
-
-.modal-card-body,
-.modal-card-foot {
-	border-top: 0;
-}
-
-.label,
-.checkbox,
-h5 {
-	font-weight: normal;
-}
-
-.video-container {
+#markdown-editor-and-preview {
 	display: flex;
-	flex-direction: column;
-	align-items: center;
-	padding: 10px;
+	flex-wrap: wrap;
+
+	.column {
+		display: flex;
+		flex-direction: column;
+		width: 350px;
+		flex-grow: 1;
+		flex-basis: initial;
+	}
 
-	iframe {
-		pointer-events: none;
+	textarea {
+		border: 0;
+		outline: none;
+		resize: none;
+		margin-right: 5px;
+		font-size: 16px;
 	}
-}
 
-.save-changes {
-	color: var(--white);
-}
+	#preview {
+		word-break: break-all;
+		overflow: auto;
+	}
 
-.tag:not(:last-child) {
-	margin-right: 5px;
+	textarea,
+	#preview {
+		padding: 5px;
+		border: 1px solid var(--light-grey-3);
+		border-radius: 3px;
+		height: 500px;
+	}
 }
 </style>

+ 2 - 42
frontend/src/components/modals/WhatIsNew.vue

@@ -15,46 +15,7 @@
 			</header>
 			<section class="modal-card-body">
 				<div class="content">
-					<p>{{ news.description }}</p>
-				</div>
-				<div v-show="news.features.length > 0" class="sect">
-					<div class="sect-head-features">
-						The features are so great
-					</div>
-					<ul class="sect-body">
-						<li v-for="feature in news.features" :key="feature">
-							{{ feature }}
-						</li>
-					</ul>
-				</div>
-				<div v-show="news.improvements.length > 0" class="sect">
-					<div class="sect-head-improvements">Improvements</div>
-					<ul class="sect-body">
-						<li
-							v-for="improvement in news.improvements"
-							:key="improvement"
-						>
-							{{ improvement }}
-						</li>
-					</ul>
-				</div>
-				<div v-show="news.bugs.length > 0" class="sect">
-					<div class="sect-head-bugs">Bugs Smashed</div>
-					<ul class="sect-body">
-						<li v-for="bug in news.bugs" :key="bug">
-							{{ bug }}
-						</li>
-					</ul>
-				</div>
-				<div v-show="news.upcoming.length > 0" class="sect">
-					<div class="sect-head-upcoming">
-						Coming Soon to a Musare near you
-					</div>
-					<ul class="sect-body">
-						<li v-for="upcoming in news.upcoming" :key="upcoming">
-							{{ upcoming }}
-						</li>
-					</ul>
+					<p>{{ news.markdown }}</p>
 				</div>
 			</section>
 		</div>
@@ -95,9 +56,8 @@ export default {
 					if (
 						parseInt(localStorage.getItem("firstVisited")) <
 						news.createdAt
-					) {
+					)
 						this.toggleModal();
-					}
 					localStorage.setItem("whatIsNew", news.createdAt);
 				}
 			} else if (!localStorage.getItem("firstVisited"))

+ 30 - 234
frontend/src/pages/Admin/tabs/News.vue

@@ -5,29 +5,31 @@
 			<table class="table is-striped">
 				<thead>
 					<tr>
+						<td>Status</td>
 						<td>Title</td>
-						<td>Description</td>
-						<td>Bugs</td>
-						<td>Features</td>
-						<td>Improvements</td>
-						<td>Upcoming</td>
+						<td>Author</td>
+						<td>Markdown</td>
 						<td>Options</td>
 					</tr>
 				</thead>
 				<tbody>
 					<tr v-for="news in news" :key="news._id">
+						<td class="news-status">{{ news.status }}</td>
 						<td>
 							<strong>{{ news.title }}</strong>
 						</td>
-						<td>{{ news.description }}</td>
-						<td>{{ news.bugs.join(", ") }}</td>
-						<td>{{ news.features.join(", ") }}</td>
-						<td>{{ news.improvements.join(", ") }}</td>
-						<td>{{ news.upcoming.join(", ") }}</td>
+						<td>
+							<user-id-to-username
+								:user-id="news.createdBy"
+								:alt="news.createdBy"
+								:link="true"
+							/>
+						</td>
+						<td>{{ news.markdown }}</td>
 						<td>
 							<button
 								class="button is-primary"
-								@click="editNewsClick(news)"
+								@click="edit(news._id)"
 							>
 								Edit
 							</button>
@@ -39,168 +41,9 @@
 				</tbody>
 			</table>
 
-			<div class="card is-fullwidth">
-				<header class="card-header">
-					<p class="card-header-title">Create News</p>
-				</header>
-				<div class="card-content">
-					<div class="content">
-						<label class="label">Title & Description</label>
-						<div class="control is-horizontal">
-							<div class="control is-grouped">
-								<p class="control is-expanded">
-									<input
-										v-model="creating.title"
-										class="input"
-										type="text"
-										placeholder="Title"
-									/>
-								</p>
-								<p class="control is-expanded">
-									<input
-										v-model="creating.description"
-										class="input"
-										type="text"
-										placeholder="Short description"
-									/>
-								</p>
-							</div>
-						</div>
-
-						<div class="columns">
-							<div class="column">
-								<label class="label">Bugs</label>
-								<p class="control has-addons">
-									<input
-										ref="new-bugs"
-										class="input"
-										type="text"
-										placeholder="Bug"
-										@keyup.enter="addChange('bugs')"
-									/>
-									<a
-										class="button is-info"
-										href="#"
-										@click="addChange('bugs')"
-										>Add</a
-									>
-								</p>
-								<span
-									v-for="(bug, index) in creating.bugs"
-									:key="bug"
-									class="tag is-info"
-								>
-									{{ bug }}
-									<button
-										class="delete is-info"
-										@click="removeChange('bugs', index)"
-									/>
-								</span>
-							</div>
-							<div class="column">
-								<label class="label">Features</label>
-								<p class="control has-addons">
-									<input
-										ref="new-features"
-										class="input"
-										type="text"
-										placeholder="Feature"
-										@keyup.enter="addChange('features')"
-									/>
-									<a
-										class="button is-info"
-										href="#"
-										@click="addChange('features')"
-										>Add</a
-									>
-								</p>
-								<span
-									v-for="(feature,
-									index) in creating.features"
-									:key="feature"
-									class="tag is-info"
-								>
-									{{ feature }}
-									<button
-										class="delete is-info"
-										@click="removeChange('features', index)"
-									/>
-								</span>
-							</div>
-						</div>
-
-						<div class="columns">
-							<div class="column">
-								<label class="label">Improvements</label>
-								<p class="control has-addons">
-									<input
-										ref="new-improvements"
-										class="input"
-										type="text"
-										placeholder="Improvement"
-										@keyup.enter="addChange('improvements')"
-									/>
-									<a
-										class="button is-info"
-										href="#"
-										@click="addChange('improvements')"
-										>Add</a
-									>
-								</p>
-								<span
-									v-for="(improvement,
-									index) in creating.improvements"
-									:key="improvement"
-									class="tag is-info"
-								>
-									{{ improvement }}
-									<button
-										class="delete is-info"
-										@click="
-											removeChange('improvements', index)
-										"
-									/>
-								</span>
-							</div>
-							<div class="column">
-								<label class="label">Upcoming</label>
-								<p class="control has-addons">
-									<input
-										ref="new-upcoming"
-										class="input"
-										type="text"
-										placeholder="Upcoming"
-										@keyup.enter="addChange('upcoming')"
-									/>
-									<a
-										class="button is-info"
-										href="#"
-										@click="addChange('upcoming')"
-										>Add</a
-									>
-								</p>
-								<span
-									v-for="(upcoming,
-									index) in creating.upcoming"
-									:key="upcoming"
-									class="tag is-info"
-								>
-									{{ upcoming }}
-									<button
-										class="delete is-info"
-										@click="removeChange('upcoming', index)"
-									/>
-								</span>
-							</div>
-						</div>
-					</div>
-				</div>
-				<footer class="card-footer">
-					<a class="card-footer-item" @click="createNews()" href="#"
-						>Create</a
-					>
-				</footer>
-			</div>
+			<button class="is-primary button" @click="edit()">
+				Create News Item
+			</button>
 		</div>
 
 		<edit-news
@@ -213,28 +56,22 @@
 
 <script>
 import { mapActions, mapState, mapGetters } from "vuex";
-
 import Toast from "toasters";
+
 import ws from "@/ws";
 
 import Confirm from "@/components/Confirm.vue";
+import UserIdToUsername from "@/components/UserIdToUsername.vue";
 
 export default {
 	components: {
 		Confirm,
+		UserIdToUsername,
 		EditNews: () => import("@/components/modals/EditNews.vue")
 	},
 	data() {
 		return {
-			editingNewsId: "",
-			creating: {
-				title: "",
-				description: "",
-				bugs: [],
-				features: [],
-				improvements: [],
-				upcoming: []
-			}
+			editingNewsId: ""
 		};
 	},
 	computed: {
@@ -250,8 +87,7 @@ export default {
 	},
 	mounted() {
 		this.socket.dispatch("news.index", res => {
-			if (res.status === "success")
-				res.data.news.forEach(news => this.addNews(news));
+			if (res.status === "success") this.setNews(res.data.news);
 		});
 
 		this.socket.on("event:admin.news.created", res =>
@@ -270,35 +106,10 @@ export default {
 		ws.onConnect(() => this.init());
 	},
 	methods: {
-		createNews() {
-			const {
-				creating: { bugs, features, improvements, upcoming }
-			} = this;
-
-			if (this.creating.title === "")
-				return new Toast("Field (Title) cannot be empty");
-			if (this.creating.description === "")
-				return new Toast("Field (Description) cannot be empty");
-			if (
-				bugs.length <= 0 &&
-				features.length <= 0 &&
-				improvements.length <= 0 &&
-				upcoming.length <= 0
-			)
-				return new Toast("You must have at least one News Item");
-
-			return this.socket.dispatch("news.create", this.creating, res => {
-				new Toast(res.message, 4000);
-				if (res.status === "success")
-					this.creating = {
-						title: "",
-						description: "",
-						bugs: [],
-						features: [],
-						improvements: [],
-						upcoming: []
-					};
-			});
+		edit(id) {
+			if (id) this.editingNewsId = id;
+			else this.editingNewsId = "";
+			this.openModal("editNews");
 		},
 		remove(id) {
 			this.socket.dispatch(
@@ -307,26 +118,6 @@ export default {
 				res => new Toast(res.message)
 			);
 		},
-		editNewsClick(news) {
-			this.editingNewsId = news._id;
-			this.openModal("editNews");
-		},
-		addChange(type) {
-			const change = this.$refs[`new-${type}`].value.trim();
-
-			if (this.creating[type].indexOf(change) !== -1)
-				return new Toast(`Tag already exists`);
-
-			if (change) {
-				this.$refs[`new-${type}`].value = "";
-				this.creating[type].push(change);
-				return true;
-			}
-			return new Toast(`${type} cannot be empty`);
-		},
-		removeChange(type, index) {
-			this.creating[type].splice(index, 1);
-		},
 		init() {
 			this.socket.dispatch("apis.joinAdminRoom", "news", () => {});
 		},
@@ -334,6 +125,7 @@ export default {
 		...mapActions("admin/news", [
 			"editNews",
 			"addNews",
+			"setNews",
 			"removeNews",
 			"updateNews"
 		])
@@ -400,4 +192,8 @@ td {
 .card-footer-item {
 	color: var(--primary-color);
 }
+
+.news-status {
+	text-transform: capitalize;
+}
 </style>

+ 10 - 60
frontend/src/pages/News.vue

@@ -16,58 +16,14 @@
 					</header>
 					<div class="card-content">
 						<div class="content">
-							<p>{{ item.description }}</p>
-						</div>
-						<div v-show="item.features.length > 0" class="sect">
-							<div class="sect-head-features">
-								The features are so great
-							</div>
-							<ul class="sect-body">
-								<li
-									v-for="feature in item.features"
-									:key="feature"
-								>
-									{{ feature }}
-								</li>
-							</ul>
-						</div>
-						<div v-show="item.improvements.length > 0" class="sect">
-							<div class="sect-head-improvements">
-								Improvements
-							</div>
-							<ul class="sect-body">
-								<li
-									v-for="improvement in item.improvements"
-									:key="improvement"
-								>
-									{{ improvement }}
-								</li>
-							</ul>
-						</div>
-						<div v-show="item.bugs.length > 0" class="sect">
-							<div class="sect-head-bugs">Bugs Smashed</div>
-							<ul class="sect-body">
-								<li v-for="bug in item.bugs" :key="bug">
-									{{ bug }}
-								</li>
-							</ul>
-						</div>
-						<div v-show="item.upcoming.length > 0" class="sect">
-							<div class="sect-head-upcoming">
-								Coming Soon to a Musare near you
-							</div>
-							<ul class="sect-body">
-								<li
-									v-for="upcoming in item.upcoming"
-									:key="upcoming"
-								>
-									{{ upcoming }}
-								</li>
-							</ul>
+							<p>{{ item.markdown }}</p>
 						</div>
 					</div>
 				</div>
-				<h3 v-if="noFound" class="has-text-centered page-title">
+				<h3
+					v-if="news.length === 0"
+					class="has-text-centered page-title"
+				>
 					No news items were found.
 				</h3>
 			</div>
@@ -87,8 +43,7 @@ export default {
 	components: { MainHeader, MainFooter },
 	data() {
 		return {
-			news: [],
-			noFound: false
+			news: []
 		};
 	},
 	computed: mapGetters({
@@ -96,15 +51,11 @@ export default {
 	}),
 	mounted() {
 		this.socket.dispatch("news.index", res => {
-			if (res.status === "success") {
-				this.news = res.data.news;
-				if (this.news.length === 0) this.noFound = true;
-			}
-		});
-		this.socket.on("event:admin.news.created", res => {
-			this.news.unshift(res.data.news);
-			this.noFound = false;
+			if (res.status === "success") this.news = res.data.news;
 		});
+		this.socket.on("event:admin.news.created", res =>
+			this.news.unshift(res.data.news)
+		);
 		this.socket.on("event:admin.news.updated", res => {
 			for (let n = 0; n < this.news.length; n += 1) {
 				if (this.news[n]._id === res.data.news._id) {
@@ -114,7 +65,6 @@ export default {
 		});
 		this.socket.on("event:admin.news.removed", res => {
 			this.news = this.news.filter(item => item._id !== res.data.newsId);
-			if (this.news.length === 0) this.noFound = true;
 		});
 	},
 	methods: {

+ 0 - 2
frontend/src/store/index.js

@@ -13,7 +13,6 @@ import admin from "./modules/admin";
 import editSongModal from "./modules/modals/editSong";
 import manageStationModal from "./modules/modals/manageStation";
 import editUserModal from "./modules/modals/editUser";
-import editNewsModal from "./modules/modals/editNews";
 import viewPunishmentModal from "./modules/modals/viewPunishment";
 import viewReportModal from "./modules/modals/viewReport";
 import reportModal from "./modules/modals/report";
@@ -34,7 +33,6 @@ export default new Vuex.Store({
 				editSong: editSongModal,
 				manageStation: manageStationModal,
 				editUser: editUserModal,
-				editNews: editNewsModal,
 				viewPunishment: viewPunishmentModal,
 				viewReport: viewReportModal,
 				report: reportModal

+ 4 - 0
frontend/src/store/modules/admin.js

@@ -156,12 +156,16 @@ const modules = {
 		},
 		getters: {},
 		actions: {
+			setNews: ({ commit }, news) => commit("setNews", news),
 			addNews: ({ commit }, news) => commit("addNews", news),
 			removeNews: ({ commit }, newsId) => commit("removeNews", newsId),
 			updateNews: ({ commit }, updatedNews) =>
 				commit("updateNews", updatedNews)
 		},
 		mutations: {
+			setNews(state, news) {
+				state.news = news;
+			},
 			addNews(state, news) {
 				state.news.push(news);
 			},

+ 0 - 25
frontend/src/store/modules/modals/editNews.js

@@ -1,25 +0,0 @@
-/* eslint no-param-reassign: 0 */
-
-export default {
-	namespaced: true,
-	state: {
-		news: {}
-	},
-	getters: {},
-	actions: {
-		editNews: ({ commit }, news) => commit("editNews", news),
-		addChange: ({ commit }, data) => commit("addChange", data),
-		removeChange: ({ commit }, data) => commit("removeChange", data)
-	},
-	mutations: {
-		editNews(state, news) {
-			state.news = news;
-		},
-		addChange(state, data) {
-			state.news[data.type].push(data.change);
-		},
-		removeChange(state, data) {
-			state.news[data.type].splice(data.index, 1);
-		}
-	}
-};