1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-09 08:32:48 +00:00

UI Overhaul

This commit is contained in:
2018-08-19 18:17:40 -05:00
parent 5fedcd4b40
commit e593472c84
58 changed files with 633 additions and 685 deletions

13
.gitignore vendored
View File

@@ -4,11 +4,14 @@ node_modules
yarn-error* yarn-error*
vendor vendor
bot bot
sounds
clips
debug debug
youtube
go-discord-bot go-discord-bot
data.db
.wwp-cache .wwp-cache
data tmp
/sounds
/clips
/youtube
/data
/data.db

View File

@@ -1,3 +1,6 @@
{ {
"editor.formatOnSave": true "editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
} }

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { Navbar } from './components/Navbar';
//styling
import './scss/index.scss';
export class Wrapper extends React.Component<any, any> {
constructor(props: any) {
super(props);
}
render() {
return (
<div>
<Navbar />
<div>{this.props.children}</div>
</div>
);
}
}

View File

@@ -1,31 +1,27 @@
import 'babel-polyfill';
import { Provider } from 'mobx-react';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { Clips, Downloader, NotFound, Oauth, Soundboard, Stats } from './pages';
import { Wrapper } from './Wrapper'; import { rootStoreInstance } from './stores';
import { Home } from './pages/Home/Home'; import { Wrapper } from './wrapper';
import { Soundboard } from './pages/Soundboard/Soundboard';
import { NotFound } from './pages/NotFound/NotFound';
import { Downloader } from './pages/Downloader/Downloader';
import { Clips } from './pages/Clips';
import { Oauth } from './pages/oauth/oauth';
import { Stats } from './pages/stats/stats';
import 'babel-polyfill';
const App: any = (): any => { const App: any = (): any => {
return ( return (
<BrowserRouter> <BrowserRouter>
<Wrapper> <Provider {...rootStoreInstance}>
<Switch> <Wrapper>
<Route exact path="/" component={Home} /> <Switch>
<Route path="/soundboard" component={Soundboard} /> <Route exact path="/" component={Soundboard} />
<Route path="/downloader" component={Downloader} /> <Route path="/downloader" component={Downloader} />
<Route path="/clips" component={Clips} /> <Route path="/clips" component={Clips} />
<Route path="/oauth" component={Oauth} /> <Route path="/oauth" component={Oauth} />
<Route path="/stats" component={Stats} /> <Route path="/stats" component={Stats} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</Wrapper> </Wrapper>
</Provider>
</BrowserRouter> </BrowserRouter>
); );
}; };

View File

@@ -1,90 +0,0 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import jwt_decode from 'jwt-decode';
import { OauthService, StorageService } from '../../services';
import './Navbar.scss';
interface Props {}
interface State {
token: string | null;
email?: string;
oauthUrl?: string;
}
export class Navbar extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
token: null,
};
}
componentDidMount() {
this.loadOauthUrl();
const token = StorageService.getJWT();
if (token) {
const claims: any = jwt_decode(token);
const email = claims['email'];
this.setState({ token, email });
}
}
async loadOauthUrl() {
try {
const oauthUrl = await OauthService.getOauthUrl();
this.setState({ oauthUrl });
} catch (e) {
console.error(e);
}
}
private logout = () => {
StorageService.clear();
window.location.href = '/';
};
renderLoginButton() {
if (!this.state.oauthUrl) {
return null;
}
return !this.state.token ? (
<a href={this.state.oauthUrl} className="Navbar__item">
Login
</a>
) : (
<a className="Navbar__item" onClick={this.logout}>
Logout
</a>
);
}
render() {
return (
<div className="Navbar">
<div className="Navbar__header">Sound Bot</div>
<NavLink exact to="/" className="Navbar__item" activeClassName="Navbar__item--active">
Home
</NavLink>
<NavLink to="/soundboard" className="Navbar__item" activeClassName="Navbar__item--active">
Soundboard
</NavLink>
<NavLink to="/downloader" className="Navbar__item" activeClassName="Navbar__item--active">
Youtube Downloader
</NavLink>
<NavLink to="/clips" className="Navbar__item" activeClassName="Navbar__item--active">
Clips
</NavLink>
<NavLink to="/stats" className="Navbar__item" activeClassName="Navbar__item--active">
Stats
</NavLink>
{this.renderLoginButton()}
{this.state.email && <div className="Navbar__email">{this.state.email}</div>}
</div>
);
}
}

View File

@@ -1 +0,0 @@
export * from './Navbar';

View File

@@ -1 +0,0 @@
export * from './SoundList';

View File

@@ -0,0 +1,37 @@
@import '../../scss/variables';
.header {
position: fixed;
top: 0;
left: 0;
background: linear-gradient(to right, $primaryBlue, darken($primaryBlue, 20%));
height: 50px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 20px;
&__title-container {
display: flex;
align-items: center;
}
&__nav-button {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
margin-right: 10px;
background: $primaryBlue;
border: none;
color: $white;
border-right: 1px solid darken($primaryBlue, 2%);
cursor: pointer;
outline: none;
&:hover {
background: darken($primaryBlue, 5%);
}
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import './header.scss';
interface IProps {
onButtonClick: () => void;
}
export class Header extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<div className="header">
<div className="header__title-container">
<button className="header__nav-button" onClick={this.props.onButtonClick}>
<i className="fa fa-lg fa-bars" />
</button>
<h2 style={{ margin: 0 }}>Sound Bot</h2>
</div>
<a href="https://github.com/mgerb/go-discord-bot" className="fa fa-lg fa-github" target="_blank" />
</div>
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './header/header';
export * from './navbar/navbar';
export * from './sound-list/sound-list';
export * from './uploader/uploader';

View File

@@ -1,31 +1,25 @@
@import '../../scss/variables'; @import '../../scss/variables';
.Navbar { .navbar {
position: fixed; position: fixed;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
top: 0; left: -$navbarWidth;
left: 0; top: 50px;
height: 100%; height: calc(100% - 50px);
width: $navbarWidth; width: $navbarWidth;
background-color: $gray2; background-color: $gray2;
border-right: 1px solid darken($gray2, 2%); border-right: 1px solid darken($gray2, 2%);
overflow-y: auto; overflow-y: auto;
padding-bottom: 10px; padding-bottom: 10px;
transition: 0.2s left ease-in-out;
&--open {
left: 0;
}
} }
.Navbar__header { .navbar__item {
font-size: 25px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background-color: $primaryBlue;
border-bottom: 1px solid $gray3;
}
.Navbar__item {
min-height: 50px; min-height: 50px;
text-decoration: none; text-decoration: none;
display: flex; display: flex;
@@ -39,18 +33,18 @@
background-color: $gray1; background-color: $gray1;
} }
& + .Navbar__item { & + & {
border-top: 1px solid $gray3; border-top: 1px solid $gray3;
} }
} }
.Navbar__item--active { .navbar__item--active {
padding-left: 4px; padding-left: 4px;
border-right: 4px solid $primaryBlue; border-right: 4px solid $primaryBlue;
color: $primaryBlue !important; color: $primaryBlue !important;
} }
.Navbar__email { .navbar__email {
padding-top: 10px; padding-top: 10px;
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { IClaims } from '../../model';
import { OauthService, StorageService } from '../../services';
import './navbar.scss';
interface Props {
claims?: IClaims;
open: boolean;
onNavClick: () => void;
}
interface State {
oauthUrl?: string;
}
export class Navbar extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
}
componentDidMount() {
this.loadOauthUrl();
}
async loadOauthUrl() {
const oauthUrl = await OauthService.getOauthUrl();
if (oauthUrl) {
this.setState({ oauthUrl });
}
}
private logout = () => {
StorageService.clear();
window.location.href = '/';
};
renderLoginButton() {
const { claims } = this.props;
if (!this.state.oauthUrl) {
return null;
}
return !claims ? (
<a href={this.state.oauthUrl} className="navbar__item">
Login
</a>
) : (
<a className="navbar__item" onClick={this.logout}>
Logout
</a>
);
}
renderNavLink = (title: string, to: string, params?: any) => {
return (
<NavLink
{...params}
to={to}
className="navbar__item"
activeClassName="navbar__item--active"
onClick={this.props.onNavClick}
>
{title}
</NavLink>
);
};
render() {
const { claims, open } = this.props;
const openClass = open ? 'navbar--open' : '';
return (
<div className={'navbar ' + openClass}>
{this.renderNavLink('Soundboard', '/', { exact: true })}
{this.renderNavLink('Youtube Downloader', '/downloader')}
{this.renderNavLink('Clips', '/clips')}
{this.renderNavLink('Stats', '/stats')}
{this.renderLoginButton()}
{claims && claims.email && <div className="navbar__email">{claims.email}</div>}
</div>
);
}
}

View File

@@ -1,12 +1,12 @@
@import '../../scss/variables'; @import '../../scss/variables';
.SoundList__item { .sound-list__item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 50px; height: 50px;
& + .SoundList__item { & + .sound-list__item {
border-top: 1px solid $gray3; border-top: 1px solid $gray3;
} }
} }

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import './sound-list.scss';
import './SoundList.scss';
interface Props { interface Props {
soundList: SoundType[]; soundList: SoundType[];
@@ -64,8 +63,8 @@ export class SoundList extends React.Component<Props, State> {
{soundList.length > 0 {soundList.length > 0
? soundList.map((sound: SoundType, index: number) => { ? soundList.map((sound: SoundType, index: number) => {
return ( return (
<div key={index} className="SoundList__item"> <div key={index} className="sound-list__item">
<div>{(sound.prefix || '') + sound.name}</div> <div className="text-wrap">{(sound.prefix || '') + sound.name}</div>
{this.checkExtension(sound.extension) && this.state.showAudioControls[index] ? ( {this.checkExtension(sound.extension) && this.state.showAudioControls[index] ? (
<audio <audio

View File

@@ -1,40 +1,22 @@
@import '../../scss/variables'; @import '../../scss/variables';
.Soundboard { .dropzone {
display: flex;
padding: 10px;
}
.Soundboard__column {
flex: 1;
}
.Soundboard__input {
display: block;
width: 200px;
margin-bottom: 10px;
margin-right: auto;
margin-left: auto;
}
.Dropzone {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 2px solid $primaryBlue; border: 2px solid $primaryBlue;
border-radius: 1em; border-radius: 1em;
max-width: 800px;
margin-bottom: 20px;
padding: 20px; padding: 20px;
margin-right: auto;
margin-left: auto;
color: $lightGray; color: $lightGray;
width: 400px;
height: 400px;
background-color: $gray2; background-color: $gray2;
transition: box-shadow 0.1s linear, background-color 0.1s linear; transition: box-shadow 0.1s linear, background-color 0.1s linear;
cursor: pointer;
} }
.Dropzone--active { .dropzone--active {
background-color: $gray3; background-color: $gray3;
box-shadow: 0px 0px 5px 1px $primaryBlue; box-shadow: 0px 0px 5px 1px $primaryBlue;
} }

View File

@@ -0,0 +1,86 @@
import React from 'react';
import Dropzone from 'react-dropzone';
import { axios } from '../../services';
import './uploader.scss';
interface IProps {
onComplete: () => void;
}
interface IState {
percentCompleted: number;
uploaded: boolean;
uploadError: string;
}
export class Uploader extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
percentCompleted: 0,
uploaded: false,
uploadError: ' ',
};
}
private config = {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent: any) => {
this.setState({
percentCompleted: Math.round((progressEvent.loaded * 100) / progressEvent.total),
});
},
};
onDrop = (acceptedFiles: any) => {
if (acceptedFiles.length > 0) {
this.uploadFile(acceptedFiles[0]);
}
};
uploadFile(file: any) {
let formData = new FormData();
formData.append('name', file.name);
formData.append('file', file);
axios
.post('/api/upload', formData, this.config)
.then(() => {
this.setState({
percentCompleted: 0,
uploaded: true,
uploadError: ' ',
});
this.props.onComplete();
})
.catch(err => {
this.setState({
percentCompleted: 0,
uploaded: false,
uploadError: err.response.data,
});
});
}
render() {
return (
<Dropzone
className="dropzone"
activeClassName="dropzone--active"
onDrop={this.onDrop}
multiple={false}
disableClick={false}
maxSize={10000000000}
accept={'audio/*'}
>
<div style={{ fontSize: '20px' }}>Click or drop file to upload</div>
{this.state.percentCompleted > 0 ? <div>Uploading: {this.state.percentCompleted}</div> : ''}
{this.state.uploaded ? <div style={{ color: 'green' }}>File uploded!</div> : ''}
<div style={{ color: '#f95f59' }}>{this.state.uploadError}</div>
</Dropzone>
);
}
}

View File

@@ -0,0 +1,12 @@
import { Permissions } from './permissions';
// JWT claims
export interface IClaims {
id: string;
username: string;
email: string;
discriminator: string;
permissions: Permissions;
exp: number;
iss: string; // issuer
}

View File

@@ -0,0 +1,2 @@
export * from './claims';
export * from './permissions';

View File

@@ -0,0 +1,5 @@
export enum Permissions {
'Admin' = 3,
'Mod' = 2,
'User' = 1,
}

View File

@@ -1 +0,0 @@
export * from './Clips';

View File

@@ -1,48 +0,0 @@
import React from 'react';
import './Home.scss';
interface Props {}
interface State {}
export class Home extends React.Component<Props, State> {
render() {
return (
<div className="content">
<div className="card">
<div className="card__header">Go Discord Bot</div>
<h3>04-09-18 Update</h3>
<ul>
<li>pubg stats no longer updated on this site</li>
<li>client dependencies all updated (including webpack 4 and react router 4)</li>
</ul>
<h3>Audio Clipping</h3>
<p>
<em>NEW:</em> Audio clipping now supported! Try it out with the <code>clip</code> command!
</p>
<h3>PUBG Stats</h3>
<p>PUBG stats are pulled from the score API.</p>
<h3>Youtube Downloader</h3>
<p>Convert Youtube URL's to MP3 files.</p>
<h3>Soundboard Upload</h3>
<p>Drag and drop files to upload. Sounds can be played in discord by typing the commands on the next page.</p>
<p>
Check out the source code on
<a href="https://github.com/mgerb/GoBot" target="_blank">
{' '}
GitHub
<i className="fa fa-github" aria-hidden="true" />
</a>
</p>
</div>
</div>
);
}
}

View File

@@ -1,12 +0,0 @@
import React from 'react';
import './NotFound.scss';
interface Props {}
interface State {}
export class NotFound extends React.Component<Props, State> {
render() {
return <div className="NotFound">404 Not Found</div>;
}
}

View File

@@ -1,33 +0,0 @@
@import '../../scss/variables';
.pubg__container {
padding: 10px;
}
.pubg__table {
border-collapse: collapse;
width: 100%;
text-align: left;
margin-top: 20px;
tr + tr {
border-top: 1px solid $gray3;
}
td,
th {
padding: 5px;
}
}
.pubg__button-row {
margin-bottom: 10px;
.button {
min-width: 100px;
& + .button {
margin-left: 5px;
}
}
}

View File

@@ -1,159 +0,0 @@
/**
* DEPRECATED
*/
import React from 'react';
import { axios } from '../../services';
import * as _ from 'lodash';
import './Pubg.scss';
interface Props {}
interface State {
players: Player[];
selectedRegion: string;
selectedMatch: string;
statList: string[];
}
interface Player {
PlayerName: string;
agg?: any;
as?: any;
na?: any;
sa?: any;
}
export class Pubg extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
players: [],
selectedRegion: 'agg',
selectedMatch: 'squad',
statList: [],
};
}
componentDidMount() {
axios.get('/api/stats/pubg').then(res => {
this.setState({
players: _.map(res.data) as any,
});
this.setStatList();
});
}
// get stat list
setStatList() {
// hacky way to find existing content -- to tired to make it pretty
let i = 0;
let stats;
while (!stats) {
if (i > this.state.players.length) {
return;
}
stats = _.find(
_.get(this.state, `players[${i}].Stats`),
(s: any) => s.Match === this.state.selectedMatch.toLowerCase(),
);
i++;
}
if (stats) {
this.setState({
statList: _.sortBy(_.map(stats.Stats, 'field')) as any,
});
}
}
insertRows(): any {
return this.state.statList.map((val: any, index: any) => {
return (
<tr key={index}>
<td>{val}</td>
{this.state.players.map((player: any, i: number) => {
// find player stats for field
let playerStat = _.find(player.Stats, (p: any) => {
return (
p.Match === this.state.selectedMatch.toLowerCase() &&
p.Region === this.state.selectedRegion.toLowerCase()
);
});
return (
<td key={i}>{_.get(_.find(_.get(playerStat, 'Stats'), (p: any) => p.field === val), 'displayValue')}</td>
);
})}
</tr>
);
});
}
buttonRegion(title: string) {
let lowerTitle = title === 'All' ? 'agg' : title.toLowerCase();
return (
<button
className={`button ${lowerTitle === this.state.selectedRegion ? 'button--primary' : ''}`}
onClick={() => {
this.setState({ selectedRegion: lowerTitle });
this.setStatList();
}}
>
{title}
</button>
);
}
buttonMatch(title: string) {
let lowerTitle = title.toLowerCase();
return (
<button
className={`button ${lowerTitle === this.state.selectedMatch ? 'button--primary' : ''}`}
onClick={() => {
this.setState({ selectedMatch: lowerTitle });
this.setStatList();
}}
>
{title}
</button>
);
}
render() {
return (
<div className="pubg__container">
<div className="card" style={{ maxWidth: 'initial' }}>
<div className="card__header">PUBG Stats</div>
<div className="pubg__button-row">
{this.buttonMatch('Solo')}
{this.buttonMatch('Duo')}
{this.buttonMatch('Squad')}
</div>
<div className="pubg__button-row">
{this.buttonRegion('All')}
{this.buttonRegion('Na')}
{this.buttonRegion('As')}
{this.buttonRegion('Au')}
</div>
<table className="pubg__table">
<tbody>
<tr>
<th />
{this.state.players.map((val: any, index: number) => {
return <th key={index}>{val.PlayerName}</th>;
})}
</tr>
{this.insertRows()}
</tbody>
</table>
</div>
</div>
);
}
}

View File

@@ -1,130 +0,0 @@
import React from 'react';
import Dropzone from 'react-dropzone';
import { axios } from '../../services';
import { SoundList, SoundType } from '../../components/SoundList';
import './Soundboard.scss';
import { AxiosRequestConfig } from 'axios';
let self: any;
interface Props {}
interface State {
percentCompleted: number;
uploaded: boolean;
uploadError: string;
soundList: SoundType[];
}
export class Soundboard extends React.Component<Props, State> {
private config: AxiosRequestConfig;
private soundListCache: any;
constructor(props: Props) {
super(props);
(this.state = {
percentCompleted: 0,
uploaded: false,
uploadError: ' ',
soundList: [],
}),
(self = this);
}
componentDidMount() {
this.config = {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: progressEvent => {
this.setState({
percentCompleted: Math.round(progressEvent.loaded * 100 / progressEvent.total),
});
},
};
this.getSoundList();
}
private getSoundList() {
if (!this.soundListCache) {
axios
.get('/api/soundlist')
.then(response => {
this.soundListCache = response.data;
this.setState({
soundList: response.data,
});
})
.catch((error: any) => {
console.error(error.response.data);
});
} else {
this.setState({
soundList: this.soundListCache,
});
}
}
onDrop(acceptedFiles: any) {
if (acceptedFiles.length > 0) {
self.uploadFile(acceptedFiles[0]);
}
}
uploadFile(file: any) {
let formData = new FormData();
formData.append('name', file.name);
formData.append('file', file);
axios
.post('/api/upload', formData, this.config)
.then(() => {
this.setState({
percentCompleted: 0,
uploaded: true,
uploadError: ' ',
});
this.soundListCache = undefined;
this.getSoundList();
})
.catch(err => {
this.setState({
percentCompleted: 0,
uploaded: false,
uploadError: err.response.data,
});
});
}
render() {
const { soundList } = this.state;
return (
<div className="Soundboard">
<div className="column">
<SoundList soundList={soundList} type="Sounds" />
</div>
<div className="column">
<div>
<Dropzone
className="Dropzone"
activeClassName="Dropzone--active"
onDrop={this.onDrop}
multiple={false}
disableClick={true}
maxSize={10000000000}
accept={'audio/*'}
>
<div style={{ fontSize: '20px' }}>Drop file here to upload.</div>
{this.state.percentCompleted > 0 ? <div>Uploading: {this.state.percentCompleted}</div> : ''}
{this.state.uploaded ? <div style={{ color: 'green' }}>File uploded!</div> : ''}
<div style={{ color: '#f95f59' }}>{this.state.uploadError}</div>
</Dropzone>
</div>
</div>
</div>
);
}
}

View File

@@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { SoundList, SoundType } from '../../components';
import { axios } from '../../services'; import { axios } from '../../services';
import { SoundList, SoundType } from '../../components/SoundList';
interface Props {} interface Props {}
interface State { interface State {
@@ -36,7 +35,7 @@ export class Clips extends React.Component<Props, State> {
render() { render() {
return ( return (
<div className="Soundboard"> <div className="content">
<div className="column"> <div className="column">
<SoundList soundList={this.state.clipList} type="Clips" /> <SoundList soundList={this.state.clipList} type="Clips" />
</div> </div>

View File

@@ -1,14 +1,10 @@
.Downloader { .downloader__input {
padding: 10px;
}
.Downloader__input {
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.Downloader__button { .downloader__button {
& + & { & + & {
margin-left: 10px; margin-left: 10px;
} }

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { axios } from '../../services'; import { axios } from '../../services';
import './downloader.scss';
import './Downloader.scss';
interface Props {} interface Props {}
@@ -70,13 +69,13 @@ export class Downloader extends React.Component<Props, State> {
render() { render() {
return ( return (
<div className="Downloader"> <div className="content">
<div className="card"> <div className="card">
<div className="card__header">Youtube to MP3</div> <div className="card__header">Youtube to MP3</div>
<input <input
placeholder="Enter Youtube URL" placeholder="Enter Youtube URL"
className="input Downloader__input" className="input downloader__input"
value={this.state.url} value={this.state.url}
onChange={event => this.setState({ url: event.target.value })} onChange={event => this.setState({ url: event.target.value })}
/> />

View File

@@ -0,0 +1,6 @@
export * from './clips/clips';
export * from './downloader/downloader';
export * from './not-found/not-found';
export * from './oauth/oauth';
export * from './soundboard/soundboard';
export * from './stats/stats';

View File

@@ -1,8 +1,7 @@
.NotFound { .not-found {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 20px; font-size: 20px;
} }

View File

@@ -0,0 +1,4 @@
import React from 'react';
import './not-found.scss';
export const NotFound = () => <div className="not-found">404 Not Found</div>;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { axios, StorageService } from '../../services';
import queryString from 'query-string'; import queryString from 'query-string';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom';
import { axios, StorageService } from '../../services';
interface Props extends RouteComponentProps<any> {} interface Props extends RouteComponentProps<any> {}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { SoundList, SoundType, Uploader } from '../../components';
import { axios } from '../../services';
import './soundboard.scss';
interface Props {}
interface State {
percentCompleted: number;
uploaded: boolean;
uploadError: string;
soundList: SoundType[];
}
export class Soundboard extends React.Component<Props, State> {
private soundListCache: any;
constructor(props: Props) {
super(props);
this.state = {
percentCompleted: 0,
uploaded: false,
uploadError: ' ',
soundList: [],
};
}
componentDidMount() {
this.getSoundList();
}
private getSoundList() {
if (!this.soundListCache) {
axios
.get('/api/soundlist')
.then(response => {
this.soundListCache = response.data;
this.setState({
soundList: response.data,
});
})
.catch((error: any) => {
console.error(error.response.data);
});
} else {
this.setState({
soundList: this.soundListCache,
});
}
}
onUploadComplete = () => {
delete this.soundListCache;
this.getSoundList();
};
render() {
const { soundList } = this.state;
return (
<div className="content">
<Uploader onComplete={this.onUploadComplete} />
<SoundList soundList={soundList} type="Sounds" />
</div>
);
}
}

View File

@@ -1,3 +0,0 @@
.Stats {
padding: 10px;
}

View File

@@ -0,0 +1,20 @@
.button {
border: none;
border-radius: 3px;
color: $white;
background: $lightGray;
padding: 10px;
cursor: pointer;
&:hover {
background: lighten($lightGray, 5%);
}
}
.button--primary {
background: $primaryBlue;
&:hover {
background: lighten($primaryBlue, 2%);
}
}

20
client/app/scss/card.scss Normal file
View File

@@ -0,0 +1,20 @@
.card {
background-color: $gray2;
border-radius: 5px;
max-width: 800px;
padding: 10px;
margin-bottom: 20px;
border: 1px solid $gray3;
}
.card__header {
margin: -10px -10px 10px -10px;
display: flex;
align-items: center;
padding-left: 10px;
padding-right: 10px;
height: 50px;
border-radius: 5px 5px 0 0;
font-size: 25px;
background-color: $gray3;
}

View File

@@ -1,4 +1,5 @@
@import '~normalize.css/normalize.css'; @import '~normalize.css/normalize.css';
@import '~font-awesome/css/font-awesome.css'; @import '~font-awesome/css/font-awesome.css';
@import './variables.scss';
@import './style.scss'; @import './style.scss';
@import './button.scss';
@import './card.scss';

View File

@@ -0,0 +1,5 @@
@mixin tinyScreen {
@media only screen and (max-width: 520px) {
@content;
}
}

View File

@@ -1,3 +1,6 @@
@import './variables.scss';
@import './mixins.scss';
html { html {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
@@ -26,11 +29,18 @@ i {
body { body {
background-color: $gray1; background-color: $gray1;
color: $white; color: $white;
padding-left: $navbarWidth; // padding-left: $navbarWidth;
}
.flex {
display: flex;
} }
.content { .content {
padding: 10px; padding: 20px;
@include tinyScreen {
padding: 10px;
}
} }
.input { .input {
@@ -43,48 +53,6 @@ body {
color: $white; color: $white;
} }
.button {
border: none;
border-radius: 3px;
color: $white;
background: $lightGray;
padding: 10px;
cursor: pointer;
&:hover {
background: lighten($lightGray, 5%);
}
}
.button--primary {
background: $primaryBlue;
&:hover {
background: lighten($primaryBlue, 2%);
}
}
.card {
background-color: $gray2;
border-radius: 5px;
max-width: 800px;
padding: 10px;
margin-bottom: 20px;
border: 1px solid $gray3;
}
.card__header {
margin: -10px -10px 10px -10px;
display: flex;
align-items: center;
padding-left: 10px;
padding-right: 10px;
height: 50px;
border-radius: 5px 5px 0 0;
font-size: 25px;
background-color: $gray3;
}
.column { .column {
flex: 1; flex: 1;
@@ -92,3 +60,8 @@ body {
margin-left: 10px; margin-left: 10px;
} }
} }
.text-wrap {
word-break: break-word;
word-wrap: break-word;
}

View File

@@ -1,14 +1,9 @@
import ax from 'axios'; import ax from 'axios';
import { StorageService } from './storage.service';
export const axios = ax.create(); export const axios = ax.create();
axios.interceptors.request.use( axios.interceptors.request.use(
config => { config => {
const jwt = StorageService.getJWT();
if (jwt) {
config.headers['Authorization'] = `Bearer ${jwt}`;
}
// Do something before request is sent // Do something before request is sent
return config; return config;
}, },

View File

@@ -0,0 +1,43 @@
import jwt_decode from 'jwt-decode';
import { action, observable } from 'mobx';
import { IClaims } from '../model';
import { axios, StorageService } from '../services';
import { Util } from '../util';
export class AppStore {
@observable
public navbarOpen: boolean = false;
@observable
public jwt?: string;
@observable
public claims?: IClaims;
constructor() {
const jwt = StorageService.getJWT();
this.setJWT(jwt as string);
this.initNavbar();
}
private initNavbar() {
if (!Util.isMobileScreen()) {
this.navbarOpen = true;
}
}
private setJWT(jwt?: string) {
if (!jwt) {
return;
}
axios.defaults.headers['Authorization'] = `Bearer ${jwt}`;
this.jwt = jwt;
const claims = jwt_decode(jwt);
if (claims) {
this.claims = claims as IClaims;
}
}
@action
public toggleNavbar = () => {
this.navbarOpen = !this.navbarOpen;
};
}

View File

@@ -0,0 +1,2 @@
export * from './app.store';
export * from './root.store';

View File

@@ -0,0 +1,7 @@
import { AppStore } from './app.store';
export class RootStore {
public appStore = new AppStore();
}
export const rootStoreInstance = new RootStore();

17
client/app/util.ts Normal file
View File

@@ -0,0 +1,17 @@
const getScreenWidth = () => {
const w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0];
return w.innerWidth || e.clientWidth || g.clientWidth;
};
const isMobileScreen = () => {
return getScreenWidth() < 520;
};
export const Util = {
getScreenWidth,
isMobileScreen,
};

16
client/app/wrapper.scss Normal file
View File

@@ -0,0 +1,16 @@
@import './scss/variables';
@import './scss/mixins';
.wrapper {
padding-top: 50px;
transition: 0.2s padding-left ease-in-out;
padding-left: 0px;
&--open {
padding-left: $navbarWidth;
}
@include tinyScreen {
padding-left: 0px;
}
}

29
client/app/wrapper.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { inject, observer } from 'mobx-react';
import React from 'react';
import { withRouter } from 'react-router';
import { Header, Navbar } from './components';
//styling
import './scss/index.scss';
import { Util } from './util';
import './wrapper.scss';
export const Wrapper = inject('appStore')(
withRouter(
observer(({ appStore, children }) => {
const openClass = appStore.navbarOpen ? 'wrapper--open' : '';
const onNavClick = () => {
if (Util.isMobileScreen()) {
appStore.toggleNavbar();
}
};
return (
<div>
<Header onButtonClick={appStore.toggleNavbar} />
<Navbar claims={appStore.claims} open={appStore.navbarOpen} onNavClick={onNavClick} />
<div className={'wrapper ' + openClass}>{children}</div>
</div>
);
}),
),
);

View File

@@ -1,6 +1,7 @@
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<meta http-equiv="expires" content="0"> <meta http-equiv="expires" content="0">
</head> </head>

View File

@@ -1,6 +1,6 @@
{ {
"name": "go-discord-bot", "name": "go-discord-bot",
"version": "0.5.2", "version": "0.6.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -5974,6 +5974,20 @@
"minimist": "0.0.8" "minimist": "0.0.8"
} }
}, },
"mobx": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.0.3.tgz",
"integrity": "sha1-U7l/Kg+bDdd3TJYkn4G/LVE9jhw="
},
"mobx-react": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-5.2.5.tgz",
"integrity": "sha512-vSwsjGwmaqTmaEsPWET/APccjirTiIIchQA3YVasKzaxIGv62BNJUHFjrkIAbGBLeqJma+ZgSu158OqQLK0vaQ==",
"requires": {
"hoist-non-react-statics": "2.5.5",
"react-lifecycles-compat": "3.0.4"
}
},
"moment": { "moment": {
"version": "2.22.0", "version": "2.22.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz",
@@ -8166,6 +8180,11 @@
"prop-types": "15.6.2" "prop-types": "15.6.2"
} }
}, },
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-router": { "react-router": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "go-discord-bot", "name": "go-discord-bot",
"version": "0.5.2", "version": "0.6.0",
"description": "Client for go-discord-bot", "description": "Client for go-discord-bot",
"scripts": { "scripts": {
"build": "NODE_ENV=prod webpack -p --progress --colors", "build": "NODE_ENV=prod webpack -p --progress --colors",
@@ -39,6 +39,8 @@
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mobx": "^5.0.3",
"mobx-react": "^5.2.5",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"normalize.css": "^8.0.0", "normalize.css": "^8.0.0",
"postcss-loader": "^2.1.5", "postcss-loader": "^2.1.5",

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"target": "es2015", "target": "es2015",
"module": "es2015", "module": "es2015",
"moduleResolution": "node", "moduleResolution": "node",
@@ -15,24 +16,10 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"alwaysStrict": true, "alwaysStrict": true,
"typeRoots": [ "typeRoots": ["./node_modules/@types", "./@types"]
"./node_modules/@types",
"./@types"
]
}, },
"filesGlob": [ "filesGlob": ["typings/index.d.ts", "src/**/*.ts", "src/**/*.tsx"],
"typings/index.d.ts", "include": ["app"],
"src/**/*.ts", "exclude": ["android", "ios", "build", "node_modules"],
"src/**/*.tsx"
],
"include": [
"app"
],
"exclude": [
"android",
"ios",
"build",
"node_modules"
],
"compileOnSave": false "compileOnSave": false
} }

View File

@@ -1,4 +1,4 @@
version=$(git describe --tags) version=$(git describe --tags)
docker build -t mgerb/go-discord-bot:$version . docker build -t mgerb/go-discord-bot:latest .
docker tag mgerb/go-discord-bot:$version mgerb/go-discord-bot:latest docker tag mgerb/go-discord-bot:latest mgerb/go-discord-bot:$version

View File

@@ -3,6 +3,7 @@ version: "2"
services: services:
go-discord-bot: go-discord-bot:
image: mgerb/go-discord-bot:latest image: mgerb/go-discord-bot:latest
restart: unless-stopped
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:

1
fresh.conf Normal file
View File

@@ -0,0 +1 @@
root: ./server

View File

@@ -2,16 +2,16 @@
A soundboard bot for discord with a Go back end and React front end. A soundboard bot for discord with a Go back end and React front end.
![Image](https://i.imgur.com/BCoLAuK.png) ![Image](./screenshots/sound-bot.png)
## How to use ## How to use
* [Download latest release here](https://github.com/mgerb/go-discord-bot/releases) - [Download latest release here](https://github.com/mgerb/go-discord-bot/releases)
* Install [youtube-dl](https://github.com/rg3/youtube-dl/blob/master/README.md#installation) - Install [youtube-dl](https://github.com/rg3/youtube-dl/blob/master/README.md#installation)
* Install [ffmpeg](https://www.ffmpeg.org/download.html) - Install [ffmpeg](https://www.ffmpeg.org/download.html)
* edit your config.json file - edit your config.json file
* run the executable - run the executable
* visit http://localhost:8080 - visit http://localhost:8080
### With docker-compose ### With docker-compose
@@ -26,6 +26,7 @@ version: "2"
services: services:
go-discord-bot: go-discord-bot:
image: mgerb/go-discord-bot:latest image: mgerb/go-discord-bot:latest
restart: unless-stopped
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
@@ -34,10 +35,10 @@ services:
### Commands ### Commands
* `clip` - clips the past minute of audio (currently bugged if more than one user is speaking) - `clip` - clips the past minute of audio (currently bugged if more than one user is speaking)
* `summon` - summons the bot to your current channel - `summon` - summons the bot to your current channel
* `dismiss` - dismisses the bot from the server - `dismiss` - dismisses the bot from the server
* `<audio clip>` - play a named audio clip - `<audio clip>` - play a named audio clip
### Uploading files ### Uploading files
@@ -78,22 +79,22 @@ Check it out in the "Stats" page on the site.
### Dependencies ### Dependencies
* Go - Go
* node/npm - node/npm
* make - make
### Compiling ### Compiling
* Make sure dependencies are installed - Make sure dependencies are installed
* install packr - `go get -u github.com/gobuffalo/packr/...` - install packr - `go get -u github.com/gobuffalo/packr/...`
* Rename the `config.template.json` to `config.json` - Rename the `config.template.json` to `config.json`
* add configurations to `config.json` - add configurations to `config.json`
* `cd client && npm run dev` - `cd client && npm run dev`
* `go run main.go` - `go run main.go`
* open a browser `localhost:<config_port>` - open a browser `localhost:<config_port>`
[Packr](https://github.com/gobuffalo/packr) is used to bundle the static web assets into the binary. [Packr](https://github.com/gobuffalo/packr) is used to bundle the static web assets into the binary.
Use these commands to compile the project. The client must be built first. Use these commands to compile the project. The client must be built first.
* `packr build` - `packr build`
* `packr install` - `packr install`

1
run_client.sh Executable file
View File

@@ -0,0 +1 @@
cd client && npm run dev

1
run_server.sh Executable file
View File

@@ -0,0 +1 @@
fresh -c fresh.conf

BIN
screenshots/sound-bot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB