diff --git a/client/app/assets/avatars/Caydiem.gif b/client/app/assets/avatars/Caydiem.gif new file mode 100644 index 0000000..5012696 Binary files /dev/null and b/client/app/assets/avatars/Caydiem.gif differ diff --git a/client/app/assets/avatars/Ordinn.gif b/client/app/assets/avatars/Ordinn.gif new file mode 100644 index 0000000..8623e0a Binary files /dev/null and b/client/app/assets/avatars/Ordinn.gif differ diff --git a/client/app/axios/axios.ts b/client/app/axios/axios.ts index 3702cb1..cf2e3dd 100644 --- a/client/app/axios/axios.ts +++ b/client/app/axios/axios.ts @@ -29,9 +29,13 @@ export const initializeAxios = (): Promise => { return config; }, (error: any) => { + nprogress.done(); + // if code is unauthorized (401) then logout if already logged in if (error.response.status === 401 && userStore.user) { userStore.resetUser(); + // redirect back to home page upon logout + window.location.pathname = '/'; } return Promise.reject(error); diff --git a/client/app/components/editor/editor.scss b/client/app/components/editor/editor.scss index ee73fa1..9ff1239 100644 --- a/client/app/components/editor/editor.scss +++ b/client/app/components/editor/editor.scss @@ -48,6 +48,5 @@ &__error-message { margin-bottom: 10px; - color: red; } } diff --git a/client/app/components/editor/editor.tsx b/client/app/components/editor/editor.tsx index 95ff3ca..16c1399 100644 --- a/client/app/components/editor/editor.tsx +++ b/client/app/components/editor/editor.tsx @@ -163,7 +163,7 @@ export class Editor extends React.Component {
{this.state.contentCharacterCount}/2000
-
{this.state.errorMessage}
+
{this.state.errorMessage}
diff --git a/client/app/pages/forum/forum.tsx b/client/app/pages/forum/forum.tsx index 29920d5..5e81ed1 100644 --- a/client/app/pages/forum/forum.tsx +++ b/client/app/pages/forum/forum.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; +import { orderBy } from 'lodash'; import { ThreadService } from '../../services'; import { Editor, ForumNav, LoginButton, ScrollToTop } from '../../components'; import { ThreadModel } from '../../model'; @@ -8,6 +9,8 @@ import { UserStore } from '../../stores/user-store'; import './forum.scss'; import { Oauth } from '../../util'; +const stickyImage = require('../../assets/sticky.gif'); + interface Props extends RouteComponentProps { userStore: UserStore; } @@ -40,10 +43,15 @@ export class Forum extends React.Component { } private async getThreads(categoryId: string) { - const threads = await ThreadService.getCategoryThreads(categoryId); + let threads = await ThreadService.getCategoryThreads(categoryId); + threads = this.orderBy(threads); this.setState({ threads }); } + private orderBy(threads: ThreadModel[]) { + return orderBy(threads, ['sticky', 'updated_at'], ['desc', 'desc']); + } + onNewTopic() { if (this.props.userStore.user) { this.setState({ showEditor: true }); @@ -94,7 +102,7 @@ export class Forum extends React.Component { renderCell(content: JSX.Element | string, style: any, center?: boolean, header?: boolean) { let classNames: string = ''; - classNames += center && ' forum-cell--center'; + classNames += center ? ' forum-cell--center': ''; classNames += header ? ' forum-cell--header' : ' forum-cell--body'; return
{content}
; } @@ -102,15 +110,20 @@ export class Forum extends React.Component { renderThreadRows() { const categoryId = this.props.match.params['id']; return this.state.threads.map((thread, index) => { + const authorBluePost = thread.user.permissions === 'admin' ? 'blue' : ''; + const lastReplyBluePost = thread.last_reply.permissions === 'admin' ? 'blue' : ''; + const sticky = thread.sticky ? : ''; return (
- {this.renderCell('flag', { maxWidth: '50px' })} + {this.renderCell(sticky, { maxWidth: '50px' }, true)} {this.renderCell({thread.title}, { minWidth: '200px' })} - {this.renderCell({thread.user.character_name || thread.user.battletag}, { maxWidth: '150px' })} + {this.renderCell({thread.user.character_name || thread.user.battletag}, { maxWidth: '150px' })} {this.renderCell({thread.reply_count}, { maxWidth: '150px' }, true)} {this.renderCell({thread.view_count}, { maxWidth: '150px' }, true)} {this.renderCell( - by {thread.last_reply.character_name || thread.last_reply.battletag}, +
+ by {thread.last_reply.character_name || thread.last_reply.battletag} +
, { maxWidth: '200px' }, )}
diff --git a/client/app/pages/index.ts b/client/app/pages/index.ts index 6bba6e4..ccc1bb9 100644 --- a/client/app/pages/index.ts +++ b/client/app/pages/index.ts @@ -1,4 +1,5 @@ export * from './forum/forum'; +export * from './login/login'; export * from './home/home'; export * from './not-found/not-found'; export * from './oauth/oauth'; diff --git a/client/app/pages/login/login.tsx b/client/app/pages/login/login.tsx new file mode 100644 index 0000000..a0edb3b --- /dev/null +++ b/client/app/pages/login/login.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { ContentContainer } from '../../components'; +import { UserService } from '../../services'; + +interface Props extends RouteComponentProps {} + +interface State { + username: string; + password: string; + errorMessage?: string; +} + +export class Login extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + username: '', + password: '', + }; + } + + private async login(event: any) { + event.preventDefault(); + const { username, password } = this.state; + + try { + await UserService.login(username, password); + window.location.pathname = '/'; + } catch (e) { + console.error(e); + this.setState({ errorMessage: 'Invalid login.' }); + } + } + + render() { + const { username, password } = this.state; + + return ( + +
this.login(e)}> +
+ + this.setState({ username: event.target.value })} + /> +
+ +
+ + this.setState({ password: event.target.value })} /> +
+ +
+ + + {this.state.errorMessage} + +
+
+
+ ); + } +} diff --git a/client/app/pages/thread/thread.tsx b/client/app/pages/thread/thread.tsx index bcd5c6c..09547b8 100644 --- a/client/app/pages/thread/thread.tsx +++ b/client/app/pages/thread/thread.tsx @@ -84,7 +84,7 @@ export class Thread extends React.Component { } renderUserInfo(reply: ReplyModel) { - const { battletag, character_avatar, character_class, character_guild, character_name, character_realm } = reply.user; + const { battletag, character_avatar, character_class, character_guild, character_name, character_realm, permissions } = reply.user; return (
@@ -93,6 +93,7 @@ export class Thread extends React.Component { {character_class &&
{character_class}
} {character_guild &&
Guild: {character_guild}
} {character_realm &&
Realm: {character_realm}
} + {permissions === 'admin' &&
Admin Poster
}
); @@ -101,6 +102,7 @@ export class Thread extends React.Component { renderReplies(): any { return this.state.thread!.replies.map((reply, index) => { const replyDark = index % 2 === 0 ? 'reply--dark' : ''; + const bluePost = reply.user.permissions === 'admin' ? 'blue-post' : ''; return (
@@ -124,7 +126,7 @@ export class Thread extends React.Component {
{this.renderQuotedReply(reply)} -
+
diff --git a/client/app/pages/user-account/user-account.tsx b/client/app/pages/user-account/user-account.tsx index 01fd13e..ae28884 100644 --- a/client/app/pages/user-account/user-account.tsx +++ b/client/app/pages/user-account/user-account.tsx @@ -35,7 +35,10 @@ export class UserAccount extends React.Component { } componentDidMount() { - this.getCharacters(); + // only load characters if user is battenet user + if (get(this.props, 'userStore.user.access_token')) { + this.getCharacters(); + } } private selectedCharacter(): CharacterModel { @@ -85,43 +88,6 @@ export class UserAccount extends React.Component { this.setState({ selectedAvatarIndex }); } - renderDropDowns() { - if (!this.selectedCharacter()) { - return
; - } - return ( -
-

Set your default character

-
- - -
- - this.onSave()}>Save -
- {this.selectedCharacter().avatarList!.map((val, index) => { - const avatarClass = this.state.selectedAvatarIndex === index ? 'avatar-list__item--selected' : ''; - return ( -
this.onAvatarSelect(index)}> - -
- ); - })} -
-
- ); - } - private async onSave() { const { name, guild, realm } = this.selectedCharacter(); const selectedAvatar = this.selectedCharacter().avatarList![this.state.selectedAvatarIndex].title; @@ -160,6 +126,43 @@ export class UserAccount extends React.Component { ); } + renderDropDowns() { + if (!this.selectedCharacter()) { + return
; + } + return ( +
+

Set your default character

+
+ + +
+ + this.onSave()}>Save +
+ {this.selectedCharacter().avatarList!.map((val, index) => { + const avatarClass = this.state.selectedAvatarIndex === index ? 'avatar-list__item--selected' : ''; + return ( +
this.onAvatarSelect(index)}> + +
+ ); + })} +
+
+ ); + } + render() { if (this.state.noCharacters) { diff --git a/client/app/routes.tsx b/client/app/routes.tsx index 530b81e..c6673ab 100644 --- a/client/app/routes.tsx +++ b/client/app/routes.tsx @@ -3,7 +3,7 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { Provider } from 'mobx-react'; import { initializeAxios } from './axios/axios'; import { Footer, Header } from './components'; -import { Forum, Home, NotFound, Oauth, Realms, Thread, UserAccount } from './pages'; +import { Forum, Home, Login, NotFound, Oauth, Realms, Thread, UserAccount } from './pages'; import { stores } from './stores/stores'; // styling @@ -48,6 +48,7 @@ export class Routes extends React.Component { +