-
-
-
367 votes
+
+
+ {/* TODO: Make these share links work */}
+
);
}
+
+ render() {
+ let voteContent;
+
+ if(this.state.didVote){
+ voteContent = this.renderDidVote();
+ } else {
+ voteContent = this.renderVoteAsk();
+ }
+
+ return (
+
+ { voteContent }
+
+ );
+ }
}
diff --git a/source/js/buyers-guide/components/creep-vote/creep-vote.scss b/source/js/buyers-guide/components/creep-vote/creep-vote.scss
index 539e4af9ff..6b6dd85d3d 100644
--- a/source/js/buyers-guide/components/creep-vote/creep-vote.scss
+++ b/source/js/buyers-guide/components/creep-vote/creep-vote.scss
@@ -1,4 +1,67 @@
+$radio-button-radius: 50px;
+$btn-shadow-width: 3px;
+
.creep-vote {
- border-top: 1px solid #c8c8ca;
border-bottom: 1px solid #c8c8ca;
+ border-top: 1px solid #c8c8ca;
+
+ button[disabled] {
+ color: #bbb;
+
+ &.btn-ghost:hover {
+ cursor: auto;
+ color: inherit;
+ background: inherit;
+ }
+ }
+
+ .btn-group {
+ display: flex;
+ justify-content: center;
+
+ .btn {
+ border: 1px solid black;
+ border-bottom-width: $btn-shadow-width;
+ color: black;
+ padding: 0.5em 1.5em;
+ text-transform: initial;
+ transition: all 0.1s linear;
+ }
+
+ input { display: none; }
+
+ input:checked + span {
+ background-color: #5cccff;
+ }
+
+ input:-moz-ui-invalid + span {
+ border-color: red;
+ }
+
+ label:first-of-type span {
+ border-radius: $radio-button-radius 0 0 $radio-button-radius;
+ }
+
+ label:last-of-type span {
+ border-radius: 0 $radio-button-radius $radio-button-radius 0;
+ }
+
+ img {
+ height: 1em;
+ }
+ }
+
+ .help-text {
+ font-style: italic;
+ }
+
+ a.share-results {
+ text-decoration: underline;
+ color: black;
+
+ &:hover {
+ color: #4383bf;
+ font-weight: bold;
+ }
+ }
}
diff --git a/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.jsx b/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.jsx
new file mode 100644
index 0000000000..62f0dcf92b
--- /dev/null
+++ b/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+export default class CreepChart extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = this.getInitialState();
+ }
+
+ getInitialState() {
+ let values = this.props.values;
+ let data = [
+ {c: `no-creep`, label: `Not creepy`, value: values[0], offset: 0},
+ {c: `little-creep`, label: `A little creepy`, value: values[1], offset: 225},
+ {c: `somewhat-creep`, label: `Somewhat creepy`, value: values[2], offset: 475},
+ {c: `very-creep`, label: `Very creepy`, value: values[3], offset: 725},
+ {c: `super-creep`, label: `Super creepy`, value: values[4], offset: 975}
+ ];
+ let sum = data.reduce((tally, v) => tally + v.value, 0);
+
+ return {
+ totalCreepiness: sum,
+ creepinessData: data
+ };
+ }
+
+ render() {
+ return (
+
+
+
+ {
+ this.state.creepinessData.map((data,index) => {
+ let percent = Math.round(100 * data.value / this.state.totalCreepiness);
+ let voteColumn = this.props.userVoteGroup === index ? `your-vote` : ``;
+
+ return (
+
+
+
+ {data.label}
+
+ |
+ {percent}% |
+
+ );
+ })
+ }
+
+
+
+
Not creepy
+
Super creepy
+
+
+ );
+ }
+}
diff --git a/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.scss b/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.scss
new file mode 100644
index 0000000000..d6ced7a7f2
--- /dev/null
+++ b/source/js/buyers-guide/components/creepiness-chart/creepiness-chart.scss
@@ -0,0 +1,116 @@
+#creepiness-score {
+ display: flex;
+ // There's probably a better way to do this.
+ border-image: url("/_images/buyers-guide/gradient-bar.svg") 7/0px 6px 6px;
+
+ tbody {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ width: 100%;
+ }
+
+ th {
+ order: 2;
+ margin: 0 1px;
+ text-align: center;
+ align-self: flex-end;
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+ flex-direction: column;
+ position: relative;
+ min-height: 85px;
+ }
+
+ .bar,
+ td {
+ position: relative;
+ bottom: -40px;
+ }
+
+ .creepiness {
+ font-weight: bold;
+ font-size: 14px;
+ }
+ //For some nice animations later on
+ .creepiness,
+ .bar {
+ transition: all 2.5s ease;
+ }
+
+ .creep-label {
+ //Make sure screen readers can see the label
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute;
+ }
+
+ //Height of this is set inline via JSX
+ .bar {
+ background-color: $light-gray;
+ }
+
+ //Show a different creep-face in each bar of the graph
+ .creep-face {
+ background-image: url("/_images/buyers-guide/faces/sprite-resized-64-colors.png");
+ background-position: center 0;
+ background-size: 35px auto;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ height: 35px;
+ margin-bottom: 10px;
+ position: relative;
+ }
+
+ .little-creep .creep-face {
+ background-position: center 25.6%;
+ }
+
+ .somewhat-creep .creep-face {
+ background-position: center 51.25%;
+ }
+
+ .very-creep .creep-face {
+ background-position: center 76.9%;
+ }
+
+ .super-creep .creep-face {
+ background-position: center 100%;
+ }
+
+ //Style the graph bar representing the range the user voted
+ .your-vote {
+ &.no-creep .bar {
+ background-color: #1808f2;
+ }
+
+ &.little-creep .bar {
+ background-color: #4a17d4;
+ }
+
+ &.somewhat-creep .bar {
+ background-color: #7f28b7;
+ }
+
+ &.very-creep .bar {
+ background-color: #b0379b;
+ }
+
+ &.super-creep .bar {
+ background-color: #e4487d;
+ }
+ }
+
+ td {
+ order: 1;
+ text-align: center;
+ }
+
+ tr {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ flex-grow: 1;
+ margin: 0 1px;
+ }
+}
diff --git a/source/js/buyers-guide/components/creepometer/creepometer.jsx b/source/js/buyers-guide/components/creepometer/creepometer.jsx
index eb55c7004e..389ed2bd21 100644
--- a/source/js/buyers-guide/components/creepometer/creepometer.jsx
+++ b/source/js/buyers-guide/components/creepometer/creepometer.jsx
@@ -4,93 +4,129 @@ export default class Creepometer extends React.Component {
constructor(props) {
super(props);
+ this.faceCount = 40; // Number of face frames
+ this.faceHeight = 70; // pixel height for one frame
+ this.framePath = `/_images/buyers-guide/faces/`;
+
this.state = {
- isHandleGrabbed: false,
- handleOffset: 0
+ dragging: false,
+ percentage: 50,
+ value: 50
};
- this.handleWidth = 70; // px
- this.faceCount = 40; // Number of face frames
- this.encodedStepCount = 100; // Upper range of values to be recorded
- this.framePath = `/_images/buyers-guide/faces/`;
+ this.setupDocumentListeners();
+ }
- this.slideStart = this.slideStart.bind(this);
- this.slideMove = this.slideMove.bind(this);
- this.slideStop = this.slideStop.bind(this);
+ setupDocumentListeners() {
+ this.moveListener = (function(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.slideMove(evt);
+ }).bind(this);
+
+ this.releaseListener = (function(evt) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.slideReleased(evt);
+ this.removeDocumentListeners();
+ }).bind(this);
+ }
- this.setSliderRef = element => {
- this.sliderElement = element;
- };
+ addDocumentListeners() {
+ document.addEventListener('mousemove', this.moveListener, true);
+ document.addEventListener('touchmove', this.moveListener, true);
+ document.addEventListener('mouseup', this.releaseListener, true);
+ document.addEventListener('touchstart', this.releaseListener, true);
}
- componentDidMount() {
- // Slight delay because Firefox is too dang fast
- setTimeout(() => {
- // Set initial position
- this.setState({
- handleOffset: Math.floor(this.props.initialValue / this.encodedStepCount * this.sliderElement.scrollWidth),
- encodedValue: this.props.initialValue
- });
- }, 100);
+ removeDocumentListeners() {
+ document.removeEventListener('mousemove', this.moveListener, true);
+ document.removeEventListener('touchmove', this.moveListener, true);
+ document.removeEventListener('mouseup', this.releaseListener, true);
+ document.removeEventListener('touchstart', this.releaseListener, true);
}
slideStart(e) {
- if (e.nativeEvent.target.className === `handle`) {
- this.setState({
- isHandleGrabbed: true
- });
- }
+ this.setState({
+ parentBBox: this.sliderElement.getBoundingClientRect(),
+ dragging: true
+ });
+ // The "move" and "release" events have to be handled at
+ // the document level, because the events can be generated
+ // "nowhere near the React-managed DOM node".
+ this.addDocumentListeners();
+ }
+
+ slideReleased() {
+ this.setState({
+ dragging: false
+ });
}
slideMove(e) {
- if (this.state.isHandleGrabbed) {
- let clientX, sliderLeftEdgeX, offset;
+ if (this.state.dragging) {
+ let x = e.clientX, bbox = this.state.parentBBox, percentage, value;
+
+ if (e.touches){
+ x = e.touches[0].clientX;
+ }
- if (e.nativeEvent.type === `touchmove`){
- clientX = e.nativeEvent.touches[0].pageX;
- } else {
- clientX = e.nativeEvent.clientX;
+ // cap the position:
+ if (x > bbox.right) {
+ x = bbox.right;
+ } else if (x < bbox.left) {
+ x = bbox.left;
}
- sliderLeftEdgeX = this.sliderElement.getBoundingClientRect().left;
- offset = Math.floor(clientX - sliderLeftEdgeX);
+
+
+ // compute the handle offset
+ percentage = Math.round(100 * (x - bbox.left) / bbox.width);
+ value = percentage ? percentage : 1;
this.setState({
- handleOffset: offset,
- encodedValue: Math.floor(offset / this.sliderElement.scrollWidth * this.encodedStepCount)
+ percentage,
+ value
+ }, () => {
+ if (this.props.onChange) {
+ this.props.onChange(value);
+ }
});
}
}
- slideStop() {
- this.setState({
- isHandleGrabbed: false
- });
- }
-
render() {
- let handleX = this.state.handleOffset - this.handleWidth / 2;
- let frameChoice = Math.floor(this.faceCount / 2);
+ let frameOffset = Math.round(this.state.percentage * (this.faceCount-1)/100);
- // Don't let handle overflow slider's left side
- handleX = handleX < 0 ? 0 : handleX;
-
- if (this.sliderElement) {
- // Don't let handle overflow slider's right side
- if (handleX > this.sliderElement.scrollWidth - this.handleWidth) {
- handleX = this.sliderElement.scrollWidth - this.handleWidth;
- }
+ let trackheadOpts = {
+ style: {
+ left: `${this.state.value}%`
+ },
+ };
- frameChoice = Math.floor(handleX / this.sliderElement.scrollWidth * this.faceCount) + 1;
- }
+ let faceOpts = {
+ style: {
+ background: `url("${this.framePath}sprite-resized-64-colors.png"), #f2b946`,
+ backgroundSize: `70px`,
+ backgroundPositionX: 0,
+ backgroundPositionY: `-${frameOffset * this.faceHeight}px`,
+ backgroundRepeat: `no-repeat`,
+ },
+ };
- let pxOffset = this.handleWidth * this.faceCount - this.handleWidth * frameChoice; // offset position for spritesheet
+ let mouseOpts = {
+ onMouseDown: evt => this.slideStart(evt),
+ onTouchStart: evt => this.slideStart(evt),
+ };
return (
-
-
-
+
+
+
(this.sliderElement=e)}>
Not creepy
-
+
Super creepy
diff --git a/source/js/buyers-guide/components/creepometer/creepometer.scss b/source/js/buyers-guide/components/creepometer/creepometer.scss
index 8aaa2742cd..67529679d1 100644
--- a/source/js/buyers-guide/components/creepometer/creepometer.scss
+++ b/source/js/buyers-guide/components/creepometer/creepometer.scss
@@ -1,4 +1,6 @@
.creepometer {
+ margin-top: 70px;
+
.slider {
background: url("/_images/buyers-guide/gradient-bar.svg") no-repeat 0 center / contain;
position: relative;
@@ -24,13 +26,41 @@
right: 0;
}
- .handle {
- border-radius: 50%;
+ .trackhead {
cursor: grab;
- height: 70px;
- position: absolute;
- user-select: none;
- width: 70px;
+ position: relative;
+ display: inline-block;
z-index: 1;
+ margin-left: -35px;
+ top: -55px;
+ user-select: none;
+
+ .face {
+ position: relative;
+ border-radius: 50%;
+ height: 70px;
+ width: 70px;
+ z-index: 0;
+ }
+
+ .pip {
+ position: relative;
+ width: 70px;
+ height: 20px;
+ background: transparent;
+
+ &::after {
+ content: ' ';
+ background: white;
+ height: 12px;
+ width: 12px;
+ border-radius: 10px;
+ position: relative;
+ margin-left: 28px;
+ margin-top: 15px;
+ display: inline-block;
+ border: 1px solid grey;
+ }
+ }
}
}
diff --git a/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.jsx b/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.jsx
new file mode 100644
index 0000000000..d0085ef744
--- /dev/null
+++ b/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.jsx
@@ -0,0 +1,42 @@
+// TODO: Inject likely % in .bar and .likelyhood-words
+
+import React from 'react';
+
+export default class LikelyhoodChart extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render(){
+ let values = this.props.values;
+ let total = values[0] + values[1];
+ let perc = Math.round(100 * values[0]/total, 10);
+
+ return (
+
+
+
+
+
+ Likely
+ |
+
+
+ {100 - perc}% Likely to buy it
+ |
+
+
+
+ Not likely
+ |
+
+
+ {perc}% Not likely to buy it
+ |
+
+
+
+
+ );
+ }
+}
diff --git a/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.scss b/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.scss
new file mode 100644
index 0000000000..ff9df9984c
--- /dev/null
+++ b/source/js/buyers-guide/components/likelyhood-chart/likelyhood-chart.scss
@@ -0,0 +1,51 @@
+#likelyhood-score {
+ tbody {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ height: 4.5em;
+ justify-content: space-between;
+ }
+
+ .bar {
+ //For some nice animations later on, width set in JSX
+ transition: width 2.5s ease;
+ background-color: $light-gray;
+ position: absolute;
+ height: 25px;
+ }
+
+ tr {
+ position: relative;
+ display: block;
+ }
+
+ .likelyhood-words {
+ position: relative;
+ font-weight: bold;
+ font-variant: small-caps;
+ padding: 0 5px;
+ }
+
+ .likely-label {
+ //Make sure screen readers can see the label
+ clip: rect(1px, 1px, 1px, 1px);
+ position: absolute;
+ }
+
+ th::before {
+ content: '';
+ display: block;
+ height: 1em;
+ margin: 0 0.5em;
+ width: 1em;
+ }
+
+ .likely th::before {
+ background: url("/_images/buyers-guide/icon-thumb-up.svg") no-repeat;
+ }
+
+ .unlikely th::before {
+ background: url("/_images/buyers-guide/icon-thumb-down.svg") no-repeat;
+ }
+}
diff --git a/source/sass/buyers-guide/bg-main.scss b/source/sass/buyers-guide/bg-main.scss
index b88f3cc985..191c50e7d1 100644
--- a/source/sass/buyers-guide/bg-main.scss
+++ b/source/sass/buyers-guide/bg-main.scss
@@ -23,6 +23,8 @@ $bp-xl: #{map-get($grid-breakpoints, xl)}; // >= 1200px
@import '../../js/buyers-guide/components/creepometer/creepometer';
@import '../../js/buyers-guide/components/creep-vote/creep-vote';
@import '../../js/buyers-guide/components/primary-nav/primary-nav';
+@import '../../js/buyers-guide/components/creepiness-chart/creepiness-chart';
+@import '../../js/buyers-guide/components/likelyhood-chart/likelyhood-chart';
// Non-React Components