Ver código fonte

Admins can now update and remove News using pub/sub

theflametrooper 8 anos atrás
pai
commit
19e44558c6

+ 42 - 0
backend/logic/actions/news.js

@@ -16,6 +16,22 @@ cache.sub('news.create', news => {
 	});
 });
 
+cache.sub('news.remove', news => {
+	utils.socketsFromUser(news.createdBy, sockets => {
+		sockets.forEach(socket => {
+			socket.emit('event:admin.news.removed', news);
+		});
+	});
+});
+
+cache.sub('news.update', news => {
+	utils.socketsFromUser(news.createdBy, sockets => {
+		sockets.forEach(socket => {
+			socket.emit('event:admin.news.updated', news);
+		});
+	});
+});
+
 module.exports = {
 
 	index: (session, cb) => {
@@ -52,6 +68,32 @@ module.exports = {
 		});
 	}),
 
+	remove: hooks.adminRequired((session, news, cb, userId) => {
+		db.models.news.remove({ _id: news._id }, err => {
+			if (err) {
+				logger.log("NEWS_REMOVE", "ERROR", `Creating news failed. "${err.message}"`);
+				return cb({ 'status': 'failure', 'message': 'Something went wrong' });
+			} else {
+				cache.pub('news.remove', news);
+				logger.log("NEWS_REMOVE", "SUCCESS", `Removing news successful.`);
+				return cb({ 'status': 'success', 'message': 'Successfully removed News' });
+			}
+		});
+	}),
+
+	update: hooks.adminRequired((session, _id, news, cb, userId) => {
+		db.models.news.update({ _id }, news, { upsert: true }, err => {
+			if (err) {
+				logger.log("NEWS_UPDATE", "ERROR", `Updating news failed. "${err.message}"`);
+				return cb({ 'status': 'failure', 'message': 'Something went wrong' });
+			} else {
+				cache.pub('news.update', news);
+				logger.log("NEWS_UPDATE", "SUCCESS", `Updating news successful.`);
+				return cb({ 'status': 'success', 'message': 'Successfully updated News' });
+			}
+		});
+	}),
+
 	newest: (session, cb) => {
 		async.waterfall([
 			(next) => {

+ 89 - 19
frontend/components/Admin/News.vue

@@ -1,5 +1,35 @@
 <template>
 	<div class='container'>
+		<table class='table is-striped'>
+			<thead>
+				<tr>
+					<td>Title</td>
+					<td>Description</td>
+					<td>Bugs</td>
+					<td>Features</td>
+					<td>Improvements</td>
+					<td>Upcoming</td>
+					<td>Options</td>
+				</tr>
+			</thead>
+			<tbody>
+				<tr v-for='(index, news) in news' track-by='$index'>
+					<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>
+						<button class='button is-primary' @click='editNews(news)'>Edit</button>
+						<button class='button is-danger' @click='removeNews(news)'>Remove</button>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+
 		<div class='card is-fullwidth'>
 			<header class='card-header'>
 				<p class='card-header-title'>Create News</p>
@@ -11,10 +41,10 @@
 					<div class='control is-horizontal'>
 						<div class='control is-grouped'>
 							<p class='control is-expanded'>
-								<input class='input' type='text' placeholder='Title' v-model='news.title'>
+								<input class='input' type='text' placeholder='Title' v-model='creating.title'>
 							</p>
 							<p class='control is-expanded'>
-								<input class='input' type='text' placeholder='Short description' v-model='news.description'>
+								<input class='input' type='text' placeholder='Short description' v-model='creating.description'>
 							</p>
 						</div>
 					</div>
@@ -24,9 +54,9 @@
 							<label class='label'>Bugs</label>
 							<p class='control has-addons'>
 								<input class='input' id='new-bugs' type='text' placeholder='Bug' v-on:keyup.enter='addChange("bugs")'>
-								<a class='button is-info' href='#' @click='addChange("bugs")'>Add Bug</a>
+								<a class='button is-info' href='#' @click='addChange("bugs")'>Add</a>
 							</p>
-							<span class='tag is-info' v-for='(index, bug) in news.bugs' track-by='$index'>
+							<span class='tag is-info' v-for='(index, bug) in creating.bugs' track-by='$index'>
 								{{ bug }}
 								<button class='delete is-info' @click='removeChange("bugs", index)'></button>
 							</span>
@@ -35,9 +65,9 @@
 							<label class='label'>Features</label>
 							<p class='control has-addons'>
 								<input class='input' id='new-features' type='text' placeholder='Feature' v-on:keyup.enter='addChange("features")'>
-								<a class='button is-info' href='#' @click='addChange("features")'>Add Feature</a>
+								<a class='button is-info' href='#' @click='addChange("features")'>Add</a>
 							</p>
-							<span class='tag is-info' v-for='(index, feature) in news.features' track-by='$index'>
+							<span class='tag is-info' v-for='(index, feature) in creating.features' track-by='$index'>
 								{{ feature }}
 								<button class='delete is-info' @click='removeChange("features", index)'></button>
 							</span>
@@ -49,9 +79,9 @@
 							<label class='label'>Improvements</label>
 							<p class='control has-addons'>
 								<input class='input' id='new-improvements' type='text' placeholder='Improvement' v-on:keyup.enter='addChange("improvements")'>
-								<a class='button is-info' href='#' @click='addChange("improvements")'>Add Improvement</a>
+								<a class='button is-info' href='#' @click='addChange("improvements")'>Add</a>
 							</p>
-							<span class='tag is-info' v-for='(index, improvement) in news.improvements' track-by='$index'>
+							<span class='tag is-info' v-for='(index, improvement) in creating.improvements' track-by='$index'>
 								{{ improvement }}
 								<button class='delete is-info' @click='removeChange("improvements", index)'></button>
 							</span>
@@ -60,9 +90,9 @@
 							<label class='label'>Upcoming</label>
 							<p class='control has-addons'>
 								<input class='input' id='new-upcoming' type='text' placeholder='Upcoming' v-on:keyup.enter='addChange("upcoming")'>
-								<a class='button is-info' href='#' @click='addChange("upcoming")'>Add Upcoming Change</a>
+								<a class='button is-info' href='#' @click='addChange("upcoming")'>Add</a>
 							</p>
-							<span class='tag is-info' v-for='(index, upcoming) in news.upcoming' track-by='$index'>
+							<span class='tag is-info' v-for='(index, upcoming) in creating.upcoming' track-by='$index'>
 								{{ upcoming }}
 								<button class='delete is-info' @click='removeChange("upcoming", index)'></button>
 							</span>
@@ -76,52 +106,81 @@
 			</footer>
 		</div>
 	</div>
+
+	<edit-news v-if='modals.editNews'></edit-news>
 </template>
 
 <script>
 	import { Toast } from 'vue-roaster';
 	import io from '../../io';
 
+	import EditNews from '../Modals/EditNews.vue';
+
 	export default {
+		components: { EditNews },
 		data() {
 			return {
-				news: {
+				modals: { editNews: false },
+				news: [],
+				creating: {
 					title: '',
 					description: '',
 					bugs: [],
 					features: [],
 					improvements: [],
 					upcoming: []
-				}
+				},
+				editing: {}
 			}
 		},
 		methods: {
+			toggleModal: function () {
+				this.modals.editNews = !this.modals.editNews;
+			},
 			createNews: function () {
 				let _this = this;
 
-				let { news: { bugs, features, improvements, upcoming } } = this;
+				let { creating: { bugs, features, improvements, upcoming } } = this;
 
-				if (this.news.title === '') return Toast.methods.addToast('Field (Title) cannot be empty', 3000);
-				if (this.news.description === '') return Toast.methods.addToast('Field (Description) cannot be empty', 3000);
+				if (this.creating.title === '') return Toast.methods.addToast('Field (Title) cannot be empty', 3000);
+				if (this.creating.description === '') return Toast.methods.addToast('Field (Description) cannot be empty', 3000);
 				if (
 					bugs.length <= 0 && features.length <= 0 && 
 					improvements.length <= 0 && upcoming.length <= 0
 				) return Toast.methods.addToast('You must have at least one News Item', 3000);
 
-				_this.socket.emit('news.create', _this.news, result => {
+				_this.socket.emit('news.create', _this.creating, result => {
 					Toast.methods.addToast(result.message, 4000);
 				});
 			},
+			removeNews: function (news) {
+				this.socket.emit('news.remove', news, res => {
+					Toast.methods.addToast(res.message, 8000);
+				});
+			},
+			editNews: function (news) {
+				this.editing = news;
+				this.toggleModal();
+			},
+			updateNews: function (close) {
+				let _this = this;
+				this.socket.emit('news.update', _this.editing._id, _this.editing, res => {
+					Toast.methods.addToast(res.message, 4000);
+					if (res.status === 'success') {
+						if (close) _this.toggleModal();
+					}
+				});
+			},
 			addChange: function (type) {
 				let change = $(`#new-${type}`).val().trim();
 
-				if (this.news[type].indexOf(change) !== -1) return Toast.methods.addToast(`Tag already exists`, 3000);
+				if (this.creating[type].indexOf(change) !== -1) return Toast.methods.addToast(`Tag already exists`, 3000);
 
-				if (change) this.news[type].push(change);
+				if (change) this.creating[type].push(change);
 				else Toast.methods.addToast(`${type} cannot be empty`, 3000);
 			},
 			removeChange: function (type, index) {
-				this.news[type].splice(index, 1);
+				this.creating[type].splice(index, 1);
 			},
 			init: function () {
 				this.socket.emit('apis.joinAdminRoom', 'news', data => {});
@@ -131,6 +190,15 @@
 			let _this = this;
 			io.getSocket((socket) => {
 				_this.socket = socket;
+				_this.socket.emit('news.index', result => {
+					_this.news = result.data;
+				});
+				_this.socket.on('event:admin.news.created', news => {
+					_this.news.unshift(news);
+				});
+				_this.socket.on('event:admin.news.removed', news => {
+					_this.news = _this.news.filter(item => item._id !== news._id);
+				});
 				if (_this.socket.connected) _this.init();
 				io.onConnect(() => {
 					_this.init();
@@ -143,6 +211,8 @@
 <style lang='scss' scoped>
 	.tag:not(:last-child) { margin-right: 5px; }
 
+	td { vertical-align: middle; }
+
 	.is-info:focus { background-color: #0398db; }
 
 	.card-footer-item { color: #03A9F4; }

+ 1 - 3
frontend/components/Admin/Songs.vue

@@ -135,9 +135,7 @@
 							}
 						});
 					}
-					if (close) {
-						_this.toggleModal();
-					}
+					if (close) _this.toggleModal();
 				});
 			},
 			remove: function (id, index) {

+ 236 - 0
frontend/components/Modals/EditNews.vue

@@ -0,0 +1,236 @@
+<template>
+	<modal title='Edit News'>
+		<div slot='body'>
+			<label class='label'>Title</label>
+			<p class='control'>
+				<input class='input' type='text' placeholder='News Title' v-model='$parent.editing.title' autofocus>
+			</p>
+			<label class='label'>Description</label>
+			<p class='control'>
+				<input class='input' type='text' placeholder='News Description' v-model='$parent.editing.description'>
+			</p>
+			<div class="columns">
+				<div class="column">
+					<label class='label'>Bugs</label>
+					<p class='control has-addons'>
+						<input class='input' id='edit-bugs' type='text' placeholder='Bug' v-on:keyup.enter='addChange("bugs")'>
+						<a class='button is-info' href='#' @click='addChange("bugs")'>Add</a>
+					</p>
+					<span class='tag is-info' v-for='(index, bug) in $parent.editing.bugs' track-by='$index'>
+						{{ bug }}
+						<button class='delete is-info' @click='removeChange("bugs", index)'></button>
+					</span>
+				</div>
+				<div class="column">
+					<label class='label'>Features</label>
+					<p class='control has-addons'>
+						<input class='input' id='edit-features' type='text' placeholder='Feature' v-on:keyup.enter='addChange("features")'>
+						<a class='button is-info' href='#' @click='addChange("features")'>Add</a>
+					</p>
+					<span class='tag is-info' v-for='(index, feature) in $parent.editing.features' track-by='$index'>
+						{{ feature }}
+						<button class='delete is-info' @click='removeChange("features", index)'></button>
+					</span>
+				</div>
+			</div>
+
+			<div class="columns">
+				<div class="column">
+					<label class='label'>Improvements</label>
+					<p class='control has-addons'>
+						<input class='input' id='edit-improvements' type='text' placeholder='Improvement' v-on:keyup.enter='addChange("improvements")'>
+						<a class='button is-info' href='#' @click='addChange("improvements")'>Add</a>
+					</p>
+					<span class='tag is-info' v-for='(index, improvement) in $parent.editing.improvements' track-by='$index'>
+						{{ improvement }}
+						<button class='delete is-info' @click='removeChange("improvements", index)'></button>
+					</span>
+				</div>
+				<div class="column">
+					<label class='label'>Upcoming</label>
+					<p class='control has-addons'>
+						<input class='input' id='edit-upcoming' type='text' placeholder='Upcoming' v-on:keyup.enter='addChange("upcoming")'>
+						<a class='button is-info' href='#' @click='addChange("upcoming")'>Add</a>
+					</p>
+					<span class='tag is-info' v-for='(index, upcoming) in $parent.editing.upcoming' track-by='$index'>
+						{{ upcoming }}
+						<button class='delete is-info' @click='removeChange("upcoming", index)'></button>
+					</span>
+				</div>
+			</div>
+		</div>
+		<div slot='footer'>
+			<button class='button is-success' @click='$parent.updateNews(false)'>
+				<i class='material-icons save-changes'>done</i>
+				<span>&nbsp;Save</span>
+			</button>
+			<button class='button is-success' @click='$parent.updateNews(true)'>
+				<i class='material-icons save-changes'>done</i>
+				<span>&nbsp;Save and close</span>
+			</button>
+			<button class='button is-danger' @click='$parent.toggleModal()'>
+				<span>&nbsp;Close</span>
+			</button>
+		</div>
+	</modal>
+</template>
+
+<script>
+	import { Toast } from 'vue-roaster';
+
+	import Modal from './Modal.vue';
+
+	export default {
+		components: { Modal },
+		methods: {
+			addChange: function (type) {
+				let change = $(`#edit-${type}`).val().trim();
+
+				if (this.$parent.editing[type].indexOf(change) !== -1) return Toast.methods.addToast(`Tag already exists`, 3000);
+
+				if (change) this.$parent.editing[type].push(change);
+				else Toast.methods.addToast(`${type} cannot be empty`, 3000);
+			},
+			removeChange: function (type, index) {
+				this.$parent.editing[type].splice(index, 1);
+			},
+		},
+		events: {
+			closeModal: function() {
+				this.$parent.toggleModal();
+			}
+		}
+	}
+</script>
+
+<style type='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: #c2c0c2;
+		border-radius: 0;
+		border: 0;
+	}
+
+	input[type=range]::-webkit-slider-thumb {
+		box-shadow: 0;
+		border: 0;
+		height: 19px;
+		width: 19px;
+		border-radius: 15px;
+		background: #03a9f4;
+		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: #c2c0c2;
+		border-radius: 0;
+		border: 0;
+	}
+
+	input[type=range]::-moz-range-thumb {
+		box-shadow: 0;
+		border: 0;
+		height: 19px;
+		width: 19px;
+		border-radius: 15px;
+		background: #03a9f4;
+		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: #c2c0c2;
+		border-radius: 1.3px;
+	}
+
+	input[type=range]::-ms-fill-lower {
+		background: #c2c0c2;
+		border: 0;
+		border-radius: 0;
+		box-shadow: 0;
+	}
+
+	input[type=range]::-ms-fill-upper {
+		background: #c2c0c2;
+		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: #03a9f4;
+		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 {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		padding: 10px;
+
+		iframe { pointer-events: none; }
+	}
+
+	.save-changes { color: #fff; }
+
+	.tag:not(:last-child) { margin-right: 5px; }
+</style>

+ 11 - 1
frontend/components/pages/News.vue

@@ -53,7 +53,7 @@
 		methods: {
 			formatDate: unix => {
 				return moment(unix).format('DD-MM-YYYY');
-			},
+			}
 		},
 		data() {
 			return {
@@ -70,6 +70,16 @@
 				_this.socket.on('event:admin.news.created', news => {
 					_this.news.unshift(news);
 				});
+				_this.socket.on('event:admin.news.updated', news => {
+					for (let n = 0; n < _this.news.length; n++) {
+						if (_this.news[n]._id === news._id) {
+							_this.news.$set(n, news);
+						}
+					}
+				});
+				_this.socket.on('event:admin.news.removed', news => {
+					_this.news = _this.news.filter(item => item._id !== news._id);
+				});
 			});
 		}
 	}