mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-06 23:02:45 +00:00
Merge pull request #2586 from G-Ambatte/experimentalNotificationDB
This commit is contained in:
@@ -2,35 +2,44 @@ require('./admin.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
||||||
|
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
||||||
|
|
||||||
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
const tabGroups = ['brew', 'notifications'];
|
||||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
|
||||||
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
|
|
||||||
const Stats = require('./stats/stats.jsx');
|
|
||||||
|
|
||||||
const Admin = createClass({
|
const Admin = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState : function(){
|
||||||
|
return ({
|
||||||
|
currentTab : 'brew'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClick : function(newTab){
|
||||||
|
if(this.state.currentTab === newTab) return;
|
||||||
|
this.setState({
|
||||||
|
currentTab : newTab
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='admin'>
|
return <div className='admin'>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fas fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
homebrewery admin
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className='container'>
|
<main className='container'>
|
||||||
<Stats />
|
<nav className='tabs'>
|
||||||
<hr />
|
{tabGroups.map((tab, idx)=>{ return <button className={tab===this.state.currentTab ? 'active' : ''} key={idx} onClick={()=>{ return this.handleClick(tab); }}>{tab.toUpperCase()}</button>; })}
|
||||||
<BrewLookup />
|
</nav>
|
||||||
<hr />
|
{this.state.currentTab==='brew' && <BrewUtils />}
|
||||||
<BrewCleanup />
|
{this.state.currentTab==='notifications' && <NotificationUtils />}
|
||||||
<hr />
|
</main>
|
||||||
<BrewCompress />
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,39 +6,95 @@
|
|||||||
|
|
||||||
@import 'font-awesome/css/font-awesome.css';
|
@import 'font-awesome/css/font-awesome.css';
|
||||||
|
|
||||||
html,body, #reactContainer, .naturalCrit{
|
html,body, #reactContainer, .naturalCrit { min-height : 100%; }
|
||||||
min-height : 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@sidebarWidth : 250px;
|
@sidebarWidth : 250px;
|
||||||
|
|
||||||
body{
|
body {
|
||||||
background-color : #eee;
|
height : 100%;
|
||||||
font-family : 'Open Sans', sans-serif;
|
|
||||||
color : #4b5055;
|
|
||||||
font-weight : 100;
|
|
||||||
text-rendering : optimizeLegibility;
|
|
||||||
margin : 0;
|
|
||||||
padding : 0;
|
padding : 0;
|
||||||
height : 100%;
|
margin : 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-weight : 100;
|
||||||
|
color : #4B5055;
|
||||||
|
background-color : #EEEEEE;
|
||||||
|
text-rendering : optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin{
|
:where(.admin) {
|
||||||
|
|
||||||
header{
|
header {
|
||||||
|
padding : 20px 0px;
|
||||||
|
margin-bottom : 30px;
|
||||||
|
font-size : 2em;
|
||||||
|
color : white;
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
font-size: 2em;
|
i { margin-right : 30px; }
|
||||||
padding : 20px 0px;
|
}
|
||||||
color : white;
|
|
||||||
margin-bottom: 30px;
|
hr { margin : 30px 0px; }
|
||||||
i{
|
|
||||||
margin-right: 30px;
|
:where(.container) {
|
||||||
|
input {
|
||||||
|
height : 33px;
|
||||||
|
padding : 0px 10px;
|
||||||
|
margin-bottom : 20px;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
height : 37px;
|
||||||
|
vertical-align : middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
@maxItemWidth : 132px;
|
||||||
|
dt {
|
||||||
|
float : left;
|
||||||
|
width : @maxItemWidth;
|
||||||
|
clear : left;
|
||||||
|
text-align : right;
|
||||||
|
&::after { content : ' : '; }
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
height : 1em;
|
||||||
|
padding : 0 0 0.5em 0;
|
||||||
|
margin-left : @maxItemWidth + 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button {
|
||||||
|
margin-right : 3px;
|
||||||
|
margin-left : 3px;
|
||||||
|
color : black;
|
||||||
|
background-color : #EEEEEE;
|
||||||
|
border : 1px solid #444444;
|
||||||
|
border-radius : 5px;
|
||||||
|
&:hover {
|
||||||
|
color : #EEEEEE;
|
||||||
|
background-color : #444444;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
margin-right : 2px;
|
||||||
|
margin-left : 2px;
|
||||||
|
text-decoration : underline;
|
||||||
|
background-color : #CCCCCC;
|
||||||
|
border : 2px solid #444444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notificationUtils {
|
||||||
|
display : flex;
|
||||||
|
gap : 50px;
|
||||||
|
justify-content : space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr{
|
.error {
|
||||||
margin : 30px 0px;
|
background: rgb(178, 54, 54);
|
||||||
|
color:white;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-block:10px;
|
||||||
|
padding:10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
.BrewCleanup{
|
|
||||||
.removeBox{
|
|
||||||
margin-top: 20px;
|
|
||||||
button{
|
|
||||||
background-color: @red;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.BrewCompress{
|
|
||||||
.removeBox{
|
|
||||||
margin-top: 20px;
|
|
||||||
button{
|
|
||||||
background-color: @red;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
|
|
||||||
.brewLookup{
|
|
||||||
input{
|
|
||||||
height : 33px;
|
|
||||||
margin-bottom : 20px;
|
|
||||||
padding : 0px 10px;
|
|
||||||
font-family : monospace;
|
|
||||||
}
|
|
||||||
button{
|
|
||||||
vertical-align : middle;
|
|
||||||
height : 37px;
|
|
||||||
}
|
|
||||||
dl{
|
|
||||||
@maxItemWidth : 132px;
|
|
||||||
dt{
|
|
||||||
float : left;
|
|
||||||
clear : left;
|
|
||||||
width : @maxItemWidth;
|
|
||||||
text-align : right;
|
|
||||||
&::after {
|
|
||||||
content: " : ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dd{
|
|
||||||
height : 1em;
|
|
||||||
margin-left : @maxItemWidth + 6px;
|
|
||||||
padding : 0 0 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
client/admin/brewUtils/brewCleanup/brewCleanup.less
Normal file
9
client/admin/brewUtils/brewCleanup/brewCleanup.less
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.BrewCleanup {
|
||||||
|
.removeBox {
|
||||||
|
margin-top : 20px;
|
||||||
|
button {
|
||||||
|
margin-right : 10px;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
client/admin/brewUtils/brewCompress/brewCompress.less
Normal file
9
client/admin/brewUtils/brewCompress/brewCompress.less
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.BrewCompress {
|
||||||
|
.removeBox {
|
||||||
|
margin-top : 20px;
|
||||||
|
button {
|
||||||
|
margin-right : 10px;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
require('./brewLookup.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
24
client/admin/brewUtils/brewUtils.jsx
Normal file
24
client/admin/brewUtils/brewUtils.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
|
||||||
|
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
||||||
|
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||||
|
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
|
||||||
|
const Stats = require('./stats/stats.jsx');
|
||||||
|
|
||||||
|
const BrewUtils = createClass({
|
||||||
|
render : function(){
|
||||||
|
return <>
|
||||||
|
<Stats />
|
||||||
|
<hr />
|
||||||
|
<BrewLookup />
|
||||||
|
<hr />
|
||||||
|
<BrewCleanup />
|
||||||
|
<hr />
|
||||||
|
<BrewCompress />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = BrewUtils;
|
||||||
13
client/admin/brewUtils/stats/stats.less
Normal file
13
client/admin/brewUtils/stats/stats.less
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
.Stats {
|
||||||
|
position : relative;
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
position : absolute;
|
||||||
|
top : 0px;
|
||||||
|
left : 0px;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
background-color : rgba(238,238,238, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
require('./notificationAdd.less');
|
||||||
|
const React = require('react');
|
||||||
|
const { useState, useRef } = require('react');
|
||||||
|
const request = require('superagent');
|
||||||
|
|
||||||
|
const NotificationAdd = ()=>{
|
||||||
|
const [notificationResult, setNotificationResult] = useState(null);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const dismissKeyRef = useRef(null);
|
||||||
|
const titleRef = useRef(null);
|
||||||
|
const textRef = useRef(null);
|
||||||
|
const startAtRef = useRef(null);
|
||||||
|
const stopAtRef = useRef(null);
|
||||||
|
|
||||||
|
const saveNotification = async ()=>{
|
||||||
|
const dismissKey = dismissKeyRef.current.value;
|
||||||
|
const title = titleRef.current.value;
|
||||||
|
const text = textRef.current.value;
|
||||||
|
const startAt = new Date(startAtRef.current.value);
|
||||||
|
const stopAt = new Date(stopAtRef.current.value);
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if(!dismissKey || !title || !text || isNaN(startAt.getTime()) || isNaN(stopAt.getTime())) {
|
||||||
|
setError('All fields are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(startAt >= stopAt) {
|
||||||
|
setError('End date must be after the start date!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
dismissKey,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
startAt : startAt?.toISOString() ?? '',
|
||||||
|
stopAt : stopAt?.toISOString() ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSearching(true);
|
||||||
|
setError(null);
|
||||||
|
const response = await request.post('/admin/notification/add').send(data);
|
||||||
|
console.log(response.body);
|
||||||
|
|
||||||
|
// Reset form fields
|
||||||
|
dismissKeyRef.current.value = '';
|
||||||
|
titleRef.current.value = '';
|
||||||
|
textRef.current.value = '';
|
||||||
|
|
||||||
|
setNotificationResult('Notification successfully created.');
|
||||||
|
setSearching(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err.response.body.message);
|
||||||
|
setError(`Error saving notification: ${err.response.body.message}`);
|
||||||
|
setSearching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='notificationAdd'>
|
||||||
|
<h2>Add Notification</h2>
|
||||||
|
|
||||||
|
<label className='field'>
|
||||||
|
Dismiss Key:
|
||||||
|
<input className='fieldInput' type='text' ref={dismissKeyRef} required
|
||||||
|
placeholder='GOOGLEDRIVENOTIF'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='field'>
|
||||||
|
Title:
|
||||||
|
<input className='fieldInput' type='text' ref={titleRef} required
|
||||||
|
placeholder='Stop using Google Drive as image host'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='field'>
|
||||||
|
Text:
|
||||||
|
<textarea className='fieldInput' type='text' ref={textRef} required
|
||||||
|
placeholder='Google Drive is not an image hosting site, you should not use it as such.'
|
||||||
|
>
|
||||||
|
</textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='field'>
|
||||||
|
Start Date:
|
||||||
|
<input type='date' className='fieldInput' ref={startAtRef} required/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className='field'>
|
||||||
|
End Date:
|
||||||
|
<input type='date' className='fieldInput' ref={stopAtRef} required/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='notificationResult'>{notificationResult}</div>
|
||||||
|
|
||||||
|
<button className='notificationSave' onClick={saveNotification} disabled={searching}>
|
||||||
|
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-save'}`}/>
|
||||||
|
Save Notification
|
||||||
|
</button>
|
||||||
|
{error && <div className='error'>{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = NotificationAdd;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
.notificationAdd {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
width : 500px;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display : grid;
|
||||||
|
grid-template-columns : 120px 150px;
|
||||||
|
align-items : center;
|
||||||
|
justify-items : stretch;
|
||||||
|
width : 100%;
|
||||||
|
margin-bottom : 20px;
|
||||||
|
|
||||||
|
|
||||||
|
input {
|
||||||
|
height : 33px;
|
||||||
|
padding : 0px 10px;
|
||||||
|
margin-bottom : unset;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width : 50ch;
|
||||||
|
min-height : 7em;
|
||||||
|
max-height : 20em;
|
||||||
|
resize : vertical;
|
||||||
|
padding : 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 200px;
|
||||||
|
|
||||||
|
i { margin-right : 10px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
require('./notificationLookup.less');
|
||||||
|
|
||||||
|
const React = require('react');
|
||||||
|
const { useState } = require('react');
|
||||||
|
const request = require('superagent');
|
||||||
|
const Moment = require('moment');
|
||||||
|
|
||||||
|
const NotificationDetail = ({ notification, onDelete })=>(
|
||||||
|
<>
|
||||||
|
<dl>
|
||||||
|
<dt>Key</dt>
|
||||||
|
<dd>{notification.dismissKey}</dd>
|
||||||
|
|
||||||
|
<dt>Title</dt>
|
||||||
|
<dd>{notification.title || 'No Title'}</dd>
|
||||||
|
|
||||||
|
<dt>Text</dt>
|
||||||
|
<dd>{notification.text || 'No Text'}</dd>
|
||||||
|
|
||||||
|
<dt>Created</dt>
|
||||||
|
<dd>{Moment(notification.createdAt).format('LLLL')}</dd>
|
||||||
|
|
||||||
|
<dt>Start</dt>
|
||||||
|
<dd>{Moment(notification.startAt).format('LLLL') || 'No Start Time'}</dd>
|
||||||
|
|
||||||
|
<dt>Stop</dt>
|
||||||
|
<dd>{Moment(notification.stopAt).format('LLLL') || 'No End Time'}</dd>
|
||||||
|
</dl>
|
||||||
|
<button onClick={()=>onDelete(notification.dismissKey)}>DELETE</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotificationLookup = ()=>{
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
|
||||||
|
const lookupAll = async ()=>{
|
||||||
|
setSearching(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/notification/all');
|
||||||
|
setNotifications(res.body || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
setError(`Error looking up notifications: ${err.response.body.message}`);
|
||||||
|
} finally {
|
||||||
|
setSearching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNotification = async (dismissKey)=>{
|
||||||
|
if(!dismissKey) return;
|
||||||
|
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
`Really delete notification ${dismissKey}?`
|
||||||
|
);
|
||||||
|
if(!confirmed) {
|
||||||
|
console.log('Delete notification cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Delete notification confirm');
|
||||||
|
try {
|
||||||
|
await request.delete(`/admin/notification/delete/${dismissKey}`);
|
||||||
|
lookupAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
setError(`Error deleting notification: ${err.response.body.message}`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNotificationsList = ()=>{
|
||||||
|
if(error)
|
||||||
|
return <div className='error'>{error}</div>;
|
||||||
|
|
||||||
|
if(notifications.length === 0)
|
||||||
|
return <div className='noNotification'>No notifications available.</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className='notificationList'>
|
||||||
|
{notifications.map((notification)=>(
|
||||||
|
<li key={notification.dismissKey} >
|
||||||
|
<details>
|
||||||
|
<summary>{notification.title || 'No Title'}</summary>
|
||||||
|
<NotificationDetail notification={notification} onDelete={deleteNotification} />
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='notificationLookup'>
|
||||||
|
<h2>Check all Notifications</h2>
|
||||||
|
<button onClick={lookupAll}>
|
||||||
|
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-search'}`} />
|
||||||
|
</button>
|
||||||
|
{renderNotificationsList()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = NotificationLookup;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
.notificationLookup {
|
||||||
|
width : 450px;
|
||||||
|
height : fit-content;
|
||||||
|
|
||||||
|
.notificationList {
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
max-height : 500px;
|
||||||
|
margin-block : 20px;
|
||||||
|
overflow : auto;
|
||||||
|
border : 1px solid;
|
||||||
|
border-radius : 5px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding : 10px;
|
||||||
|
background : #CCCCCC;
|
||||||
|
|
||||||
|
&:nth-child(even) { background : #DDDDDD; }
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius : 5px;
|
||||||
|
border-top-right-radius : 5px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius : 5px;
|
||||||
|
border-bottom-left-radius : 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-size : 20px;
|
||||||
|
font-weight : 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dt{
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.noNotification { margin-block : 20px; }
|
||||||
|
}
|
||||||
15
client/admin/notificationUtils/notificationUtils.jsx
Normal file
15
client/admin/notificationUtils/notificationUtils.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const React = require('react');
|
||||||
|
|
||||||
|
const NotificationLookup = require('./notificationLookup/notificationLookup.jsx');
|
||||||
|
const NotificationAdd = require('./notificationAdd/notificationAdd.jsx');
|
||||||
|
|
||||||
|
const NotificationUtils = ()=>{
|
||||||
|
return (
|
||||||
|
<section className='notificationUtils'>
|
||||||
|
<NotificationAdd />
|
||||||
|
<NotificationLookup />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = NotificationUtils;
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
.Stats{
|
|
||||||
position : relative;
|
|
||||||
.pending{
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
left : 0px;
|
|
||||||
height : 100%;
|
|
||||||
width : 100%;
|
|
||||||
background-color : rgba(238,238,238, 0.5);
|
|
||||||
}
|
|
||||||
dl{
|
|
||||||
@maxItemWidth : 132px;
|
|
||||||
dt{
|
|
||||||
float : left;
|
|
||||||
clear : left;
|
|
||||||
width : @maxItemWidth;
|
|
||||||
text-align : right;
|
|
||||||
&::after {
|
|
||||||
content: " : ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dd{
|
|
||||||
margin : 0 0 0 @maxItemWidth + 10px;
|
|
||||||
padding : 0 0 0.5em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
|
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
|
const NotificationModel = require('./notifications.model.js').model;
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const Moment = require('moment');
|
const Moment = require('moment');
|
||||||
//const render = require('vitreum/steps/render');
|
//const render = require('vitreum/steps/render');
|
||||||
@@ -138,12 +139,48 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ####################### NOTIFICATIONS
|
||||||
|
|
||||||
|
router.get('/admin/notification/all', async (req, res, next)=>{
|
||||||
|
try {
|
||||||
|
const notifications = await NotificationModel.getAll();
|
||||||
|
return res.json(notifications);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error getting all notifications: ', error.message);
|
||||||
|
return res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
||||||
|
console.table(req.body);
|
||||||
|
try {
|
||||||
|
const notification = await NotificationModel.addNotification(req.body);
|
||||||
|
return res.status(201).json(notification);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error adding notification: ', error.message);
|
||||||
|
return res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
|
||||||
|
try {
|
||||||
|
const notification = await NotificationModel.deleteNotification(req.params.id);
|
||||||
|
return res.json(notification);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
|
||||||
|
return res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||||
templateFn('admin', {
|
templateFn('admin', {
|
||||||
url : req.originalUrl
|
url : req.originalUrl
|
||||||
})
|
})
|
||||||
.then((page)=>res.send(page))
|
.then((page)=>res.send(page))
|
||||||
.catch((err)=>res.sendStatus(500));
|
.catch((err)=>{
|
||||||
|
console.log(err);
|
||||||
|
res.sendStatus(500);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
116
server/admin.api.spec.js
Normal file
116
server/admin.api.spec.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
const supertest = require('supertest');
|
||||||
|
|
||||||
|
const app = supertest.agent(require('app.js').app)
|
||||||
|
.set('X-Forwarded-Proto', 'https');
|
||||||
|
|
||||||
|
const NotificationModel = require('./notifications.model.js').model;
|
||||||
|
|
||||||
|
describe('Tests for admin api', ()=>{
|
||||||
|
afterEach(()=>{
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Notifications', ()=>{
|
||||||
|
it('should return list of all notifications', async ()=>{
|
||||||
|
const testNotifications = ['a', 'b'];
|
||||||
|
|
||||||
|
jest.spyOn(NotificationModel, 'find')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.get('/admin/notification/all')
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual(testNotifications);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a new notification', async ()=>{
|
||||||
|
const inputNotification = {
|
||||||
|
title : 'Test Notification',
|
||||||
|
text : 'This is a test notification',
|
||||||
|
startAt : new Date().toISOString(),
|
||||||
|
stopAt : new Date().toISOString(),
|
||||||
|
dismissKey : 'testKey'
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedNotification = {
|
||||||
|
...inputNotification,
|
||||||
|
_id : expect.any(String),
|
||||||
|
createdAt : expect.any(String),
|
||||||
|
startAt : inputNotification.startAt,
|
||||||
|
stopAt : inputNotification.stopAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(NotificationModel.prototype, 'save')
|
||||||
|
.mockImplementationOnce(function() {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.post('/admin/notification/add')
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.send(inputNotification);
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
expect(response.body).toEqual(savedNotification);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error adding a notification without dismissKey', async () => {
|
||||||
|
const inputNotification = {
|
||||||
|
title : 'Test Notification',
|
||||||
|
text : 'This is a test notification',
|
||||||
|
startAt : new Date().toISOString(),
|
||||||
|
stopAt : new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
//Change 'save' function to just return itself instead of actually interacting with the database
|
||||||
|
jest.spyOn(NotificationModel.prototype, 'save')
|
||||||
|
.mockImplementationOnce(function() {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.post('/admin/notification/add')
|
||||||
|
.set('Authorization', 'Basic ' + Buffer.from('admin:password3').toString('base64'))
|
||||||
|
.send(inputNotification);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({ message: 'Dismiss key is required!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a notification based on its dismiss key', async ()=>{
|
||||||
|
const dismissKey = 'testKey';
|
||||||
|
|
||||||
|
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||||
|
.mockImplementationOnce((key) => {
|
||||||
|
return { exec: jest.fn().mockResolvedValue(key) };
|
||||||
|
});
|
||||||
|
const response = await app
|
||||||
|
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||||
|
|
||||||
|
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle error deleting a notification that doesnt exist', async ()=>{
|
||||||
|
const dismissKey = 'testKey';
|
||||||
|
|
||||||
|
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return { exec: jest.fn().mockResolvedValue() };
|
||||||
|
});
|
||||||
|
const response = await app
|
||||||
|
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||||
|
|
||||||
|
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({ message: 'Notification not found' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
62
server/notifications.model.js
Normal file
62
server/notifications.model.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const NotificationSchema = new mongoose.Schema({
|
||||||
|
dismissKey : { type: String, unique: true, required: true },
|
||||||
|
title : { type: String, default: '' },
|
||||||
|
text : { type: String, default: '' },
|
||||||
|
createdAt : { type: Date, default: Date.now },
|
||||||
|
startAt : { type: Date, default: Date.now },
|
||||||
|
stopAt : { type: Date, default: Date.now },
|
||||||
|
}, { versionKey: false });
|
||||||
|
|
||||||
|
NotificationSchema.statics.addNotification = async function(data) {
|
||||||
|
if(!data.dismissKey) throw { message: 'Dismiss key is required!' };
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
title : '',
|
||||||
|
text : '',
|
||||||
|
startAt : new Date(),
|
||||||
|
stopAt : new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationData = _.defaults(data, defaults);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newNotification = new this(notificationData);
|
||||||
|
const savedNotification = await newNotification.save();
|
||||||
|
return savedNotification;
|
||||||
|
} catch (err) {
|
||||||
|
throw { message: err.message || 'Error saving notification' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationSchema.statics.deleteNotification = async function(dismissKey) {
|
||||||
|
if(!dismissKey) throw { message: 'Dismiss key is required!' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deletedNotification = await this.findOneAndDelete({ dismissKey }).exec();
|
||||||
|
if(!deletedNotification) {
|
||||||
|
throw { message: 'Notification not found' };
|
||||||
|
}
|
||||||
|
return deletedNotification;
|
||||||
|
} catch (err) {
|
||||||
|
throw { message: err.message || 'Error deleting notification' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationSchema.statics.getAll = async function() {
|
||||||
|
try {
|
||||||
|
const notifications = await this.find().exec();
|
||||||
|
return notifications;
|
||||||
|
} catch (err) {
|
||||||
|
throw { message: err.message || 'Error retrieving notifications' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Notification = mongoose.model('Notification', NotificationSchema);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
schema : NotificationSchema,
|
||||||
|
model : Notification,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user