0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-30 11:02:38 +00:00

Merge pull request #4022 from 5e-Cleric/update-admin

Update admin
This commit is contained in:
Víctor Losada Hernández
2025-02-12 23:14:39 +01:00
committed by GitHub
17 changed files with 138 additions and 139 deletions

View File

@@ -1,49 +1,48 @@
require('./admin.less'); import './admin.less';
const React = require('react'); import React, { useEffect, useState } from 'react';
const createClass = require('create-react-class');
const BrewUtils = require('./brewUtils/brewUtils.jsx'); const BrewUtils = require('./brewUtils/brewUtils.jsx');
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
import AuthorUtils from './authorUtils/authorUtils.jsx'; import AuthorUtils from './authorUtils/authorUtils.jsx';
const tabGroups = ['brew', 'notifications', 'authors']; const tabGroups = ['brew', 'notifications', 'authors'];
const Admin = createClass({ const Admin = ()=>{
getDefaultProps : function() { const [currentTab, setCurrentTab] = useState('brew');
return {};
},
getInitialState : function(){ useEffect(()=>{
return ({ setCurrentTab(localStorage.getItem('hbAdminTab'));
currentTab : 'brew' }, []);
});
},
handleClick : function(newTab){ useEffect(()=>{
if(this.state.currentTab === newTab) return; localStorage.setItem('hbAdminTab', currentTab);
this.setState({ }, [currentTab]);
currentTab : newTab
});
},
render : function(){ return (
return <div className='admin'> <div className='admin'>
<header> <header>
<div className='container'> <div className='container'>
<i className='fas fa-rocket' /> <i className='fas fa-rocket' />
homebrewery admin The Homebrewery Admin Page
<a href='/'>back to homepage</a>
</div> </div>
</header> </header>
<main className='container'> <main className='container'>
<nav className='tabs'> <nav className='tabs'>
{tabGroups.map((tab, idx)=>{ return <button className={tab===this.state.currentTab ? 'active' : ''} key={idx} onClick={()=>{ return this.handleClick(tab); }}>{tab.toUpperCase()}</button>; })} {tabGroups.map((tab, idx)=>(
<button
className={tab === currentTab ? 'active' : ''}
key={idx}
onClick={()=>setCurrentTab(tab)}>
{tab.toUpperCase()}
</button>
))}
</nav> </nav>
{this.state.currentTab==='brew' && <BrewUtils />} {currentTab === 'brew' && <BrewUtils />}
{this.state.currentTab==='notifications' && <NotificationUtils />} {currentTab === 'notifications' && <NotificationUtils />}
{this.state.currentTab==='authors' && <AuthorUtils />} {currentTab === 'authors' && <AuthorUtils />}
</main> </main>
</div>; </div>
} );
}); };
module.exports = Admin; module.exports = Admin;

View File

@@ -22,7 +22,7 @@ body {
} }
:where(.admin) { :where(.admin) {
padding-bottom : 50px;
header { header {
padding : 20px 0px; padding : 20px 0px;
margin-bottom : 30px; margin-bottom : 30px;
@@ -30,6 +30,7 @@ body {
color : white; color : white;
background-color : @red; background-color : @red;
i { margin-right : 30px; } i { margin-right : 30px; }
a { float : right; }
} }
hr { margin : 30px 0px; } hr { margin : 30px 0px; }
@@ -48,21 +49,23 @@ body {
} }
dl { dl {
@maxItemWidth : 132px; display : grid;
grid-template-columns : 120px 1fr;
row-gap : 10px;
align-items : center;
justify-items : start;
padding-top : 0.5em;
dt { dt {
float : left; float : left;
width : @maxItemWidth; clear : left;
clear : left; height : fit-content;
text-align : right; font-weight : 900;
text-align : right;
&::after { content : ' : '; } &::after { content : ' : '; }
} }
dd { dd { height : fit-content; }
height : 1em;
padding : 0 0 0.5em 0;
margin-left : @maxItemWidth + 6px;
}
} }
.tabs button { .tabs button {
margin-right : 3px; margin-right : 3px;
margin-left : 3px; margin-left : 3px;
@@ -90,11 +93,45 @@ body {
} }
} }
table {
padding : 10px;
tr {
border-bottom : 1px solid;
&:last-of-type { border : none; }
&:nth-child(even) { background : #DDDDDD; }
}
thead {
background : rgb(193,236,230);
border-bottom : 2px solid;
}
th, td {
padding : 5px 10px;
vertical-align : middle;
text-align : center;
border-right : 1px solid;
&:last-child { border-right : none; }
}
th { font-weight : 900; }
td {
&:first-child {
font-weight : 900;
text-align : left;
}
}
}
.error { .error {
background: rgb(178, 54, 54); float : right;
color:white; padding : 10px;
font-weight: 900; margin-block : 10px;
margin-block:10px; font-weight : 900;
padding:10px; color : white;
background : rgb(178, 54, 54);
} }
} }

View File

@@ -26,7 +26,7 @@ const authorLookup = ()=>{
</>; </>;
return <> return <>
<h2>{`Results - ${results.length}`}</h2> <h2>{`Results - ${results.length} brews` }</h2>
<table className='resultsTable'> <table className='resultsTable'>
<thead> <thead>
<tr> <tr>
@@ -45,10 +45,10 @@ const authorLookup = ()=>{
}) })
.map((brew, idx)=>{ .map((brew, idx)=>{
return <tr key={idx}> return <tr key={idx}>
<td>{brew.title}</td> <td><strong>{brew.title}</strong></td>
<td>{brew.shareId}</td> <td><a href={`/share/${brew.shareId}`}>{brew.shareId}</a></td>
<td>{brew.editId}</td> <td>{brew.editId}</td>
<td>{brew.updatedAt}</td> <td style={{ width: '200px' }}>{brew.updatedAt}</td>
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td> <td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
</tr>; </tr>;
})} })}

View File

@@ -26,23 +26,4 @@
} }
} }
table.resultsTable {
* {
border: 1px solid black;
vertical-align: middle;
padding: 5px;
}
th {
font-weight: bold;
}
th, td {
text-align: center;
&:first-of-type {
text-align: left;
}
}
}
} }

View File

@@ -1,10 +1,8 @@
require('./brewCleanup.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const request = require('superagent'); const request = require('superagent');
const BrewCleanup = createClass({ const BrewCleanup = createClass({
displayName : 'BrewCleanup', displayName : 'BrewCleanup',
getDefaultProps(){ getDefaultProps(){
@@ -39,9 +37,9 @@ const BrewCleanup = createClass({
if(!this.state.primed) return; if(!this.state.primed) return;
if(!this.state.count){ if(!this.state.count){
return <div className='removeBox'>No Matching Brews found.</div>; return <div className='result noBrews'>No Matching Brews found.</div>;
} }
return <div className='removeBox'> return <div className='result'>
<button onClick={this.cleanup} className='remove'> <button onClick={this.cleanup} className='remove'>
{this.state.pending {this.state.pending
? <i className='fas fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
@@ -52,7 +50,7 @@ const BrewCleanup = createClass({
</div>; </div>;
}, },
render(){ render(){
return <div className='BrewCleanup'> return <div className='brewUtil brewCleanup'>
<h2> Brew Cleanup </h2> <h2> Brew Cleanup </h2>
<p>Removes very short brews to tidy up the database</p> <p>Removes very short brews to tidy up the database</p>
@@ -65,7 +63,7 @@ const BrewCleanup = createClass({
{this.renderPrimed()} {this.renderPrimed()}
{this.state.error {this.state.error
&& <div className='error'>{this.state.error.toString()}</div> && <div className='error noBrews'>{this.state.error.toString()}</div>
} }
</div>; </div>;
} }

View File

@@ -1,9 +0,0 @@
.BrewCleanup {
.removeBox {
margin-top : 20px;
button {
margin-right : 10px;
background-color : @red;
}
}
}

View File

@@ -1,10 +1,7 @@
require('./brewCompress.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const request = require('superagent'); const request = require('superagent');
const BrewCompress = createClass({ const BrewCompress = createClass({
displayName : 'BrewCompress', displayName : 'BrewCompress',
getDefaultProps(){ getDefaultProps(){
@@ -53,9 +50,9 @@ const BrewCompress = createClass({
if(!this.state.primed) return; if(!this.state.primed) return;
if(!this.state.count){ if(!this.state.count){
return <div className='removeBox'>No Matching Brews found.</div>; return <div className='result noBrews'>No Matching Brews found.</div>;
} }
return <div className='removeBox'> return <div className='result'>
<button onClick={this.cleanup} className='remove'> <button onClick={this.cleanup} className='remove'>
{this.state.pending {this.state.pending
? <i className='fas fa-spin fa-spinner' /> ? <i className='fas fa-spin fa-spinner' />
@@ -69,7 +66,7 @@ const BrewCompress = createClass({
</div>; </div>;
}, },
render(){ render(){
return <div className='BrewCompress'> return <div className='brewUtil brewCompress'>
<h2> Brew Compression </h2> <h2> Brew Compression </h2>
<p>Compresses the text in brews to binary</p> <p>Compresses the text in brews to binary</p>

View File

@@ -1,9 +0,0 @@
.BrewCompress {
.removeBox {
margin-top : 20px;
button {
margin-right : 10px;
background-color : @red;
}
}
}

View File

@@ -1,5 +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');
@@ -55,7 +53,7 @@ const BrewLookup = createClass({
renderFoundBrew(){ renderFoundBrew(){
const brew = this.state.foundBrew; const brew = this.state.foundBrew;
return <div className='foundBrew'> return <div className='result'>
<dl> <dl>
<dt>Title</dt> <dt>Title</dt>
<dd>{brew.title}</dd> <dd>{brew.title}</dd>
@@ -90,7 +88,7 @@ const BrewLookup = createClass({
}, },
render(){ render(){
return <div className='brewLookup'> return <div className='brewUtil brewLookup'>
<h2>Brew Lookup</h2> <h2>Brew Lookup</h2>
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' /> <input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
<button onClick={this.lookup}> <button onClick={this.lookup}>
@@ -106,7 +104,7 @@ const BrewLookup = createClass({
{this.state.foundBrew {this.state.foundBrew
? this.renderFoundBrew() ? this.renderFoundBrew()
: <div className='noBrew'>No brew found.</div> : <div className='result noBrew'>No brew found.</div>
} }
</div>; </div>;
} }

View File

@@ -1,6 +0,0 @@
.brewLookup {
.cleanButton {
display : inline-block;
width : 100%;
}
}

View File

@@ -1,6 +1,6 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
require('./brewUtils.less');
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
const BrewLookup = require('./brewLookup/brewLookup.jsx'); const BrewLookup = require('./brewLookup/brewLookup.jsx');

View File

@@ -0,0 +1,29 @@
.brewUtil {
.result {
margin-top : 20px;
button {
margin-right : 10px;
background-color : @red;
}
}
.cleanButton {
display : inline-block;
width : 100%;
}
}
.stats {
position : relative;
.pending {
position : absolute;
top : 0.5em;
left : 100px;
width : 100%;
height : 100%;
}
&:has(.pending) { opacity : 0.5; }
dl { grid-template-columns : 200px 250px; }
}

View File

@@ -1,11 +1,8 @@
require('./stats.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 request = require('superagent'); const request = require('superagent');
const Stats = createClass({ const Stats = createClass({
displayName : 'Stats', displayName : 'Stats',
getDefaultProps(){ getDefaultProps(){
@@ -14,7 +11,8 @@ const Stats = createClass({
getInitialState(){ getInitialState(){
return { return {
stats : { stats : {
totalBrews : 0 totalBrews : 0,
totalPublishedBrews : 0
}, },
fetching : false fetching : false
}; };
@@ -29,11 +27,13 @@ const Stats = createClass({
.finally(()=>this.setState({ fetching: false })); .finally(()=>this.setState({ fetching: false }));
}, },
render(){ render(){
return <div className='Stats'> return <div className='brewUtil stats'>
<h2> Stats </h2> <h2> Stats </h2>
<dl> <dl>
<dt>Total Brew Count</dt> <dt>Total Brew Count</dt>
<dd>{this.state.stats.totalBrews}</dd> <dd>{this.state.stats.totalBrews}</dd>
<dt>Total Brews Published</dt>
<dd>{this.state.stats.totalPublishedBrews}</dd>
</dl> </dl>
{this.state.fetching {this.state.fetching

View File

@@ -1,13 +0,0 @@
.Stats {
position : relative;
.pending {
position : absolute;
top : 0px;
left : 0px;
width : 100%;
height : 100%;
background-color : rgba(238,238,238, 0.5);
}
}

View File

@@ -6,18 +6,21 @@
.field { .field {
display : grid; display : grid;
grid-template-columns : 120px 150px; grid-template-columns : 120px 200px;
align-items : center; align-items : center;
justify-items : stretch; justify-items : stretch;
width : 100%; width : 100%;
margin-bottom : 20px; margin-bottom : 20px;
input { input {
height : 33px; height : 33px;
padding : 0px 10px; padding : 0px 10px;
margin-bottom : unset; margin-bottom : unset;
font-family : monospace; font-family : monospace;
&[type="date"] {
width:14ch;
}
} }
textarea { textarea {

View File

@@ -1,8 +1,8 @@
.notificationLookup { .notificationLookup {
width : 450px; width : 450px;
height : fit-content; height : fit-content;
.noNotification { margin-block : 20px; }
.notificationList { .notificationList {
display : flex; display : flex;
flex-direction : column; flex-direction : column;
@@ -30,11 +30,6 @@
font-size : 20px; font-size : 20px;
font-weight : 900; font-weight : 900;
} }
dl dt{
font-weight: 900;
}
} }
} }
.noNotification { margin-block : 20px; }
} }

View File

@@ -147,7 +147,6 @@ router.put('/admin/compress/:id', (req, res)=>{
}); });
}); });
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try { try {
const totalBrewsCount = await HomebrewModel.countDocuments({}); const totalBrewsCount = await HomebrewModel.countDocuments({});