1
0
mirror of https://github.com/mgerb/classic-wow-forums synced 2026-01-11 09:32:51 +00:00

client - user account page done

This commit is contained in:
2018-01-13 00:31:01 -06:00
parent 57663b802c
commit e9d46ed5ce
12 changed files with 306 additions and 110 deletions

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { inject, observer } from 'mobx-react'; import { inject, observer } from 'mobx-react';
import { Portrait } from '../portrait/portrait'; import { Portrait } from '../portrait/portrait';
import { UserStore } from '../../stores/user-store'; import { UserStore } from '../../stores/user-store';
import { CharacterService } from '../../services';
interface Props extends RouteComponentProps<any> { interface Props {
className?: string; className?: string;
userStore?: UserStore; userStore?: UserStore;
onNavigate: (des: string) => any;
} }
interface State {} interface State {}
@@ -26,9 +27,16 @@ export class LoginButton extends React.Component<Props, State> {
} }
renderPortrait() { renderPortrait() {
const avatarSrc = CharacterService.getAvatar(this.props.userStore!.user!.character_avatar!);
return ( return (
<div onClick={() => this.props.history.push('/user-account')} style={{ cursor: 'pointer' }}> <div style={{ padding: '10px' }}>
<Portrait imageSrc={require('../../assets/Tyren.gif')}/> <div onClick={() => this.props.onNavigate('/user-account')} style={{ cursor: 'pointer' }}>
{avatarSrc && <Portrait imageSrc={avatarSrc}/>}
</div>
<div style={{ textAlign: 'center' }}>
{!avatarSrc && <p><a onClick={() => this.props.onNavigate('/user-account')}>Account</a></p>}
<div><b>{this.props.userStore!.user!.character_name}</b></div>
</div>
</div> </div>
); );
} }

View File

@@ -1,71 +1,6 @@
export interface AvatarModel { export interface AvatarModel {
title: string;
imageSrc: any; imageSrc: any;
raceId: number;
title: string;
} }
export const AvatarList: AvatarModel[] = [
{
title: 'dwarf_f',
imageSrc: require('../assets/avatars/Dwarf_female.gif'),
},
{
title: 'dwarf_m',
imageSrc: require('../assets/avatars/Dwarf_male.gif'),
},
{
title: 'gnome_f',
imageSrc: require('../assets/avatars/Gnome_female.gif'),
},
{
title: 'gnome_m',
imageSrc: require('../assets/avatars/Gnome_male.gif'),
},
{
title: 'human_f',
imageSrc: require('../assets/avatars/Human_female.gif'),
},
{
title: 'human_m',
imageSrc: require('../assets/avatars/Human_male.gif'),
},
{
title: 'night_elf_f',
imageSrc: require('../assets/avatars/Night_elf_female.gif'),
},
{
title: 'night_elf_m',
imageSrc: require('../assets/avatars/Night_elf_male.gif'),
},
{
title: 'orc_f',
imageSrc: require('../assets/avatars/Orc_female.gif'),
},
{
title: 'orc_m',
imageSrc: require('../assets/avatars/Orc_male.gif'),
},
{
title: 'tauren_f',
imageSrc: require('../assets/avatars/Tauren_female.gif'),
},
{
title: 'tauren_m',
imageSrc: require('../assets/avatars/Tauren_male.gif'),
},
{
title: 'troll_f',
imageSrc: require('../assets/avatars/Troll_female.gif'),
},
{
title: 'troll_m',
imageSrc: require('../assets/avatars/Troll_male.gif'),
},
{
title: 'undead_f',
imageSrc: require('../assets/avatars/Undead_female.gif'),
},
{
title: 'undead_m',
imageSrc: require('../assets/avatars/Undead_male.gif'),
},
];

View File

@@ -1,3 +1,5 @@
import { AvatarModel } from './avatar';
export interface CharacterModel { export interface CharacterModel {
achievementPoints: number; achievementPoints: number;
battlegroup: string; battlegroup: string;
@@ -10,4 +12,7 @@ export interface CharacterModel {
race: number; race: number;
realm: string; realm: string;
spec: any; spec: any;
avatarList?: AvatarModel[];
races?: number[];
} }

View File

@@ -34,7 +34,7 @@ export class Forum extends React.Component<Props, State> {
<div className="forum-header"> <div className="forum-header">
<ForumNav /> <ForumNav />
<div style={{ height: '100%' }}> <div style={{ height: '100%' }}>
<LoginButton {...this.props}/> <LoginButton onNavigate={dest => this.props.history.push(dest)}/>
</div> </div>
</div> </div>
); );

View File

@@ -12,7 +12,7 @@
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
top: 0; top: 0;
&:hover, &__selected { &:hover, &--selected {
top: -1px; top: -1px;
box-shadow: 0 0 10px #00C0FF; box-shadow: 0 0 10px #00C0FF;
} }

View File

@@ -4,8 +4,8 @@ import { RouteComponentProps } from 'react-router-dom';
import { get, groupBy, map } from 'lodash'; import { get, groupBy, map } from 'lodash';
import { ContentContainer, Portrait, ScrollToTop } from '../../components'; import { ContentContainer, Portrait, ScrollToTop } from '../../components';
import { UserStore } from '../../stores/user-store'; import { UserStore } from '../../stores/user-store';
import { UserService } from '../../services'; import { CharacterService, UserService } from '../../services';
import { AvatarList, CharacterModel } from '../../model'; import { CharacterModel } from '../../model';
import './user-account.scss'; import './user-account.scss';
interface Props extends RouteComponentProps<any> { interface Props extends RouteComponentProps<any> {
@@ -16,6 +16,9 @@ interface State {
characters: {[realm: string]: CharacterModel[]}; characters: {[realm: string]: CharacterModel[]};
selectedRealm?: string; selectedRealm?: string;
selectedCharIndex: number; selectedCharIndex: number;
selectedAvatarIndex: number;
insufficientScope?: boolean;
noCharacters: boolean;
} }
@inject('userStore') @inject('userStore')
@@ -25,7 +28,9 @@ export class UserAccount extends React.Component<Props, State> {
super(props); super(props);
this.state = { this.state = {
characters: {}, characters: {},
noCharacters: false,
selectedCharIndex: 0, selectedCharIndex: 0,
selectedAvatarIndex: 0,
}; };
} }
@@ -42,22 +47,43 @@ export class UserAccount extends React.Component<Props, State> {
async getCharacters() { async getCharacters() {
try { try {
const res = await UserService.getCharacters() as any; const res = await UserService.getCharacters() as any;
const characters = groupBy(res, 'realm'); if (res.characters) {
if (res.characters.length === 0) {
this.setState({ noCharacters: true });
return;
}
// remove classes that weren't in vanilla
const characters = groupBy(res.characters, 'realm');
this.setState({ this.setState({
characters, characters,
selectedRealm: res[0].realm, selectedRealm: res.characters[0].realm,
insufficientScope: false,
}); });
} else {
this.setState({ insufficientScope: true });
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} }
onRealmSelect(event: any) { onRealmSelect(event: any) {
this.setState({ selectedRealm: event.target.value, selectedCharIndex: 0 }); this.setState({
selectedRealm: event.target.value,
selectedCharIndex: 0,
selectedAvatarIndex: 0,
});
} }
onCharSelect(event: any) { onCharSelect(event: any) {
this.setState({ selectedCharIndex: event.target.value as any }); this.setState({
selectedCharIndex: event.target.value as any,
selectedAvatarIndex: 0,
});
}
onAvatarSelect(selectedAvatarIndex: number) {
this.setState({ selectedAvatarIndex });
} }
renderDropDowns() { renderDropDowns() {
@@ -65,7 +91,9 @@ export class UserAccount extends React.Component<Props, State> {
return <div></div>; return <div></div>;
} }
return ( return (
<div style={{ marginBottom: '10px' }}> <div>
<h2>Set your default character</h2>
<div style={{ margin: '0 10px 10px 0', display: 'inline-block' }}>
<select value={this.selectedCharacter().realm} <select value={this.selectedCharacter().realm}
onChange={event => this.onRealmSelect(event)}> onChange={event => this.onRealmSelect(event)}>
{map(this.state.characters, (_, realm: string) => { {map(this.state.characters, (_, realm: string) => {
@@ -78,11 +106,14 @@ export class UserAccount extends React.Component<Props, State> {
return <option key={this.state.selectedRealm! + index} value={index}>{value.name}</option>; return <option key={this.state.selectedRealm! + index} value={index}>{value.name}</option>;
})} })}
</select> </select>
</div>
<a onClick={() => this.onSave()}>Save</a>
<div className="avatar-list"> <div className="avatar-list">
{AvatarList.map((val, index) => { {this.selectedCharacter().avatarList!.map((val, index) => {
const avatarClass = this.state.selectedAvatarIndex === index ? 'avatar-list__item--selected' : '';
return ( return (
<div key={index} className="avatar-list__item"> <div key={index} className={`avatar-list__item ${avatarClass}`} onClick={() => this.onAvatarSelect(index)}>
<img src={val.imageSrc}/> <img src={val.imageSrc}/>
</div> </div>
); );
@@ -94,25 +125,61 @@ export class UserAccount extends React.Component<Props, State> {
private async onSave() { private async onSave() {
const { name, guild, realm } = this.selectedCharacter(); const { name, guild, realm } = this.selectedCharacter();
const selectedAvatar = this.selectedCharacter().avatarList![this.state.selectedAvatarIndex].title;
const charClass = CharacterService.getClass(this.selectedCharacter().class);
const data = { const data = {
character_name: name, character_name: name,
character_class: 'Rogue', // todo get class from number character_class: charClass.name,
character_guild: guild, character_guild: guild,
character_realm: realm, character_realm: realm,
character_avatar: 'Avatar', // TODO: character_avatar: selectedAvatar,
}; };
await UserService.saveCharacter(data); await UserService.saveCharacter(data);
} }
private logout() {
this.props.userStore!.resetUser();
window.location.pathname = '/';
}
private renderScopeError() {
return (
<div>
<p>
To set your default character
we need access to your WoW profile.
</p>
<ul>
<li><a href="https://us.battle.net/account/management/authorizations.html"
target="_blank">Navigate to your Battle.net Authorized Applications</a></li>
<li>Remove Classic WoW Forums</li>
<li>Log out and back into Classic WoW Forums</li>
<li>Grant Classic WoW Forums access to your WoW profile</li>
</ul>
</div>
);
}
render() { render() {
const { battletag, character_name, character_class, character_guild, character_realm } = this.props.userStore!.user!;
if (this.state.noCharacters) {
return <div>You have no WoW characters in your account.</div>;
}
// user must be logged in to view this page
if (!this.props.userStore!.user) {
return <div></div>;
}
const { battletag, character_name, character_class, character_guild, character_realm, character_avatar } = this.props.userStore!.user!;
const { insufficientScope } = this.state;
return ( return (
<ScrollToTop> <ScrollToTop>
<ContentContainer style={{ minHeight: '500px' }}> <ContentContainer style={{ minHeight: '500px', paddingTop: '40px' }}>
<div className="flex"> <div className="flex">
<Portrait imageSrc={require('../../assets/Tyren.gif')}/> {character_avatar && <Portrait imageSrc={CharacterService.getAvatar(character_avatar!)}/>}
<div style={{ paddingLeft: '10px' }}> <div style={{ paddingLeft: '10px' }}>
{battletag && <div><b>Battletag: </b>{battletag}</div>} {battletag && <div><b>Battletag: </b>{battletag}</div>}
{character_name && <div><b>Character: </b>{character_name}</div>} {character_name && <div><b>Character: </b>{character_name}</div>}
@@ -120,12 +187,13 @@ export class UserAccount extends React.Component<Props, State> {
{character_guild && <div><b>Guild: </b>{character_guild}</div>} {character_guild && <div><b>Guild: </b>{character_guild}</div>}
{character_realm && <div><b>Realm: </b>{character_realm}</div>} {character_realm && <div><b>Realm: </b>{character_realm}</div>}
</div> </div>
<div className="flex-1" style={{ textAlign: 'right' }}>
<a onClick={() => this.logout()}>Logout</a>
</div>
</div> </div>
<div> <div>
<h2>Set a new default character</h2> {insufficientScope === true ? this.renderScopeError() : this.renderDropDowns()}
{this.renderDropDowns()}
<a onClick={() => this.onSave()}>Save</a>
</div> </div>
</ContentContainer> </ContentContainer>

View File

@@ -0,0 +1,156 @@
import { find, filter, get } from 'lodash';
import { AvatarModel } from '../model';
const getAvatar = (title: string): any => {
const av = find(avatarList, { title });
return get(av, 'imageSrc');
};
const getClass = (index: number): {id: number, name: string; races: number[] } => {
return find(classList, { id: index })!;
};
const getFilteredAvatarList = (raceIdList: number[]) => {
return filter(avatarList, (av) => {
return raceIdList.includes(av.raceId);
});
};
// taken right from API data
const classList = [
{
id: 1,
name: 'Warrior',
races: [1, 2, 3, 4, 5, 6, 7, 8],
},
{
id: 2,
name: 'Paladin',
races: [1, 3],
},
{
id: 3,
name: 'Hunter',
races: [1, 4, 5, 6, 7],
},
{
id: 4,
name: 'Rogue',
races: [1, 2, 3, 4, 5, 7, 8],
},
{
id: 5,
name: 'Priest',
races: [1, 3, 4, 7, 8],
},
{
id: 7,
name: 'Shaman',
races: [5, 6, 7],
},
{
id: 8,
name: 'Mage',
races: [2, 3, 7, 8],
},
{
id: 9,
name: 'Warlock',
races: [2, 3, 5, 8],
},
{
id: 11,
name: 'Druid',
races: [4, 6],
},
];
const avatarList: AvatarModel[] = [
{
raceId: 1,
title: 'dwarf_f',
imageSrc: require('../assets/avatars/Dwarf_female.gif'),
},
{
raceId: 1,
title: 'dwarf_m',
imageSrc: require('../assets/avatars/Dwarf_male.gif'),
},
{
raceId: 2,
title: 'gnome_f',
imageSrc: require('../assets/avatars/Gnome_female.gif'),
},
{
raceId: 2,
title: 'gnome_m',
imageSrc: require('../assets/avatars/Gnome_male.gif'),
},
{
raceId: 3,
title: 'human_f',
imageSrc: require('../assets/avatars/Human_female.gif'),
},
{
raceId: 3,
title: 'human_m',
imageSrc: require('../assets/avatars/Human_male.gif'),
},
{
raceId: 4,
title: 'night_elf_f',
imageSrc: require('../assets/avatars/Night_elf_female.gif'),
},
{
raceId: 4,
title: 'night_elf_m',
imageSrc: require('../assets/avatars/Night_elf_male.gif'),
},
{
raceId: 5,
title: 'orc_f',
imageSrc: require('../assets/avatars/Orc_female.gif'),
},
{
raceId: 5,
title: 'orc_m',
imageSrc: require('../assets/avatars/Orc_male.gif'),
},
{
raceId: 6,
title: 'tauren_f',
imageSrc: require('../assets/avatars/Tauren_female.gif'),
},
{
raceId: 6,
title: 'tauren_m',
imageSrc: require('../assets/avatars/Tauren_male.gif'),
},
{
raceId: 7,
title: 'troll_f',
imageSrc: require('../assets/avatars/Troll_female.gif'),
},
{
raceId: 7,
title: 'troll_m',
imageSrc: require('../assets/avatars/Troll_male.gif'),
},
{
raceId: 8,
title: 'undead_f',
imageSrc: require('../assets/avatars/Undead_female.gif'),
},
{
raceId: 8,
title: 'undead_m',
imageSrc: require('../assets/avatars/Undead_male.gif'),
},
];
export const CharacterService = {
avatarList,
getAvatar,
getClass,
getFilteredAvatarList,
};

View File

@@ -1,3 +1,4 @@
export * from './category.service'; export * from './category.service';
export * from './character.service';
export * from './thread.service'; export * from './thread.service';
export * from './user.service'; export * from './user.service';

View File

@@ -1,6 +1,8 @@
import { chain } from 'lodash';
import axios from '../axios/axios'; import axios from '../axios/axios';
import userStore from '../stores/user-store'; import userStore from '../stores/user-store';
import { CharacterModel } from '../model'; import { CharacterModel } from '../model';
import { CharacterService } from './character.service';
// fetch user and store in local storage // fetch user and store in local storage
const authorize = async (code: string): Promise<void> => { const authorize = async (code: string): Promise<void> => {
@@ -12,16 +14,31 @@ const authorize = async (code: string): Promise<void> => {
} }
}; };
const getCharacters = async (): Promise<CharacterModel | null> => { const getCharacters = async (): Promise<any> => {
try { try {
const res = await axios.get('/api/user/characters'); const res = await axios.get('/api/user/characters');
return res.data.data.characters; const characters = res.data.data.characters;
if (!!characters) {
res.data.data.characters = filterCharacters(characters);
}
return res.data.data;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
return null; return null;
}; };
const filterCharacters = (chars: CharacterModel[]): CharacterModel[] => {
return chain(chars)
.filter(c => !!CharacterService.getClass(c.class))
.map((c) => {
c.races = CharacterService.getClass(c.class).races;
c.avatarList = CharacterService.getFilteredAvatarList(c.races);
return c;
})
.value();
};
const saveCharacter = async (character: any): Promise<any> => { const saveCharacter = async (character: any): Promise<any> => {
try { try {
const res = await axios.put('/api/user/characters', character); const res = await axios.put('/api/user/characters', character);

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "es7"],
"experimentalDecorators": true, "experimentalDecorators": true,
"target": "es2015", "target": "es2015",
"module": "es2015", "module": "es2015",

View File

@@ -45,7 +45,12 @@ defmodule MyApp.BattleNet.User do
defp parse_character_response({:ok, %HTTPoison.Response{body: body}}, user_id) do defp parse_character_response({:ok, %HTTPoison.Response{body: body}}, user_id) do
case Poison.decode(body) do case Poison.decode(body) do
{:ok, data} -> {:ok, data} ->
# only cache end point if characters return
if (!data["characters"]) do
{:error, data}
else
Cachex.set(:myapp, "usr_char:#{user_id}", data, ttl: :timer.minutes(10)) # 10 minutes Cachex.set(:myapp, "usr_char:#{user_id}", data, ttl: :timer.minutes(10)) # 10 minutes
end
{:ok, data} {:ok, data}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end

View File

@@ -70,8 +70,8 @@ defmodule MyApp.Data.Thread do
:title, :title,
:view_count, :view_count,
:reply_count, :reply_count,
user: [:id, :battletag], user: [:id, :battletag, :character_guild, :character_name, :character_class, :character_realm, :character_avatar],
last_reply: [:id, :battletag], last_reply: [:id, :battletag, :character_guild, :character_name, :character_class, :character_realm, :character_avatar],
]), ]),
where: [category_id: ^category_id], where: [category_id: ^category_id],
preload: [:user, :last_reply] preload: [:user, :last_reply]