Browse Source

move cropper and radio buttons into separate component and add more js tests

Jess MacQueen 8 years ago
parent
commit
41a91050c9

+ 358 - 0
src/sentry/static/sentry/app/components/avatarCropper.jsx

@@ -0,0 +1,358 @@
+import React from 'react';
+
+import AlertActions from '../actions/alertActions';
+import {t} from '../locale';
+
+
+const AvatarCropper = React.createClass({
+  propTypes: {
+    user: React.PropTypes.object.isRequired,
+    updateDataUrlState: React.PropTypes.func.isRequired,
+    savedDataUrl: React.PropTypes.string
+  },
+
+  getInitialState() {
+    return {
+      mousePosition: {
+        pageX: null,
+        pageY: null
+      },
+      resizeDimensions: {
+        top: 0,
+        left: 0,
+        size: 0
+      }
+    };
+  },
+
+  MIN_DIMENSION: 256,
+
+  MAX_DIMENSION: 1024,
+
+  onChange(ev) {
+    let file = ev.target.files[0];
+
+    if (!/^image\//.test(file.type))
+      return void this.handleError('That is not a supported file type.');
+
+    this.state.objectURL && window.URL.revokeObjectURL(this.state.objectURL);
+    this.setState({
+      file: file,
+      objectURL: window.URL.createObjectURL(file)
+    }, () => {
+      this.props.updateDataUrlState({savedDataUrl: null});
+    });
+  },
+
+  componentWIllUnmount() {
+    this.state.objectURL && window.URL.revokeObjectURL(this.state.objectURL);
+  },
+
+  updateDimensions(ev) {
+    let $container = $(this.refs.cropContainer);
+    let resizeDimensions = this.state.resizeDimensions;
+    let pageY = ev.pageY;
+    let pageX = ev.pageX;
+    let top = resizeDimensions.top + (pageY - this.state.mousePosition.pageY);
+    let left = resizeDimensions.left + (pageX - this.state.mousePosition.pageX);
+    if (top < 0) {
+      top = 0;
+      pageY = this.state.mousePosition.pageY;
+    } else if (top + resizeDimensions.size > $container.height()) {
+      top = $container.height() - resizeDimensions.size;
+      pageY = this.state.mousePosition.pageY;
+    }
+    if (left < 0) {
+      left = 0;
+      pageX = this.state.mousePosition.pageX;
+    } else if (left + resizeDimensions.size > $container.width()) {
+      left = $container.width() - resizeDimensions.size;
+      pageX = this.state.mousePosition.pageX;
+    }
+    this.setState({
+      resizeDimensions: Object.assign({}, resizeDimensions, {top, left}),
+      mousePosition: {pageX, pageY}
+    });
+  },
+
+  startMove() {
+    $(document).on('mousemove', this.updateDimensions);
+    $(document).on('mouseup', this.onMouseUp);
+  },
+
+  stopMove() {
+    $(document).off('mousemove', this.updateDimensions);
+    $(document).off('mouseup', this.onMouseUp);
+    this.drawToCanvas();
+  },
+
+  onMouseDown(ev) {
+    ev.preventDefault();
+    this.setState({mousePosition: {
+      pageY: ev.pageY,
+      pageX: ev.pageX
+    }});
+    this.startMove();
+  },
+
+  onMouseUp(ev) {
+    ev.preventDefault();
+    this.stopMove();
+  },
+
+  startResize(direction, ev) {
+    ev.stopPropagation();
+    ev.preventDefault();
+    $(document).on('mousemove', this.updateSize);
+    $(document).on('mouseup', this.stopResize);
+    this.setState({
+      resizeDirection: direction,
+      mousePosition: {
+        pageY: ev.pageY,
+        pageX: ev.pageX
+      }
+    });
+  },
+
+  stopResize(ev) {
+    ev.stopPropagation();
+    ev.preventDefault();
+    $(document).off('mousemove', this.updateSize);
+    $(document).off('mouseup', this.stopResize);
+    this.drawToCanvas();
+  },
+
+  updateSize(ev) {
+    let yDiff = ev.pageY - this.state.mousePosition.pageY;
+    let xDiff = ev.pageX - this.state.mousePosition.pageX;
+    let $container = $(this.refs.cropContainer);
+
+    this.setState({
+      resizeDimensions: this.getNewDimensions($container, yDiff, xDiff),
+      mousePosition: {pageX: ev.pageX, pageY: ev.pageY}
+    });
+  },
+
+  // Normalize diff accross dimensions so that negative diffs
+  // are always making the cropper smaller and positive ones
+  // are making the cropper larger
+  getDiffNW(yDiff, xDiff) {
+    return ((yDiff - (yDiff * 2)) + (xDiff - (xDiff * 2))) / 2;
+  },
+
+  getDiffNE(yDiff, xDiff) {
+    return ((yDiff - (yDiff * 2)) + xDiff) / 2;
+  },
+
+  getDiffSW(yDiff, xDiff) {
+    return (yDiff + (xDiff - (xDiff * 2))) / 2;
+  },
+
+  getDiffSE(yDiff, xDiff) {
+    return (yDiff + xDiff) / 2;
+  },
+
+  getNewDimensions($container, yDiff, xDiff) {
+    let oldDimensions = this.state.resizeDimensions;
+    let resizeDirection = this.state.resizeDirection;
+    let diff = this['getDiff' + resizeDirection.toUpperCase()](yDiff, xDiff);
+
+    let height = $container.height() - oldDimensions.top;
+    let width = $container.width() - oldDimensions.left;
+
+    // Depending on the direction, we update different dimensions:
+    // nw: size, top, left
+    // ne: size, top
+    // sw: size, left
+    // se: size
+    let newDimensions = {size: oldDimensions.size + diff};
+    if (resizeDirection === 'nw' || resizeDirection === 'ne') {
+      newDimensions.top = oldDimensions.top - diff;
+      height = $container.height() - newDimensions.top;
+    }
+
+    if (resizeDirection === 'nw' || resizeDirection === 'sw') {
+      newDimensions.left = oldDimensions.left - diff;
+      width = $container.width() - newDimensions.left;
+    }
+
+    if (newDimensions.top < 0) {
+      newDimensions.size = newDimensions.size + newDimensions.top;
+      // Only update left if it's something we edit in this direction
+      if (newDimensions.hasOwnProperty('left')) {
+        newDimensions.left = newDimensions.left - newDimensions.top;
+      }
+      newDimensions.top = 0;
+    }
+
+    if (newDimensions.left < 0) {
+      newDimensions.size = newDimensions.size + newDimensions.left;
+      // Only update top if it's something we edit in this direction
+      if (newDimensions.hasOwnProperty('top')) {
+        newDimensions.top = newDimensions.top - newDimensions.left;
+      }
+      newDimensions.left = 0;
+    }
+
+    let maxSize = Math.min(width, height);
+    if (newDimensions.size > maxSize) {
+      if (newDimensions.hasOwnProperty('top')) {
+        newDimensions.top = newDimensions.top + newDimensions.size - maxSize;
+      }
+      if (newDimensions.hasOwnProperty('left')) {
+        newDimensions.left = newDimensions.left + newDimensions.size - maxSize;
+      }
+      newDimensions.size = maxSize;
+    } else if (newDimensions.size < this.MIN_DIMENSION) {
+      if (newDimensions.hasOwnProperty('top')) {
+        newDimensions.top = newDimensions.top + newDimensions.size - this.MIN_DIMENSION;
+      }
+      if (newDimensions.hasOwnProperty('left')) {
+        newDimensions.left = newDimensions.left + newDimensions.size - this.MIN_DIMENSION;
+      }
+      newDimensions.size = this.MIN_DIMENSION;
+    }
+    return Object.assign({}, oldDimensions, newDimensions);
+  },
+
+  handleError(msg) {
+    AlertActions.addAlert({
+      message: t(msg),
+      type: 'error'
+    });
+  },
+
+  validateImage() {
+    let img = this.refs.image;
+    if (img.naturalWidth < this.MIN_DIMENSION ||
+          img.naturalHeight < this.MIN_DIMENSION) {
+      return ('Please upload an image larger than ' +
+              this.MIN_DIMENSION + 'px by ' + this.MIN_DIMENSION + 'px.');
+    }
+    if (img.naturalWidth > this.MAX_DIMENSION ||
+          img.naturalHeight > this.MAX_DIMENSION) {
+      return ('Please upload an image smaller than ' +
+              this.MAX_DIMENSION + 'px by ' + this.MAX_DIMENSION + 'px.');
+    }
+  },
+
+  onLoad(ev) {
+    let error = this.validateImage();
+    if (error) {
+      window.URL.revokeObjectURL(this.state.objectURL);
+      this.setState({objectURL: null});
+      this.handleError(error);
+      return;
+    }
+    let $img = $(this.refs.image);
+    let dimension = Math.min($img.height(), $img.width());
+    this.setState({
+      resizeDimensions: Object.assign({size: dimension, top: 0, left: 0})
+    }, this.drawToCanvas);
+  },
+
+  drawToCanvas() {
+    let canvas = this.refs.canvas;
+    let resizeDimensions = this.state.resizeDimensions;
+    let img = this.refs.image;
+    // Calculate difference between natural dimensions and rendered dimensions
+    let imgRatio = (img.naturalHeight / $(img).height() +
+                    img.naturalWidth / $(img).width()) / 2;
+    canvas.width = resizeDimensions.size * imgRatio;
+    canvas.height = resizeDimensions.size * imgRatio;
+    canvas.getContext('2d').drawImage(img,
+                                      resizeDimensions.left * imgRatio,
+                                      resizeDimensions.top * imgRatio,
+                                      resizeDimensions.size * imgRatio,
+                                      resizeDimensions.size * imgRatio,
+                                      0, 0,
+                                      resizeDimensions.size * imgRatio,
+                                      resizeDimensions.size * imgRatio);
+    this.finishCrop();
+  },
+
+  finishCrop() {
+    let canvas = this.refs.canvas;
+    this.props.updateDataUrlState({dataUrl: canvas.toDataURL()});
+  },
+
+  getImgSrc() {
+    let uuid = this.props.user.avatar.avatarUuid;
+    let photoUrl = uuid && '/avatar/' + uuid + '/';
+    return this.props.savedDataUrl || this.state.objectURL || photoUrl;
+  },
+
+  onImgDrag(ev) {
+    ev.preventDefault();
+  },
+
+  renderImageCrop() {
+    let src = this.getImgSrc();
+    if (!src) {
+      return null;
+    }
+    let resizeDimensions = this.state.resizeDimensions;
+    let style = {
+      top: resizeDimensions.top,
+      left: resizeDimensions.left,
+      width: resizeDimensions.size,
+      height: resizeDimensions.size
+    };
+    return (
+      <div className="image-cropper">
+        <div className="crop-container" ref="cropContainer">
+          <div className="image-container">
+            <img className="preview" ref="image" src={src}
+                 onLoad={this.onLoad} onDragStart={this.onImgDrag}/>
+          </div>
+          <div className="cropper" style={style} onMouseDown={this.onMouseDown}>
+            <div onMouseDown={this.startResize.bind(this, 'nw')} className="resizer nw"></div>
+            <div onMouseDown={this.startResize.bind(this, 'ne')} className="resizer ne"></div>
+            <div onMouseDown={this.startResize.bind(this, 'se')} className="resizer se"></div>
+            <div onMouseDown={this.startResize.bind(this, 'sw')} className="resizer sw"></div>
+          </div>
+        </div>
+      </div>
+    );
+  },
+
+  uploadClick(ev) {
+    ev.preventDefault();
+    this.refs.file.click();
+  },
+
+  renderCanvas() {
+    if (!this.getImgSrc()) {
+      return null;
+    }
+    return (
+      <div className="canvas-container">
+        <canvas ref="canvas"></canvas>
+      </div>
+    );
+  },
+
+  render() {
+    let src = this.getImgSrc();
+    let style = {
+        position : 'absolute'
+    };
+
+    return (
+      <div>
+        {!src &&
+        <div className="image-well well blankslate">
+          <p><a onClick={this.uploadClick}><strong>{t('Upload a photo')}</strong></a>{t(' to get started.')}</p>
+        </div>}
+        {this.renderImageCrop()}
+        {this.renderCanvas()}
+        <div className="form-group">
+          {src && <a onClick={this.uploadClick}>{t('Change Photo')}</a>}
+          <input ref="file" type="file" accept="image/*" onChange={this.onChange} style={style}/>
+        </div>
+      </div>
+    );
+  }
+});
+
+export default AvatarCropper;

+ 48 - 0
src/sentry/static/sentry/app/components/avatarRadio.jsx

@@ -0,0 +1,48 @@
+import React from 'react';
+
+import {t} from '../locale';
+
+
+const AvatarRadio = React.createClass({
+  propTypes: {
+    user: React.PropTypes.object.isRequired,
+    updateUser: React.PropTypes.func.isRequired
+  },
+
+  OPTIONS: {
+    upload: 'Upload a Photo',
+    gravatar: 'Use Gravatar',
+    letter_avatar: 'Use my initials'
+  },
+
+  onChange(ev) {
+    let avatar = Object.assign({}, this.props.user.avatar, {avatarType: ev.target.value});
+    this.props.user.avatar = avatar;
+    this.props.updateUser(this.props.user);
+  },
+
+  render() {
+    let radios = [];
+    for (let opt in this.OPTIONS) {
+      radios.push(
+        <li className="radio" key={opt}>
+          <label>
+            <input type="radio" name="avatar-type" value={opt} onChange={this.onChange}
+                   checked={this.props.user.avatar.avatarType === opt}/>
+            {this.OPTIONS[opt]}
+          </label>
+        </li>
+      );
+    }
+    return (
+      <div>
+        <legend>{t('Avatar Type')}</legend>
+        <ul className="radio-inputs">
+          {radios}
+        </ul>
+      </div>
+    );
+  }
+});
+
+export default AvatarRadio;

+ 5 - 399
src/sentry/static/sentry/app/components/avatarSettings.jsx

@@ -2,407 +2,13 @@ import React from 'react';
 
 import AlertActions from '../actions/alertActions';
 import ApiMixin from '../mixins/apiMixin';
+import AvatarCropper from '../components/avatarCropper';
+import AvatarRadio from '../components/avatarRadio';
 import LoadingError from '../components/loadingError';
 import LoadingIndicator from '../components/loadingIndicator';
 import {t} from '../locale';
 
 
-const AvatarCropper = React.createClass({
-  propTypes: {
-    user: React.PropTypes.object.isRequired,
-    updateDataUrlState: React.PropTypes.func.isRequired,
-    savedDataUrl: React.PropTypes.string
-  },
-
-  mixins: [ApiMixin],
-
-  getInitialState() {
-    return {
-      error: null,
-      mousePosition: {
-        pageX: null,
-        pageY: null
-      },
-      resizeDimensions: {
-        top: 0,
-        left: 0,
-        size: 0
-      }
-    };
-  },
-
-  MIN_DIMENSION: 256,
-
-  MAX_DIMENSION: 1024,
-
-  onChange(ev) {
-    let file = ev.target.files[0];
-
-    if (!/^image\//.test(file.type))
-      return void this.setState({error: 'That is not a supported file type.'});
-
-    this.state.objectURL && window.URL.revokeObjectURL(this.state.objectURL);
-    this.setState({
-      file: file,
-      objectURL: window.URL.createObjectURL(file)
-    }, () => {
-      this.props.updateDataUrlState({savedDataUrl: null});
-    });
-  },
-
-  componentWIllUnmount() {
-    this.state.objectURL && window.URL.revokeObjectURL(this.state.objectURL);
-  },
-
-  updateDimensions(ev) {
-    let $container = $(this.refs.cropContainer);
-    let resizeDimensions = this.state.resizeDimensions;
-    let pageY = ev.pageY;
-    let pageX = ev.pageX;
-    let top = resizeDimensions.top + (pageY - this.state.mousePosition.pageY);
-    let left = resizeDimensions.left + (pageX - this.state.mousePosition.pageX);
-    if (top < 0) {
-      top = 0;
-      pageY = this.state.mousePosition.pageY;
-    } else if (top + resizeDimensions.size > $container.height()) {
-      top = $container.height() - resizeDimensions.size;
-      pageY = this.state.mousePosition.pageY;
-    }
-    if (left < 0) {
-      left = 0;
-      pageX = this.state.mousePosition.pageX;
-    } else if (left + resizeDimensions.size > $container.width()) {
-      left = $container.width() - resizeDimensions.size;
-      pageX = this.state.mousePosition.pageX;
-    }
-    this.setState({
-      resizeDimensions: Object.assign({}, resizeDimensions, {top, left}),
-      mousePosition: {pageX, pageY}
-    });
-  },
-
-  startMove() {
-    $(document).on('mousemove', this.updateDimensions);
-    $(document).on('mouseup', this.onMouseUp);
-  },
-
-  stopMove() {
-    $(document).off('mousemove', this.updateDimensions);
-    $(document).off('mouseup', this.onMouseUp);
-    this.drawToCanvas();
-  },
-
-  onMouseDown(ev) {
-    ev.preventDefault();
-    this.setState({mousePosition: {
-      pageY: ev.pageY,
-      pageX: ev.pageX
-    }});
-    this.startMove();
-  },
-
-  onMouseUp(ev) {
-    ev.preventDefault();
-    this.stopMove();
-  },
-
-  startResize(direction, ev) {
-    ev.stopPropagation();
-    ev.preventDefault();
-    $(document).on('mousemove', this.updateSize);
-    $(document).on('mouseup', this.stopResize);
-    this.setState({
-      resizeDirection: direction,
-      mousePosition: {
-        pageY: ev.pageY,
-        pageX: ev.pageX
-      }
-    });
-  },
-
-  stopResize(ev) {
-    ev.stopPropagation();
-    ev.preventDefault();
-    $(document).off('mousemove', this.updateSize);
-    $(document).off('mouseup', this.stopResize);
-    this.drawToCanvas();
-  },
-
-  updateSize(ev) {
-    let yDiff = ev.pageY - this.state.mousePosition.pageY;
-    let xDiff = ev.pageX - this.state.mousePosition.pageX;
-    let $container = $(this.refs.cropContainer);
-
-    this.setState({
-      resizeDimensions: this.getNewDimensions($container, yDiff, xDiff),
-      mousePosition: {pageX: ev.pageX, pageY: ev.pageY}
-    });
-  },
-
-  // Normalize diff accross dimensions so that negative diffs
-  // are always making the cropper smaller and positive ones
-  // are making the cropper larger
-  getDiffNW(yDiff, xDiff) {
-    return ((yDiff - (yDiff * 2)) + (xDiff - (xDiff * 2))) / 2;
-  },
-
-  getDiffNE(yDiff, xDiff) {
-    return ((yDiff - (yDiff * 2)) + xDiff) / 2;
-  },
-
-  getDiffSW(yDiff, xDiff) {
-    return (yDiff + (xDiff - (xDiff * 2))) / 2;
-  },
-
-  getDiffSE(yDiff, xDiff) {
-    return (yDiff + xDiff) / 2;
-  },
-
-  getNewDimensions($container, yDiff, xDiff) {
-    let oldDimensions = this.state.resizeDimensions;
-    let resizeDirection = this.state.resizeDirection;
-    let diff = this['getDiff' + resizeDirection.toUpperCase()](yDiff, xDiff);
-
-    let height = $container.height() - oldDimensions.top;
-    let width = $container.width() - oldDimensions.left;
-
-    // Depending on the direction, we update different dimensions:
-    // nw: size, top, left
-    // ne: size, top
-    // sw: size, left
-    // se: size
-    let newDimensions = {size: oldDimensions.size + diff};
-    if (resizeDirection === 'nw' || resizeDirection === 'ne') {
-      newDimensions.top = oldDimensions.top - diff;
-      height = $container.height() - newDimensions.top;
-    }
-
-    if (resizeDirection === 'nw' || resizeDirection === 'sw') {
-      newDimensions.left = oldDimensions.left - diff;
-      width = $container.width() - newDimensions.left;
-    }
-
-    if (newDimensions.top < 0) {
-      newDimensions.size = newDimensions.size + newDimensions.top;
-      // Only update left if it's something we edit in this direction
-      if (newDimensions.hasOwnProperty('left')) {
-        newDimensions.left = newDimensions.left - newDimensions.top;
-      }
-      newDimensions.top = 0;
-    }
-
-    if (newDimensions.left < 0) {
-      newDimensions.size = newDimensions.size + newDimensions.left;
-      // Only update top if it's something we edit in this direction
-      if (newDimensions.hasOwnProperty('top')) {
-        newDimensions.top = newDimensions.top - newDimensions.left;
-      }
-      newDimensions.left = 0;
-    }
-
-    let maxSize = Math.min(width, height);
-    if (newDimensions.size > maxSize) {
-      if (newDimensions.hasOwnProperty('top')) {
-        newDimensions.top = newDimensions.top + newDimensions.size - maxSize;
-      }
-      if (newDimensions.hasOwnProperty('left')) {
-        newDimensions.left = newDimensions.left + newDimensions.size - maxSize;
-      }
-      newDimensions.size = maxSize;
-    } else if (newDimensions.size < this.MIN_DIMENSION) {
-      if (newDimensions.hasOwnProperty('top')) {
-        newDimensions.top = newDimensions.top + newDimensions.size - this.MIN_DIMENSION;
-      }
-      if (newDimensions.hasOwnProperty('left')) {
-        newDimensions.left = newDimensions.left + newDimensions.size - this.MIN_DIMENSION;
-      }
-      newDimensions.size = this.MIN_DIMENSION;
-    }
-    return Object.assign({}, oldDimensions, newDimensions);
-  },
-
-  handleError(msg) {
-    AlertActions.addAlert({
-      message: t(msg),
-      type: 'error'
-    });
-  },
-
-  validateImage() {
-    let img = this.refs.image;
-    if (img.naturalWidth < this.MIN_DIMENSION ||
-          img.naturalHeight < this.MIN_DIMENSION) {
-      return ('Please upload an image larger than ' +
-              this.MIN_DIMENSION + 'px by ' + this.MIN_DIMENSION + 'px.');
-    }
-    if (img.naturalWidth > this.MAX_DIMENSION ||
-          img.naturalHeight > this.MAX_DIMENSION) {
-      return ('Please upload an image smaller than ' +
-              this.MAX_DIMENSION + 'px by ' + this.MAX_DIMENSION + 'px.');
-    }
-  },
-
-  onLoad(ev) {
-    let error = this.validateImage();
-    if (error) {
-      window.URL.revokeObjectURL(this.state.objectURL);
-      this.setState({objectURL: null});
-      this.handleError(error);
-      return;
-    }
-    let $img = $(this.refs.image);
-    let dimension = Math.min($img.height(), $img.width());
-    this.setState({
-      resizeDimensions: Object.assign({size: dimension, top: 0, left: 0})
-    }, this.drawToCanvas);
-  },
-
-  drawToCanvas() {
-    let canvas = this.refs.canvas;
-    let resizeDimensions = this.state.resizeDimensions;
-    let img = this.refs.image;
-    // Calculate difference between natural dimensions and rendered dimensions
-    let imgRatio = (img.naturalHeight / $(img).height() +
-                    img.naturalWidth / $(img).width()) / 2;
-    canvas.width = resizeDimensions.size * imgRatio;
-    canvas.height = resizeDimensions.size * imgRatio;
-    canvas.getContext('2d').drawImage(img,
-                                      resizeDimensions.left * imgRatio,
-                                      resizeDimensions.top * imgRatio,
-                                      resizeDimensions.size * imgRatio,
-                                      resizeDimensions.size * imgRatio,
-                                      0, 0,
-                                      resizeDimensions.size * imgRatio,
-                                      resizeDimensions.size * imgRatio);
-    this.finishCrop();
-  },
-
-  finishCrop() {
-    let canvas = this.refs.canvas;
-    this.props.updateDataUrlState({dataUrl: canvas.toDataURL()});
-  },
-
-  getImgSrc() {
-    let uuid = this.props.user.avatar.avatarUuid;
-    let photoUrl = uuid && '/avatar/' + uuid + '/';
-    return this.props.savedDataUrl || this.state.objectURL || photoUrl;
-  },
-
-  onImgDrag(ev) {
-    ev.preventDefault();
-  },
-
-  renderImageCrop() {
-    let src = this.getImgSrc();
-    if (!src) {
-      return null;
-    }
-    let resizeDimensions = this.state.resizeDimensions;
-    let style = {
-      top: resizeDimensions.top,
-      left: resizeDimensions.left,
-      width: resizeDimensions.size,
-      height: resizeDimensions.size
-    };
-    return (
-      <div className="image-cropper">
-        <div className="crop-container" ref="cropContainer">
-          <div className="image-container">
-            <img className="preview" ref="image" src={src}
-                 onLoad={this.onLoad} onDragStart={this.onImgDrag}/>
-          </div>
-          <div className="cropper" style={style} onMouseDown={this.onMouseDown}>
-            <div onMouseDown={this.startResize.bind(this, 'nw')} className="resizer nw"></div>
-            <div onMouseDown={this.startResize.bind(this, 'ne')} className="resizer ne"></div>
-            <div onMouseDown={this.startResize.bind(this, 'se')} className="resizer se"></div>
-            <div onMouseDown={this.startResize.bind(this, 'sw')} className="resizer sw"></div>
-          </div>
-        </div>
-      </div>
-    );
-  },
-
-  uploadClick(ev) {
-    ev.preventDefault();
-    this.refs.file.click();
-  },
-
-  renderCanvas() {
-    if (!this.getImgSrc()) {
-      return null;
-    }
-    return (
-      <div className="canvas-container">
-        <canvas ref="canvas"></canvas>
-      </div>
-    );
-  },
-
-  render() {
-    let src = this.getImgSrc();
-    let style = {
-        position : "absolute"
-    };
-
-    return (
-      <div>
-        {!src &&
-        <div className="image-well well blankslate">
-          <p><a onClick={this.uploadClick}><strong>{t('Upload a photo')}</strong></a>{t(' to get started.')}</p>
-        </div>}
-        {this.renderImageCrop()}
-        {this.renderCanvas()}
-        <div className="form-group">
-          {src && <a onClick={this.uploadClick}>{t('Change Photo')}</a>}
-          <input ref="file" type="file" accept="image/*" onChange={this.onChange} style={style}/>
-        </div>
-      </div>
-    );
-  }
-});
-
-const AvatarRadio = React.createClass({
-  propTypes: {
-    user: React.PropTypes.object.isRequired,
-    updateUser: React.PropTypes.func.isRequired
-  },
-
-  OPTIONS: {
-    upload: 'Upload a Photo',
-    gravatar: 'Use Gravatar',
-    letter_avatar: 'Use my initials'
-  },
-
-  onChange(ev) {
-    let avatar = Object.assign({}, this.props.user.avatar, {avatarType: ev.target.value});
-    this.props.user.avatar = avatar;
-    this.props.updateUser(this.props.user);
-  },
-
-  render() {
-    let radios = [];
-    for (let opt in this.OPTIONS) {
-      radios.push(
-        <li className="radio" key={opt}>
-          <label>
-            <input type="radio" name="avatar-type" value={opt} onChange={this.onChange}
-                   checked={this.props.user.avatar.avatarType === opt}/>
-            {this.OPTIONS[opt]}
-          </label>
-        </li>
-      );
-    }
-    return (
-      <div>
-        <legend>{t('Avatar Type')}</legend>
-        <ul className="radio-inputs">
-          {radios}
-        </ul>
-      </div>
-    );
-  }
-});
-
 const AvatarSettings = React.createClass({
   propTypes: {
     userId: React.PropTypes.number
@@ -485,17 +91,17 @@ const AvatarSettings = React.createClass({
       return <LoadingError />;
     }
     if (!this.state.user) {
-      return <LoadingIndicator/>;
+      return <LoadingIndicator />;
     }
 
-    let gravatarMessage = <div className="well">Gravatars are managed through <a href="http://gravatar.com" target="_blank">Gravatar.com</a></div>;
+    let gravatarMessage = <div className="well">{t('Gravatars are managed through ')}<a href="http://gravatar.com" target="_blank">Gravatar.com</a></div>;
 
     return (
       <div>
         <form>
           <AvatarRadio user={this.state.user} updateUser={this.updateUserState}/>
 
-          {this.state.user.avatar.avatarType ==='gravatar' && gravatarMessage }
+          {this.state.user.avatar.avatarType === 'gravatar' && gravatarMessage }
 
           {this.state.user.avatar.avatarType === 'upload' &&
             <AvatarCropper {...this.props} user={this.state.user} savedDataUrl={this.state.savedDataUrl}

+ 94 - 0
tests/js/spec/components/avatarCropper.spec.jsx

@@ -0,0 +1,94 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+import AvatarCropper from 'app/components/avatarCropper';
+
+
+describe('AvatarCropper', function() {
+  const USER = {
+    email: 'a@example.com',
+      avatar: {
+        avatarType: 'gravatar',
+        avatarUuid: '2d641b5d-8c74-44de-9cb6-fbd54701b35e'
+      }
+  };
+
+  describe('getDiffNW', function() {
+    it(('should return a negative diff when yDiff and xDiff ' +
+        'are positive (cropper is getting smaller)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffNW(4, 5);
+      expect(diff).to.equal(-4.5);
+    });
+
+    it('should return a positive diff when yDiff and xDiff ' +
+       'are negative (cropper is getting bigger)', function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffNW(-4, -5);
+      expect(diff).to.equal(4.5);
+    });
+  });
+
+  describe('getDiffNE', function() {
+    it(('should return a positive diff when yDiff is negative and ' +
+        'xDiff is positive (cropper is getting bigger)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffNE(-4, 5);
+      expect(diff).to.equal(4.5);
+    });
+
+    it(('should return a negative diff when yDiff is positive and ' +
+        'xDiff is negative (cropper is getting smaller)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffNE(4, -5);
+      expect(diff).to.equal(-4.5);
+    });
+  });
+
+  describe('getDiffSE', function() {
+    it(('should return a positive diff when yDiff and ' +
+        'xDiff are positive (cropper is getting bigger)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffSE(4, 5);
+      expect(diff).to.equal(4.5);
+    });
+
+    it(('should return a negative diff when yDiff and ' +
+        'xDiff are negative (cropper is getting smaller)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffSE(-4, -5);
+      expect(diff).to.equal(-4.5);
+    });
+  });
+
+  describe('getDiffSW', function() {
+    it(('should return a positive diff when yDiff is positive and ' +
+        'xDiff is negative (cropper is getting bigger)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffSW(4, -5);
+      expect(diff).to.equal(4.5);
+    });
+
+    it(('should return a negative diff when yDiff is negative and' +
+        'xDiff is positive (cropper is getting smaller)'), function() {
+      let cropper = shallow(
+        <AvatarCropper user={USER} updateDataUrlState={function() {}}/>
+      ).instance();
+      let diff = cropper.getDiffSW(-4, 5);
+      expect(diff).to.equal(-4.5);
+    });
+  });
+});