From e9d46ed5ce19bce9b8355569d12cf9ce30b6925c Mon Sep 17 00:00:00 2001 From: Mitchell Gerber Date: Sat, 13 Jan 2018 00:31:01 -0600 Subject: [PATCH] client - user account page done --- .../components/login-button/login-button.tsx | 16 +- client/app/model/avatar.ts | 69 +------- client/app/model/character.ts | 5 + client/app/pages/forum/forum.tsx | 2 +- .../app/pages/user-account/user-account.scss | 2 +- .../app/pages/user-account/user-account.tsx | 132 +++++++++++---- client/app/services/character.service.ts | 156 ++++++++++++++++++ client/app/services/index.ts | 1 + client/app/services/user.service.ts | 21 ++- client/tsconfig.json | 1 + lib/myapp/battle_net/user.ex | 7 +- lib/myapp/data/thread.ex | 4 +- 12 files changed, 306 insertions(+), 110 deletions(-) create mode 100644 client/app/services/character.service.ts diff --git a/client/app/components/login-button/login-button.tsx b/client/app/components/login-button/login-button.tsx index ad76c7e..71147a8 100644 --- a/client/app/components/login-button/login-button.tsx +++ b/client/app/components/login-button/login-button.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { Portrait } from '../portrait/portrait'; import { UserStore } from '../../stores/user-store'; +import { CharacterService } from '../../services'; -interface Props extends RouteComponentProps { +interface Props { className?: string; userStore?: UserStore; + onNavigate: (des: string) => any; } interface State {} @@ -26,9 +27,16 @@ export class LoginButton extends React.Component { } renderPortrait() { + const avatarSrc = CharacterService.getAvatar(this.props.userStore!.user!.character_avatar!); return ( -
this.props.history.push('/user-account')} style={{ cursor: 'pointer' }}> - +
+
this.props.onNavigate('/user-account')} style={{ cursor: 'pointer' }}> + {avatarSrc && } +
+
+ {!avatarSrc &&

this.props.onNavigate('/user-account')}>Account

} +
{this.props.userStore!.user!.character_name}
+
); } diff --git a/client/app/model/avatar.ts b/client/app/model/avatar.ts index 2cbb388..9980129 100644 --- a/client/app/model/avatar.ts +++ b/client/app/model/avatar.ts @@ -1,71 +1,6 @@ export interface AvatarModel { - title: string; 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'), - }, -]; diff --git a/client/app/model/character.ts b/client/app/model/character.ts index b55be31..cb13aa7 100644 --- a/client/app/model/character.ts +++ b/client/app/model/character.ts @@ -1,3 +1,5 @@ +import { AvatarModel } from './avatar'; + export interface CharacterModel { achievementPoints: number; battlegroup: string; @@ -10,4 +12,7 @@ export interface CharacterModel { race: number; realm: string; spec: any; + + avatarList?: AvatarModel[]; + races?: number[]; } diff --git a/client/app/pages/forum/forum.tsx b/client/app/pages/forum/forum.tsx index 04e9ca6..700171f 100644 --- a/client/app/pages/forum/forum.tsx +++ b/client/app/pages/forum/forum.tsx @@ -34,7 +34,7 @@ export class Forum extends React.Component {
- + this.props.history.push(dest)}/>
); diff --git a/client/app/pages/user-account/user-account.scss b/client/app/pages/user-account/user-account.scss index e110247..6c73899 100644 --- a/client/app/pages/user-account/user-account.scss +++ b/client/app/pages/user-account/user-account.scss @@ -12,7 +12,7 @@ transition: all 0.1s ease-in-out; top: 0; - &:hover, &__selected { + &:hover, &--selected { top: -1px; box-shadow: 0 0 10px #00C0FF; } diff --git a/client/app/pages/user-account/user-account.tsx b/client/app/pages/user-account/user-account.tsx index 50b677d..0aa0b21 100644 --- a/client/app/pages/user-account/user-account.tsx +++ b/client/app/pages/user-account/user-account.tsx @@ -4,8 +4,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { get, groupBy, map } from 'lodash'; import { ContentContainer, Portrait, ScrollToTop } from '../../components'; import { UserStore } from '../../stores/user-store'; -import { UserService } from '../../services'; -import { AvatarList, CharacterModel } from '../../model'; +import { CharacterService, UserService } from '../../services'; +import { CharacterModel } from '../../model'; import './user-account.scss'; interface Props extends RouteComponentProps { @@ -16,6 +16,9 @@ interface State { characters: {[realm: string]: CharacterModel[]}; selectedRealm?: string; selectedCharIndex: number; + selectedAvatarIndex: number; + insufficientScope?: boolean; + noCharacters: boolean; } @inject('userStore') @@ -25,7 +28,9 @@ export class UserAccount extends React.Component { super(props); this.state = { characters: {}, + noCharacters: false, selectedCharIndex: 0, + selectedAvatarIndex: 0, }; } @@ -42,22 +47,43 @@ export class UserAccount extends React.Component { async getCharacters() { try { const res = await UserService.getCharacters() as any; - const characters = groupBy(res, 'realm'); - this.setState({ - characters, - selectedRealm: res[0].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({ + characters, + selectedRealm: res.characters[0].realm, + insufficientScope: false, + }); + } else { + this.setState({ insufficientScope: true }); + } } catch (e) { console.error(e); } } onRealmSelect(event: any) { - this.setState({ selectedRealm: event.target.value, selectedCharIndex: 0 }); + this.setState({ + selectedRealm: event.target.value, + selectedCharIndex: 0, + selectedAvatarIndex: 0, + }); } 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() { @@ -65,24 +91,29 @@ export class UserAccount extends React.Component { return
; } return ( -
- - +
+

Set your default character

+
+ + +
+ this.onSave()}>Save
- {AvatarList.map((val, index) => { + {this.selectedCharacter().avatarList!.map((val, index) => { + const avatarClass = this.state.selectedAvatarIndex === index ? 'avatar-list__item--selected' : ''; return ( -
+
this.onAvatarSelect(index)}>
); @@ -94,25 +125,61 @@ export class UserAccount extends React.Component { private async onSave() { const { name, guild, realm } = this.selectedCharacter(); + const selectedAvatar = this.selectedCharacter().avatarList![this.state.selectedAvatarIndex].title; + const charClass = CharacterService.getClass(this.selectedCharacter().class); const data = { character_name: name, - character_class: 'Rogue', // todo get class from number + character_class: charClass.name, character_guild: guild, character_realm: realm, - character_avatar: 'Avatar', // TODO: + character_avatar: selectedAvatar, }; await UserService.saveCharacter(data); } + private logout() { + this.props.userStore!.resetUser(); + window.location.pathname = '/'; + } + + private renderScopeError() { + return ( +
+

+ To set your default character + we need access to your WoW profile. +

+ +
+ ); + } + render() { - const { battletag, character_name, character_class, character_guild, character_realm } = this.props.userStore!.user!; + + if (this.state.noCharacters) { + return
You have no WoW characters in your account.
; + } + + // user must be logged in to view this page + if (!this.props.userStore!.user) { + return
; + } + + const { battletag, character_name, character_class, character_guild, character_realm, character_avatar } = this.props.userStore!.user!; + const { insufficientScope } = this.state; return ( - +
- + {character_avatar && }
{battletag &&
Battletag: {battletag}
} {character_name &&
Character: {character_name}
} @@ -120,12 +187,13 @@ export class UserAccount extends React.Component { {character_guild &&
Guild: {character_guild}
} {character_realm &&
Realm: {character_realm}
}
+
-

Set a new default character

- {this.renderDropDowns()} - this.onSave()}>Save + {insufficientScope === true ? this.renderScopeError() : this.renderDropDowns()}
diff --git a/client/app/services/character.service.ts b/client/app/services/character.service.ts new file mode 100644 index 0000000..6cf513a --- /dev/null +++ b/client/app/services/character.service.ts @@ -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, +}; diff --git a/client/app/services/index.ts b/client/app/services/index.ts index 37d8b3e..9286c0a 100644 --- a/client/app/services/index.ts +++ b/client/app/services/index.ts @@ -1,3 +1,4 @@ export * from './category.service'; +export * from './character.service'; export * from './thread.service'; export * from './user.service'; diff --git a/client/app/services/user.service.ts b/client/app/services/user.service.ts index 0162014..89f1d28 100644 --- a/client/app/services/user.service.ts +++ b/client/app/services/user.service.ts @@ -1,6 +1,8 @@ +import { chain } from 'lodash'; import axios from '../axios/axios'; import userStore from '../stores/user-store'; import { CharacterModel } from '../model'; +import { CharacterService } from './character.service'; // fetch user and store in local storage const authorize = async (code: string): Promise => { @@ -12,16 +14,31 @@ const authorize = async (code: string): Promise => { } }; -const getCharacters = async (): Promise => { +const getCharacters = async (): Promise => { try { 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) { console.error(e); } 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 => { try { const res = await axios.put('/api/user/characters', character); diff --git a/client/tsconfig.json b/client/tsconfig.json index 8798303..03231a5 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "lib": ["dom", "es7"], "experimentalDecorators": true, "target": "es2015", "module": "es2015", diff --git a/lib/myapp/battle_net/user.ex b/lib/myapp/battle_net/user.ex index 597e119..8df079b 100644 --- a/lib/myapp/battle_net/user.ex +++ b/lib/myapp/battle_net/user.ex @@ -45,7 +45,12 @@ defmodule MyApp.BattleNet.User do defp parse_character_response({:ok, %HTTPoison.Response{body: body}}, user_id) do case Poison.decode(body) do {:ok, data} -> - Cachex.set(:myapp, "usr_char:#{user_id}", data, ttl: :timer.minutes(10)) # 10 minutes + # 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 + end {:ok, data} {:error, error} -> {:error, error} end diff --git a/lib/myapp/data/thread.ex b/lib/myapp/data/thread.ex index 52c244b..4baa5e6 100644 --- a/lib/myapp/data/thread.ex +++ b/lib/myapp/data/thread.ex @@ -70,8 +70,8 @@ defmodule MyApp.Data.Thread do :title, :view_count, :reply_count, - user: [:id, :battletag], - last_reply: [:id, :battletag], + user: [:id, :battletag, :character_guild, :character_name, :character_class, :character_realm, :character_avatar], + last_reply: [:id, :battletag, :character_guild, :character_name, :character_class, :character_realm, :character_avatar], ]), where: [category_id: ^category_id], preload: [:user, :last_reply]