Added JS avatar upload

This commit is contained in:
Lukas Obermann
2016-10-23 18:34:42 +02:00
parent e832e1761a
commit 7c905323e5
20 changed files with 1215 additions and 481 deletions
+20 -11
View File
@@ -11,24 +11,13 @@
.avatar-wrapper {
flex: 1 0 auto;
border-radius: 50%;
padding: 4px;
background: #785a28;
@include size(150px);
> div.avatar {
@include size(142px);
border-radius: 50%;
background-size: 100%;
box-shadow: 0 0 1px 1px black inset;
}
&:hover {
cursor: pointer;
> div.avatar {
box-shadow: 0 0 0 71px transparentize(black, .15) inset;
position: relative;
&::after {
position: absolute;
@@ -42,6 +31,20 @@
}
}
}
&:not(.no-avatar) {
padding: 4px;
background: #785a28;
> div.avatar::after {
top: 43px;
left: 43px;
}
}
&.no-avatar {
border-width: 4px;
}
}
.text-wrapper {
@@ -78,4 +81,10 @@
}
}
}
}
#profileavatarchange {
.avatar-wrapper {
@include size(64px);
}
}
+8 -1
View File
@@ -1,7 +1,8 @@
.avatar {
background-size: contain;
background-size: cover;
background-repeat: no-repeat;
background-position: 50%;
background-color: black;
box-shadow: 0 0 1px 1px black inset;
border-radius: 50%;
}
@@ -10,6 +11,11 @@
background: linear-gradient(to bottom, #9a7830 0%, #6d5024 100%);
padding: 2px;
border-radius: 50%;
position: relative;
> .avatar {
@include size(100%);
}
&.no-avatar {
border: 2px solid #2b2f34;
@@ -18,6 +24,7 @@
> .avatar {
box-shadow: none;
background: none;
}
}
}
+26 -13
View File
@@ -1,11 +1,10 @@
.btn, input[type="submit"] {
text-align: center;
background: #1a1a1a;
background-color: #1a1a1a;
padding: 3px 14px 6px;
letter-spacing: 0.05em;
border: 2px solid transparent;
border-image: linear-gradient(to bottom, #c6a767 0%, #ad8736 10%, #9a7830 76%, #6d5024 100%);
border-image-slice: 1;
border-image: linear-gradient(to bottom, #c6a767 0%, #ad8736 10%, #9a7830 76%, #6d5024 100%) 1 1 1 1;
display: inline-block;
box-shadow: 0 0 2px black inset, 0 0 2px black;
font: 400 17px/19px "Alegreya SC";
@@ -16,11 +15,18 @@
&:hover, &:active {
background-image: linear-gradient(to bottom, transparentize(#c89b3c, 1) 50%, transparentize(#c89b3c, .75) 100%);
border-image: linear-gradient(to bottom, #f0e6d2 0%, #c89b3c 100%);
border-image-slice: 1;
cursor: pointer;
}
&:hover {
border-image: linear-gradient(to bottom, #f0e6d2 0%, #c89b3c 100%) 1 1 1 1;
color: #f0e6d2;
}
&:active {
border-image: linear-gradient(to bottom, #c6a767 0%, #ad8736 10%, #9a7830 76%, #6d5024 100%) 1 1 1 1;
color: #5c5b57;
}
}
.btn {
@@ -28,19 +34,26 @@
text-decoration: none;
&.primary {
border-image: linear-gradient(to bottom, #0abdb7 0%, #0abdb7 60%, #0c4550 100%);
border-image-slice: 1;
border-image: linear-gradient(to bottom, #0abdb7 0%, #0abdb7 60%, #0c4550 100%) 1 1 1 1;
color: #a3c7c7;
background: #101721;
background-color: transparentize(#101721, .5);
min-width: 160px;
&:hover, &:active {
border-image: linear-gradient(to bottom, #b4fbfb 0%, #4bb8d9 100%);
border-image-slice: 1;
&:hover {
border-image: linear-gradient(to bottom, #b4fbfb 0%, #4bb8d9 100%) 1 1 1 1;
color: #caf8f7;
background-image: linear-gradient(to bottom, #102120 20%, #214341 100%);
box-shadow: 0 0 4px transparentize(#4be3de, .4), 0 0 4px transparentize(#4be3de, .4) inset;
text-shadow: 0 0 3px #4be3de;
box-shadow: 0 0 4px transparentize(#4be3de, .4), 0 0 4px transparentize(#4be3de, .4) inset;
background-image: linear-gradient(transparentize(#4bb8d9, 1) 0%, transparentize(#4bb8d9, .75) 100%);
}
&:active {
background-color: transparent;
color: #0d404c;
text-shadow: none;
border: 2px solid #0c4550;
box-shadow: 0 0 4px transparentize(#4be3de, .8), 0 0 4px transparentize(#4be3de, .8) inset;
background-image: linear-gradient(transparentize(#4bb8d9, 1) 0%, transparentize(#4bb8d9, .875) 100%);
}
}
+812 -375
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+25 -36
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -1,5 +1,8 @@
import AppDispatcher from '../dispatcher/AppDispatcher';
import ActionTypes from '../constants/ActionTypes';
import WebAPIUtils from '../utils/WebAPIUtils';
import createOverlay from '../utils/createOverlay';
import ProfileAvatarChange from '../components/content/profile/ProfileAvatarChange';
import React from 'react';
import ReactDOM from 'react-dom';
import Selections from '../components/content/rcp/Selections';
@@ -11,6 +14,12 @@ var ProfileActions = {
name
});
},
showImageUpload: function() {
createOverlay(<ProfileAvatarChange />);
},
changeAvatar: function({ source: type, extern, file }) {
WebAPIUtils.changeHeroAvatar(type, type === 'ext' ? extern : file);
},
changeHair: function(option) {
AppDispatcher.dispatch({
actionType: ActionTypes.UPDATE_HAIRCOLOR,
+14
View File
@@ -183,6 +183,20 @@ var ServerActions = {
AppDispatcher.dispatch({
actionType: ActionTypes.SAVE_HERO_SUCCESS
});
},
changeHeroAvatarSuccess: function(url) {
switch (url) {
case 'false':
this.runtimeError();
break;
default:
AppDispatcher.dispatch({
actionType: ActionTypes.UPDATE_HERO_AVATAR,
url
});
break;
}
}
};
@@ -76,6 +76,7 @@ class Herolist extends Component {
ProfileStore.getID() === null && ELStore.getStartID() !== 'EL_0' ? (
<HerolistItem
id={null}
avatar={ProfileStore.getPortrait()}
name="Ungespeicherter Held"
ap={{ _max: APStore.get() }}
/>
+2 -5
View File
@@ -46,8 +46,7 @@ class Overview extends Component {
weight: ProfileStore.getWeight()
});
showImageUpload = () => this.setState({ showImageUpload: true });
hideImageUpload = () => this.setState({ showImageUpload: false });
showImageUpload = () => ProfileActions.showImageUpload();
changeName = name => {
ProfileActions.changeName(name);
this.setState({ editName: false });
@@ -155,9 +154,7 @@ class Overview extends Component {
<div className="page" id="overview">
<Scroll>
<div className="title-wrapper">
<div className="avatar-wrapper" onClick={this.showImageUpload}>
<Avatar src={portrait} />
</div>
<Avatar src={portrait} onClick={this.showImageUpload} wrapper />
<div className="text-wrapper">
{nameElement}
<div className="rcp">
@@ -0,0 +1,119 @@
import Avatar from '../../layout/Avatar';
import Dialog from '../../layout/Dialog';
import Dropdown from '../../layout/Dropdown';
import ProfileActions from '../../../actions/ProfileActions';
import React, { Component, PropTypes } from 'react';
import TextField from '../../layout/TextField';
const Ext = ({ state, func }) => (
<div>
<TextField
key="ext"
hint="Externer Bildlink"
value={state}
onChange={func}
fullWidth />
<Avatar src={state} wrapper />
</div>
);
const FileValid = ({ func, preview }) => (
<div>
<TextField
key="file"
onChange={func}
type="file"
fullWidth />
<Avatar src={preview} wrapper />
</div>
);
const FileInvalid = ({ func, file }) => (
<div>
<TextField
key="file"
onChange={func}
type="file"
fullWidth />
{ file !== false ? (<p>Die Datei ist ungültig! Überprüfe bitte das Dateiformat!</p>) : null }
</div>
);
function File({ valid, ...other }) {
if (valid) {
return <FileValid {...other} />;
} else {
return <FileInvalid {...other} />;
}
}
function Source({ extern, file, fileValid, filePreview, source, changeExtern, changeFile }) {
if (source === 'ext') {
return <Ext state={extern} func={changeExtern} />;
} else if (source === 'file') {
return <File valid={fileValid} func={changeFile} preview={filePreview} file={file} />;
}
return null;
}
class ProfileAvatarChange extends Component {
static props = {
node: PropTypes.node
};
state = {
source: '',
extern: '',
file: false,
filePreview: '',
fileValid: false
};
constructor(props) {
super(props);
}
changeSource = source => this.setState({ source });
changeExtern = event => this.setState({ extern: event.target.value });
changeFile = event => {
let reader = new FileReader();
let file = event.target.files[0];
let filetype = file.type;
let mime = new Set(['image/jpeg','image/png','image/jpg']);
if (!file || !mime.has(filetype)) {
this.setState({ file, fileValid: !file, filePreview: '' });
} else {
reader.onload = e => {
this.setState({ file, fileValid: true, filePreview: e.target.result });
};
reader.readAsDataURL(file);
}
};
load = () => ProfileActions.changeAvatar(this.state);
render() {
const { source, extern, fileValid } = this.state;
return (
<Dialog id="profileavatarchange" title="Portrait ändern" node={this.props.node} buttons={[
{
label: 'Ändern',
onClick: this.load,
disabled: source === '' || (source === 'ext' && extern === '') || (source === 'file' && !fileValid)
}
]}>
<Dropdown
value={source}
onChange={this.changeSource}
options={[['Externes Bild', 'ext'], ['Bilddatei (max. 500KB)', 'file']]}
hint="Bildquelle auswählen"
fullWidth />
<Source {...this.state} changeExtern={this.changeExtern} changeFile={this.changeFile} />
</Dialog>
);
}
}
export default ProfileAvatarChange;
+16 -8
View File
@@ -9,22 +9,30 @@ class Avatar extends Component {
static propTypes = {
className: PropTypes.any,
src: PropTypes.string
src: PropTypes.string,
onClick: PropTypes.func,
wrapper: PropTypes.bool
};
constructor(props) {
super(props);
}
render() {
const className = classNames('avatar', this.props.className);
const { src, onClick, wrapper } = this.props;
if (this.props.src) {
if (wrapper) {
return (
<div className={className} style={{ backgroundImage: `url(${this.props.src})` }}></div>
<div className={classNames('avatar-wrapper', !src && 'no-avatar')} onClick={onClick}>
{this.props.children}
<div className={className} style={{ backgroundImage: `url(${src})` }}></div>
</div>
);
} else {
}
else if (src) {
return (
<div className={className} style={{ backgroundImage: `url(${src})` }}></div>
);
}
else {
return (
<div className={className}>
{this.props.children}
+29 -26
View File
@@ -6,17 +6,18 @@ import classNames from 'classnames';
class TextField extends Component {
static propTypes = {
autoFocus: PropTypes.bool,
className: PropTypes.any,
countCurrent: PropTypes.any,
countMax: PropTypes.any,
disabled: PropTypes.bool,
hint: PropTypes.string,
labelText: PropTypes.any,
multiLine: PropTypes.bool,
type: PropTypes.string,
value: PropTypes.any.isRequired,
onChange: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
countCurrent: PropTypes.any,
countMax: PropTypes.any
type: PropTypes.string,
value: PropTypes.any
};
static defaultProps = {
@@ -32,44 +33,46 @@ class TextField extends Component {
if (this.props.autoFocus) ReactDOM.findDOMNode(this.refs.inputElement).focus();
}
shouldComponentUpdate(nextProps) {
return nextProps.className !== this.props.className || nextProps.value !== this.props.value || nextProps.disabled !== this.props.disabled || nextProps.countCurrent !== this.props.countCurrent;
}
// shouldComponentUpdate(nextProps) {
// let { className: _className, value: _value, disabled: _disabled, countCurrent: _countCurrent, type: _type, hint: _hint } = nextProps;
// let { className, value, disabled, countCurrent, type, hint } = this.props;
// return _className !== className || _value !== value || _disabled !== disabled || _countCurrent !== countCurrent || _type !== type || _hint !== hint;
// }
render() {
const className = classNames('textfield', this.props.fullWidth && 'fullWidth', this.props.disabled && 'disabled', this.props.className);
let { className, countCurrent, countMax, disabled, fullWidth, hint, labelText, onChange, onKeyDown, type, value } = this.props;
const hintElementClassName = classNames('textfield-hint', this.props.value !== '' && 'hide');
className = classNames('textfield', fullWidth && 'fullWidth', disabled && 'disabled', className);
const hintElement = this.props.hint && (
<div className={hintElementClassName}>{this.props.hint}</div>
);
const hintElement = hint ? (
<div className={classNames('textfield-hint', value && 'hide')}>{hint}</div>
) : null;
const labelTextElement = this.props.labelText && (
<label>{this.props.labelText}</label>
);
const labelTextElement = labelText ? (
<label>{labelText}</label>
) : null;
// const inputElement = this.props.multiLine ? (
// <TextareaAutosize
// defaultValue={this.props.value}
// onChange={this.props.onChange}
// onKeyPress={this.props.onKeyPress}
// defaultValue={value}
// onChange={onChange}
// onKeyPress={onKeyDown}
// />
// ) : (
const inputElement = (
<input
type={this.props.type}
value={this.props.value}
onChange={this.props.onChange}
onKeyPress={this.props.onKeyPress}
type={type}
value={value}
onChange={onChange}
onKeyPress={onKeyDown}
ref='inputElement'
/>
);
const counterTextElement = this.props.countMax && (
<div>{this.props.countCurrent} / {this.props.countMax}</div>
);
const counterTextElement = countMax ? (
<div>{countCurrent} / {countMax}</div>
) : null;
return (
<div className={className}>
+1 -1
View File
@@ -36,7 +36,7 @@ export default keyMirror({
// ProfileStore
UPDATE_HERO_NAME: null,
UPDATE_HERO_PORTRAIT: null,
UPDATE_HERO_AVATAR: null,
UPDATE_HAIRCOLOR: null,
UPDATE_EYECOLOR: null,
UPDATE_SIZE: null,
+99
View File
@@ -0,0 +1,99 @@
import AppDispatcher from '../dispatcher/AppDispatcher';
import CultureStore from './rcp/CultureStore';
import ELStore from './ELStore';
import { EventEmitter } from 'events';
import ListStore from '../stores/ListStore';
import RaceStore from './rcp/RaceStore';
import ProfessionStore from './rcp/ProfessionStore';
import ProfessionVariantStore from './rcp/ProfessionVariantStore';
import ActionTypes from '../constants/ActionTypes';
import reactAlert from '../utils/reactAlert';
import reqPurchase from '../utils/reqPurchase';
var _history = [];
function _add(obj) {
_history.push(obj);
}
function _clear() {
_history = [];
}
function _updateAll(array) {
_history = array;
}
function _assignRCP(selections) {
// if (!selections.useCulturePackage) {
// _used -= _rcp[1];
// }
// if (selections.buyLiteracy) {
// const culture = CultureStore.getCurrent();
// let id = culture.literacy.length > 1 ? selections.litc : culture.literacy[0];
// _used += ListStore.get('SA_28').sel[id - 1][2];
// }
// let p = ProfessionStore.getCurrent();
// if (p) {
// let apCosts = reqPurchase(p.req);
// _used += apCosts;
// }
}
var HistoryStore = Object.assign({}, EventEmitter.prototype, {
emitChange: function() {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
},
get: function(index) {
return _history[index];
},
getAll: function() {
return _history;
}
});
HistoryStore.dispatchToken = AppDispatcher.register( function( payload ) {
switch( payload.actionType ) {
case ActionTypes.CLEAR_HERO:
_clear();
break;
case ActionTypes.RECEIVE_HERO:
_updateAll(payload.history);
break;
case ActionTypes.ASSIGN_RCP_ENTRIES:
_assignRCP(payload.selections);
break;
case ActionTypes.CREATE_NEW_HERO:
_clear();
break;
default:
return true;
}
HistoryStore.emitChange();
return true;
});
export default HistoryStore;
+1 -1
View File
@@ -180,7 +180,7 @@ ProfileStore.dispatchToken = AppDispatcher.register( function( payload ) {
_updateName(payload.name);
break;
case ActionTypes.UPDATE_HERO_PORTRAIT:
case ActionTypes.UPDATE_HERO_AVATAR:
_updatePortrait(payload.url);
break;
+1
View File
@@ -50,6 +50,7 @@ WaitStore.dispatchToken = AppDispatcher.register( function( payload ) {
case ActionTypes.RECEIVE_HERO:
case ActionTypes.RECEIVE_RAW_LISTS:
case ActionTypes.SAVE_HERO_SUCCESS:
case ActionTypes.UPDATE_HERO_AVATAR:
stopWaiting();
break;
+24
View File
@@ -3,6 +3,7 @@ import AccountActions from '../actions/AccountActions';
import AccountStore from '../stores/core/AccountStore';
import AppDispatcher from '../dispatcher/AppDispatcher';
import jQuery from 'jQuery';
import ProfileStore from '../stores/ProfileStore';
import React from 'react';
import ReactDOM from 'react-dom';
import ServerActions from '../actions/ServerActions';
@@ -351,6 +352,29 @@ var WebAPIUtils = {
// );
// });
},
changeHeroAvatar: function(type, data) {
ServerActions.startLoading();
var urlAdd = '';
var finalData;
if (type === 'ext') {
urlAdd = '&url=' + data;
} else if (type === 'file') {
finalData = new FormData(data);
}
// return new Promise(function(){
// jQuery.ajax({
// url: 'http://cha5app.dsa-sh.de/php/uploadheropic.php?hid=' + ProfileStore.getID() + '&type=' + type + urlAdd,
// type: 'POST',
// data: finalData,
// success: function(result) {
ServerActions.changeHeroAvatarSuccess(type === 'file' ? result : data);
// },
// error: function(error) {
// ServerActions.connectionError(error);
// }
// });
// });
},
deleteHero: function(heroid) {
ServerActions.startLoading();
return new Promise(function(resolve){
+6 -2
View File
@@ -4,6 +4,7 @@ import CombatTechniquesStore from '../stores/CombatTechniquesStore';
import CultureStore from '../stores/rcp/CultureStore';
import DisAdvStore from '../stores/DisAdvStore';
import ELStore from '../stores/ELStore';
import HistoryStore from '../stores/HistoryStore';
import LiturgiesStore from '../stores/LiturgiesStore';
import PhaseStore from '../stores/PhaseStore';
import ProfessionStore from '../stores/rcp/ProfessionStore';
@@ -21,7 +22,7 @@ export default () => {
{
client_version: VersionStore.get(),
date: (new Date()).toJSON(),
id: 'H_0',
id: ProfileStore.getID(),
phase: PhaseStore.get(),
name: ProfileStore.getName(),
avatar: ProfileStore.getPortrait(),
@@ -41,7 +42,10 @@ export default () => {
ct: CombatTechniquesStore.getForSave(),
spells: SpellsStore.getForSave(),
chants: LiturgiesStore.getForSave(),
sa: SpecialAbilitiesStore.getForSave()
sa: SpecialAbilitiesStore.getForSave(),
eq: {},
items: {},
history: HistoryStore.getAll()
}
];
+1 -1
View File
@@ -40,7 +40,7 @@
"build": "browserify -t [babelify] js/AppBootstrap.js | uglifyjs -mc > dist/bundle.min.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Lukas Obermann, Thore Schuchardt",
"author": "Lukas Obermann",
"license": "ISC",
"repository": {}
}