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:
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './Navbar';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './SoundList';
|
|
||||||
37
client/app/components/header/header.scss
Normal file
37
client/app/components/header/header.scss
Normal 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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/app/components/header/header.tsx
Normal file
26
client/app/components/header/header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
client/app/components/index.ts
Normal file
4
client/app/components/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './header/header';
|
||||||
|
export * from './navbar/navbar';
|
||||||
|
export * from './sound-list/sound-list';
|
||||||
|
export * from './uploader/uploader';
|
||||||
@@ -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;
|
||||||
86
client/app/components/navbar/navbar.tsx
Normal file
86
client/app/components/navbar/navbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
86
client/app/components/uploader/uploader.tsx
Normal file
86
client/app/components/uploader/uploader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
client/app/model/claims.ts
Normal file
12
client/app/model/claims.ts
Normal 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
|
||||||
|
}
|
||||||
2
client/app/model/index.ts
Normal file
2
client/app/model/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './claims';
|
||||||
|
export * from './permissions';
|
||||||
5
client/app/model/permissions.ts
Normal file
5
client/app/model/permissions.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export enum Permissions {
|
||||||
|
'Admin' = 3,
|
||||||
|
'Mod' = 2,
|
||||||
|
'User' = 1,
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './Clips';
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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 })}
|
||||||
/>
|
/>
|
||||||
6
client/app/pages/index.ts
Normal file
6
client/app/pages/index.ts
Normal 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';
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
4
client/app/pages/not-found/not-found.tsx
Normal file
4
client/app/pages/not-found/not-found.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './not-found.scss';
|
||||||
|
|
||||||
|
export const NotFound = () => <div className="not-found">404 Not Found</div>;
|
||||||
@@ -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> {}
|
||||||
|
|
||||||
|
|||||||
66
client/app/pages/soundboard/soundboard.tsx
Normal file
66
client/app/pages/soundboard/soundboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.Stats {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|||||||
20
client/app/scss/button.scss
Normal file
20
client/app/scss/button.scss
Normal 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
20
client/app/scss/card.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
5
client/app/scss/mixins.scss
Normal file
5
client/app/scss/mixins.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@mixin tinyScreen {
|
||||||
|
@media only screen and (max-width: 520px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
43
client/app/stores/app.store.ts
Normal file
43
client/app/stores/app.store.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
2
client/app/stores/index.ts
Normal file
2
client/app/stores/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './app.store';
|
||||||
|
export * from './root.store';
|
||||||
7
client/app/stores/root.store.ts
Normal file
7
client/app/stores/root.store.ts
Normal 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
17
client/app/util.ts
Normal 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
16
client/app/wrapper.scss
Normal 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
29
client/app/wrapper.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
21
client/package-lock.json
generated
21
client/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
1
fresh.conf
Normal file
@@ -0,0 +1 @@
|
|||||||
|
root: ./server
|
||||||
47
readme.md
47
readme.md
@@ -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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
1
run_client.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
cd client && npm run dev
|
||||||
1
run_server.sh
Executable file
1
run_server.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
fresh -c fresh.conf
|
||||||
BIN
screenshots/sound-bot.png
Normal file
BIN
screenshots/sound-bot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user