1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-08 08:02:49 +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*
vendor
bot
sounds
clips
debug
youtube
go-discord-bot
data.db
.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 ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { Wrapper } from './Wrapper';
import { Home } from './pages/Home/Home';
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';
import { Clips, Downloader, NotFound, Oauth, Soundboard, Stats } from './pages';
import { rootStoreInstance } from './stores';
import { Wrapper } from './wrapper';
const App: any = (): any => {
return (
<BrowserRouter>
<Wrapper>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/soundboard" component={Soundboard} />
<Route path="/downloader" component={Downloader} />
<Route path="/clips" component={Clips} />
<Route path="/oauth" component={Oauth} />
<Route path="/stats" component={Stats} />
<Route component={NotFound} />
</Switch>
</Wrapper>
<Provider {...rootStoreInstance}>
<Wrapper>
<Switch>
<Route exact path="/" component={Soundboard} />
<Route path="/downloader" component={Downloader} />
<Route path="/clips" component={Clips} />
<Route path="/oauth" component={Oauth} />
<Route path="/stats" component={Stats} />
<Route component={NotFound} />
</Switch>
</Wrapper>
</Provider>
</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';
.Navbar {
.navbar {
position: fixed;
display: flex;
flex-direction: column;
top: 0;
left: 0;
height: 100%;
left: -$navbarWidth;
top: 50px;
height: calc(100% - 50px);
width: $navbarWidth;
background-color: $gray2;
border-right: 1px solid darken($gray2, 2%);
overflow-y: auto;
padding-bottom: 10px;
transition: 0.2s left ease-in-out;
&--open {
left: 0;
}
}
.Navbar__header {
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 {
.navbar__item {
min-height: 50px;
text-decoration: none;
display: flex;
@@ -39,18 +33,18 @@
background-color: $gray1;
}
& + .Navbar__item {
& + & {
border-top: 1px solid $gray3;
}
}
.Navbar__item--active {
.navbar__item--active {
padding-left: 4px;
border-right: 4px solid $primaryBlue;
color: $primaryBlue !important;
}
.Navbar__email {
.navbar__email {
padding-top: 10px;
flex: 1;
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';
.SoundList__item {
.sound-list__item {
display: flex;
justify-content: space-between;
align-items: center;
height: 50px;
& + .SoundList__item {
& + .sound-list__item {
border-top: 1px solid $gray3;
}
}

View File

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

View File

@@ -1,40 +1,22 @@
@import '../../scss/variables';
.Soundboard {
display: flex;
padding: 10px;
}
.Soundboard__column {
flex: 1;
}
.Soundboard__input {
display: block;
width: 200px;
margin-bottom: 10px;
margin-right: auto;
margin-left: auto;
}
.Dropzone {
.dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px solid $primaryBlue;
border-radius: 1em;
max-width: 800px;
margin-bottom: 20px;
padding: 20px;
margin-right: auto;
margin-left: auto;
color: $lightGray;
width: 400px;
height: 400px;
background-color: $gray2;
transition: box-shadow 0.1s linear, background-color 0.1s linear;
cursor: pointer;
}
.Dropzone--active {
.dropzone--active {
background-color: $gray3;
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 { SoundList, SoundType } from '../../components';
import { axios } from '../../services';
import { SoundList, SoundType } from '../../components/SoundList';
interface Props {}
interface State {
@@ -36,7 +35,7 @@ export class Clips extends React.Component<Props, State> {
render() {
return (
<div className="Soundboard">
<div className="content">
<div className="column">
<SoundList soundList={this.state.clipList} type="Clips" />
</div>

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { axios } from '../../services';
import './Downloader.scss';
import './downloader.scss';
interface Props {}
@@ -70,13 +69,13 @@ export class Downloader extends React.Component<Props, State> {
render() {
return (
<div className="Downloader">
<div className="content">
<div className="card">
<div className="card__header">Youtube to MP3</div>
<input
placeholder="Enter Youtube URL"
className="input Downloader__input"
className="input downloader__input"
value={this.state.url}
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%;
display: flex;
align-items: center;
justify-content: center;
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 React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { axios, StorageService } from '../../services';
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 '~font-awesome/css/font-awesome.css';
@import './variables.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 {
font-family: 'Roboto', sans-serif;
}
@@ -26,11 +29,18 @@ i {
body {
background-color: $gray1;
color: $white;
padding-left: $navbarWidth;
// padding-left: $navbarWidth;
}
.flex {
display: flex;
}
.content {
padding: 10px;
padding: 20px;
@include tinyScreen {
padding: 10px;
}
}
.input {
@@ -43,48 +53,6 @@ body {
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 {
flex: 1;
@@ -92,3 +60,8 @@ body {
margin-left: 10px;
}
}
.text-wrap {
word-break: break-word;
word-wrap: break-word;
}

View File

@@ -1,14 +1,9 @@
import ax from 'axios';
import { StorageService } from './storage.service';
export const axios = ax.create();
axios.interceptors.request.use(
config => {
const jwt = StorageService.getJWT();
if (jwt) {
config.headers['Authorization'] = `Bearer ${jwt}`;
}
// Do something before request is sent
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>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<meta http-equiv="expires" content="0">
</head>

View File

@@ -1,6 +1,6 @@
{
"name": "go-discord-bot",
"version": "0.5.2",
"version": "0.6.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -5974,6 +5974,20 @@
"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": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz",
@@ -8166,6 +8180,11 @@
"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": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
version=$(git describe --tags)
docker build -t mgerb/go-discord-bot:$version .
docker tag mgerb/go-discord-bot:$version mgerb/go-discord-bot:latest
docker build -t 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:
go-discord-bot:
image: mgerb/go-discord-bot:latest
restart: unless-stopped
ports:
- 8080:8080
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.
![Image](https://i.imgur.com/BCoLAuK.png)
![Image](./screenshots/sound-bot.png)
## How to use
* [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 [ffmpeg](https://www.ffmpeg.org/download.html)
* edit your config.json file
* run the executable
* visit http://localhost:8080
- [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 [ffmpeg](https://www.ffmpeg.org/download.html)
- edit your config.json file
- run the executable
- visit http://localhost:8080
### With docker-compose
@@ -26,6 +26,7 @@ version: "2"
services:
go-discord-bot:
image: mgerb/go-discord-bot:latest
restart: unless-stopped
ports:
- 8080:8080
volumes:
@@ -34,10 +35,10 @@ services:
### Commands
* `clip` - clips the past minute of audio (currently bugged if more than one user is speaking)
* `summon` - summons the bot to your current channel
* `dismiss` - dismisses the bot from the server
* `<audio clip>` - play a named audio clip
- `clip` - clips the past minute of audio (currently bugged if more than one user is speaking)
- `summon` - summons the bot to your current channel
- `dismiss` - dismisses the bot from the server
- `<audio clip>` - play a named audio clip
### Uploading files
@@ -78,22 +79,22 @@ Check it out in the "Stats" page on the site.
### Dependencies
* Go
* node/npm
* make
- Go
- node/npm
- make
### Compiling
* Make sure dependencies are installed
* install packr - `go get -u github.com/gobuffalo/packr/...`
* Rename the `config.template.json` to `config.json`
* add configurations to `config.json`
* `cd client && npm run dev`
* `go run main.go`
* open a browser `localhost:<config_port>`
- Make sure dependencies are installed
- install packr - `go get -u github.com/gobuffalo/packr/...`
- Rename the `config.template.json` to `config.json`
- add configurations to `config.json`
- `cd client && npm run dev`
- `go run main.go`
- open a browser `localhost:<config_port>`
[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.
* `packr build`
* `packr install`
- `packr build`
- `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