diff --git a/app/components/compare-reasons.jsx b/app/components/compare-reasons.jsx
deleted file mode 100644
index b0d4cf0f0..000000000
--- a/app/components/compare-reasons.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-// https://github.com/EnCiv/civil-pursuit/issues/77
-
-'use strict'
-import React, { useEffect, useState } from 'react'
-import { createUseStyles } from 'react-jss'
-import PairCompare from './pair-compare'
-import { H, Level } from 'react-accessible-headings'
-
-function CompareReasons(props) {
- const { pointList = [], side = '', onDone = () => {}, className, ...otherProps } = props
- const classes = useStyles()
- const [completedPoints, setCompletedPoints] = useState(new Set())
- const [percentDone, setPercentDone] = useState(0)
-
- useEffect(() => {
- if (completedPoints.size === pointList.length) {
- onDone({ valid: true, value: percentDone })
- } else {
- onDone({ valid: false, value: percentDone })
- }
- }, [completedPoints, percentDone])
-
- useEffect(() => {
- if (pointList.length === 0) setPercentDone(100)
- else {
- setPercentDone(Number(((completedPoints.size / pointList.length) * 100).toFixed(2)))
- }
- }, [completedPoints, pointList])
-
- const handlePairCompare = ({ valid, value }, idx) => {
- setCompletedPoints(prevPoints => {
- const updatedPoints = new Set(prevPoints)
- if (valid) {
- updatedPoints.add(idx)
- } else {
- updatedPoints.delete(idx)
- }
- return updatedPoints
- })
- }
-
- return (
-
- {pointList.map((headlinePoint, idx) => (
-
-
Please choose the most convincing explanation for...
-
{headlinePoint.subject}
-
- handlePairCompare(value, idx)}
- />
-
-
- ))}
-
- )
-}
-
-const useStyles = createUseStyles(theme => ({
- container: {
- fontFamily: theme.font.fontFamily,
- },
- headlinePoint: {
- borderTop: '0.0625rem solid #000000',
- marginBottom: '4rem',
- paddingTop: '2rem',
- '&:first-child': {
- borderTop: 'none',
- },
- },
- headlineTitle: {
- fontWeight: '600',
- fontSize: '1.5rem',
- lineHeight: '2rem',
- },
- headlineSubject: {
- fontWeight: '300',
- fontSize: '2.25rem',
- lineHeight: '2.9375rem',
- },
- pairCompare: {
- marginTop: '1rem',
- },
-}))
-
-export default CompareReasons
diff --git a/app/components/deliberation-context.js b/app/components/deliberation-context.js
index 61e6a393d..b58f3ad81 100644
--- a/app/components/deliberation-context.js
+++ b/app/components/deliberation-context.js
@@ -1,5 +1,6 @@
import React, { createContext, useCallback, useState, useRef } from 'react'
-import { merge } from 'lodash'
+import setOrDeleteByMutatePath from '../lib/set-or-delete-by-mutate-path'
+
export const DeliberationContext = createContext({})
export default DeliberationContext
@@ -12,19 +13,9 @@ export function DeliberationContextProvider(props) {
const upsert = useCallback(
obj => {
setData(data => {
- // if something changes in a top level prop, the top level ref has to be changed so it will cause a rerender
- const newData = { ...data }
- Object.keys(obj).forEach(key => {
- if (typeof obj[key] !== 'object') {
- newData[key] = obj[key]
- } else {
- const newProp = Array.isArray(obj[key]) ? [] : {}
- merge(newProp, data[key], obj[key])
- newData[key] = newProp
- }
- })
- deriveReducedPointList(newData, local)
- return newData // spread because we need to return a new reference
+ let newData = setOrDeleteByMutatePath(data, obj)
+ newData = deriveReducedPointList(newData, local)
+ return newData // is a new ref is there were changes above, or may be the original ref if no changes
})
},
[setData]
@@ -53,10 +44,7 @@ export function deriveReducedPointList(data, local) {
const { pointById, groupIdsLists } = data
if (!pointById || !groupIdsLists) return data
if (local.pointById === pointById && local.groupIdsList === groupIdsLists) return data // nothing to update
- const reducedPointTable = Object.entries(pointById).reduce(
- (reducedPointTable, [id, point]) => ((reducedPointTable[id] = { point }), reducedPointTable),
- {}
- )
+ const reducedPointTable = Object.entries(pointById).reduce((reducedPointTable, [id, point]) => ((reducedPointTable[id] = { point }), reducedPointTable), {})
let updated = false
for (const [firstId, ...groupIds] of groupIdsLists) {
reducedPointTable[firstId].group = groupIds.map(id => reducedPointTable[id].point)
@@ -66,17 +54,15 @@ export function deriveReducedPointList(data, local) {
// then copy them over so they are unchanged
for (const pointWithGroup of data.reducedPointList) {
const ptid = pointWithGroup.point._id
- if (
- reducedPointTable[ptid]?.point === pointWithGroup.point &&
- aEqual(reducedPointTable[ptid]?.group, pointWithGroup.group)
- )
- reducedPointTable[ptid] = pointWithGroup // if contentss are unchanged - unchange the ref
+ if (reducedPointTable[ptid]?.point === pointWithGroup.point && aEqual(reducedPointTable[ptid]?.group, pointWithGroup.group)) reducedPointTable[ptid] = pointWithGroup // if contentss are unchanged - unchange the ref
else updated = true
}
const newReducedPointList = Object.values(reducedPointTable)
local.pointById = pointById
local.groupIdsList = groupIdsLists
- if (!(newReducedPointList.length === data.reducedPointList.length && !updated))
+ if (!(newReducedPointList.length === data.reducedPointList.length && !updated)) {
data.reducedPointList = newReducedPointList
+ return { ...data }
+ }
return data
}
diff --git a/app/components/pair-compare.jsx b/app/components/pair-compare.jsx
index 9b648c9ea..e3b112bcd 100644
--- a/app/components/pair-compare.jsx
+++ b/app/components/pair-compare.jsx
@@ -1,170 +1,143 @@
// https://github.com/EnCiv/civil-pursuit/issues/53
+// https://github.com/EnCiv/civil-pursuit/issues/200
'use strict'
-import React, { useEffect, useRef, useState } from 'react'
+import React, { useEffect, useState } from 'react'
import Point from './point'
import { createUseStyles } from 'react-jss'
import { SecondaryButton } from './button.jsx'
+import ObjectId from 'bson-objectid'
+import cx from 'classnames'
function PairCompare(props) {
- const { pointList = [], onDone = () => {}, mainPoint = { subject: '', description: '' }, ...otherProps } = props
+ const { whyRankList = [], onDone = () => {}, mainPoint = { subject: '', description: '' }, discussionId, round, ...otherProps } = props
// idxLeft and idxRight can swap places at any point - they are simply pointers to the current two elements
const [idxLeft, setIdxLeft] = useState(0)
const [idxRight, setIdxRight] = useState(1)
-
- const isInitialRender = useRef(true)
-
- const visibleRightPointRef = useRef(null)
- const visibleLeftPointRef = useRef(null)
- const hiddenLeftPointRef = useRef(null)
- const hiddenRightPointRef = useRef(null)
-
- const [pointsIdxCounter, setPointsIdxCounter] = useState(1)
- const [selectedPoint, setSelectedPoint] = useState(null)
+ const [nextLeftPoint, setNextLeftPoint] = useState(null)
+ const [nextRightPoint, setNextRightPoint] = useState(null)
const [isRightTransitioning, setIsRightTransitioning] = useState(false)
const [isLeftTransitioning, setIsLeftTransitioning] = useState(false)
const classes = useStyles()
-
- useEffect(() => {
- if (isSelectionComplete()) {
- setSelectedPoint(pointList[idxLeft] ? pointList[idxLeft] : pointList[idxRight])
- }
- }, [pointsIdxCounter])
-
+ const [ranksByParentId, neverSetRanksByParentId] = useState(whyRankList.reduce((ranksByParentId, whyRank) => (whyRank.rank && (ranksByParentId[whyRank.rank.parentId] = whyRank.rank), ranksByParentId), {}))
+ // if all points are ranked, show the (first) one ranked most important, go to start over state
+ // otherwise compare the list
useEffect(() => {
- if (isInitialRender.current) {
- isInitialRender.current = false
- return
- }
-
- if (selectedPoint) {
- onDone({ valid: true, value: selectedPoint })
- } else {
- onDone({ valid: false, value: null })
+ let selectedIdx
+ let updated = 0
+ whyRankList.forEach((whyRank, i) => {
+ if (whyRank.rank && ranksByParentId[whyRank.rank.parentId] !== whyRank.rank) {
+ updated++
+ ranksByParentId[whyRank.rank.parentId] = whyRank.rank
+ if (whyRank.rank.category === 'most') selectedIdx = i
+ }
+ })
+ const ranks = Object.values(ranksByParentId)
+ if (updated && ranks.length === whyRankList.length && ranks.length > 0 && ranks.every(rank => rank.category)) {
+ // skip if an update from above after the user has completed ranking - likely this is the initial render
+ setIdxLeft(selectedIdx ?? whyRankList.length) // idx could be 0
+ setIdxRight(whyRankList.length)
+ setTimeout(() => onDone({ valid: true, value: undefined }))
}
- }, [selectedPoint])
+ }, [whyRankList])
+
+ // send up the rank, and track it locally
+ function rankIdxCategory(idx, category) {
+ if (idx >= whyRankList.length) return // if only one to rank, this could be out of bounds
+ const value = ranksByParentId[whyRankList[idx]._id]
+ ? { ...ranksByParentId[whyRankList[idx]._id], category }
+ : {
+ _id: ObjectId().toString(),
+ category,
+ parentId: whyRankList[idx].why._id,
+ stage: 'why',
+ discussionId,
+ round,
+ }
+ ranksByParentId[value.parentId] = value
+ const ranks = Object.values(ranksByParentId)
+ const valid = ranks.length === whyRankList.length && ranks.every(rank => rank.category)
+ setTimeout(() => onDone({ valid, value }))
+ }
const handleLeftPointClick = () => {
- // prevent transitions from firing on last comparison
- if (idxRight >= pointList.length - 1 || idxLeft >= pointList.length - 1) {
- if (idxLeft >= idxRight) {
- setIdxRight(idxLeft + 1)
- } else {
- setIdxRight(idxRight + 1)
- }
- setPointsIdxCounter(pointsIdxCounter + 1)
+ rankIdxCategory(idxRight, 'neutral')
+ if (Math.max(idxLeft, idxRight) + 1 >= whyRankList.length) {
+ // they've all been ranked
+ rankIdxCategory(idxLeft, 'most')
+ setIdxRight(whyRankList.length)
return
}
-
+ const nextRightIdx = Math.min(idxLeft >= idxRight ? idxLeft + 1 : idxRight + 1, whyRankList.length)
setIsLeftTransitioning(true)
- const visiblePointRight = visibleRightPointRef.current
- const hiddenPointRight = hiddenRightPointRef.current
-
- Object.assign(visiblePointRight.style, {
- position: 'relative',
- transform: 'translateX(200%)',
- transition: 'transform 0.5s linear',
- })
- Object.assign(hiddenPointRight.style, {
- position: 'absolute',
- transform: 'translateY(8.25rem)',
- transition: 'transform 0.5s linear',
- })
+ setNextRightPoint(whyRankList[nextRightIdx].why)
setTimeout(() => {
setIsLeftTransitioning(false)
- if (idxLeft >= idxRight) {
- setIdxRight(idxLeft + 1)
- } else {
- setIdxRight(idxRight + 1)
- }
-
- setPointsIdxCounter(pointsIdxCounter + 1)
-
- Object.assign(visiblePointRight.style, { position: '', transition: 'none', transform: '' })
- Object.assign(hiddenPointRight.style, { position: '', transition: 'none', transform: '' })
+ setNextRightPoint(null)
+ setIdxRight(nextRightIdx)
}, 500)
}
const handleRightPointClick = () => {
- // prevent transitions from firing on last comparison
- if (idxRight >= pointList.length - 1 || idxLeft >= pointList.length - 1) {
- if (idxLeft >= idxRight) {
- setIdxLeft(idxLeft + 1)
- } else {
- setIdxLeft(idxRight + 1)
- }
- setPointsIdxCounter(pointsIdxCounter + 1)
+ rankIdxCategory(idxLeft, 'neutral')
+ if (Math.max(idxLeft, idxRight) + 1 >= whyRankList.length) {
+ // they've all been ranked
+ rankIdxCategory(idxRight, 'most')
+ setIdxLeft(whyRankList.length)
return
}
-
+ const nextLeftIdx = Math.min(idxLeft >= idxRight ? idxLeft + 1 : idxRight + 1, whyRankList.length)
+ setNextLeftPoint(whyRankList[nextLeftIdx].why)
setIsRightTransitioning(true)
- const visiblePointLeft = visibleLeftPointRef.current
- const hiddenPointLeft = hiddenLeftPointRef.current
-
- Object.assign(visiblePointLeft.style, {
- position: 'relative',
- transform: 'translateX(-200%)',
- transition: 'transform 0.5s linear',
- })
- Object.assign(hiddenPointLeft.style, {
- position: 'absolute',
- transform: 'translateY(8.25rem)',
- transition: 'transform 0.5s linear',
- })
setTimeout(() => {
setIsRightTransitioning(false)
- if (idxLeft >= idxRight) {
- setIdxLeft(idxLeft + 1)
- } else {
- setIdxLeft(idxRight + 1)
- }
-
- setPointsIdxCounter(pointsIdxCounter + 1)
-
- Object.assign(visiblePointLeft.style, { position: '', transition: 'none', transform: '' })
- Object.assign(hiddenPointLeft.style, { position: '', transition: 'none', transform: '' })
+ setNextLeftPoint(null)
+ setIdxLeft(nextLeftIdx)
}, 500)
}
const handleNeitherButton = () => {
- if (selectedPoint) return
+ rankIdxCategory(idxLeft, 'neutral')
+ rankIdxCategory(idxRight, 'neutral')
if (idxLeft >= idxRight) {
- setIdxRight(idxLeft + 1)
- setIdxLeft(idxLeft + 2)
+ setIdxRight(Math.min(idxLeft + 1, whyRankList.length))
+ setIdxLeft(Math.min(idxLeft + 2, whyRankList.length))
} else {
- setIdxLeft(idxRight + 1)
- setIdxRight(idxRight + 2)
+ setIdxLeft(Math.min(idxRight + 1, whyRankList.length))
+ setIdxRight(Math.min(idxRight + 2, whyRankList.length))
}
-
- setPointsIdxCounter(pointsIdxCounter + 2)
}
const handleStartOverButton = () => {
+ Object.values(ranksByParentId).forEach(rank => (rank.category = undefined)) // reset the categories so we can start again
onDone({ valid: false, value: null })
setIdxRight(1)
setIdxLeft(0)
- setPointsIdxCounter(1)
- setSelectedPoint(null)
}
- const isSelectionComplete = () => {
- return pointsIdxCounter >= pointList.length
+ const handleYes = () => {
+ if (idxLeft < whyRankList.length) {
+ rankIdxCategory(idxLeft, 'most')
+ } else if (idxRight < whyRankList.length) {
+ rankIdxCategory(idxRight, 'most')
+ }
+ }
+ const handleNo = () => {
+ if (idxLeft < whyRankList.length) {
+ rankIdxCategory(idxLeft, 'neutral')
+ } else if (idxRight < whyRankList.length) {
+ rankIdxCategory(idxRight, 'neutral')
+ }
}
- const nextIndex = idxLeft > idxRight ? idxLeft + 1 : idxRight + 1
- const hiddenEmptyLeftPoint =
- const hiddenTransitioningLeftPoint = (
-
- )
- const hiddenEmptyRightPoint =
- const hiddenTransitioningRightPoint = (
-
- )
-
+ const pointsIdxCounter = Math.max(idxLeft, idxRight)
+ const isSelectionComplete = pointsIdxCounter >= whyRankList.length
+ const ranks = isSelectionComplete && Object.values(ranksByParentId) // isSelectionComple here so we don't do work if not needed
+ const allRanked = isSelectionComplete && ranks.length === whyRankList.length && ranks.every(rank => rank.category) // isSelectionComple here so we don't do work if not needed
return (
@@ -172,55 +145,38 @@ function PairCompare(props) {
{mainPoint.description}
-
{`${
- pointsIdxCounter <= pointList.length ? pointsIdxCounter : pointList.length
- } out of ${pointList.length}`}
+
{`${pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length} out of ${whyRankList.length}`}
- {pointsIdxCounter < pointList.length && (
-
- {isRightTransitioning ? hiddenTransitioningLeftPoint : hiddenEmptyLeftPoint}
-
- )}
- {pointsIdxCounter < pointList.length && (
-
- {isLeftTransitioning ? hiddenTransitioningRightPoint : hiddenEmptyRightPoint}
-
- )}
+
= whyRankList.length - 1 && classes.hidden)}>
+
+
+
= whyRankList.length - 1 && classes.hidden)}>
+
+
-
- {idxLeft < pointList.length && (
-
-
- {!isSelectionComplete() ? (
- Neither
- ) : (
- Start Over
- )}
-
+ {!isSelectionComplete || allRanked ? (
+
{!isSelectionComplete ? Neither : Start Over}
+ ) : (
+
+ )}
)
@@ -261,6 +217,9 @@ const useStyles = createUseStyles(theme => ({
marginBottom: '1rem',
clipPath: 'xywh(0 0 100% 500%)',
},
+ hidden: {
+ display: 'none',
+ },
hiddenPoint: {
width: '30%',
},
@@ -294,6 +253,21 @@ const useStyles = createUseStyles(theme => ({
justifyContent: 'center',
margin: '2rem auto',
},
+ transitioningDown: {
+ position: 'absolute',
+ transform: 'translateY(8.25rem)',
+ transition: 'transform 0.5s linear',
+ },
+ transitioningLeft: {
+ position: 'relative',
+ transform: 'translateX(-200%)',
+ transition: 'transform 0.5s linear',
+ },
+ transitioningRight: {
+ position: 'relative',
+ transform: 'translateX(200%)',
+ transition: 'transform 0.5s linear',
+ },
}))
const sharedStatusBadgeStyle = () => ({
diff --git a/app/components/ranking.jsx b/app/components/ranking.jsx
index 75e85c326..34ae389b6 100644
--- a/app/components/ranking.jsx
+++ b/app/components/ranking.jsx
@@ -14,22 +14,19 @@ const selectedOption = (
const unselectedOption =
export default function Ranking(props) {
- //Isolate props and set initial state
const { disabled, defaultValue, className, onDone, ...otherProps } = props
let [response, setResponse] = useState(responseOptions.includes(defaultValue) ? defaultValue : '')
useEffect(() => {
- if (defaultValue) {
- if (!responseOptions.includes(defaultValue)) {
- setResponse(undefined)
- onDone && onDone({ valid: false, value: '' })
- } else {
- setResponse(defaultValue)
- onDone && onDone({ valid: true, value: defaultValue })
- }
+ if (!defaultValue && !response) return // do not call onDone if initally empty, or if change to empty from above when it's already empty
+ if (responseOptions.includes(defaultValue)) {
+ setResponse(defaultValue)
+ onDone && onDone({ valid: true, value: defaultValue })
+ } else {
+ setResponse('')
+ onDone && onDone({ valid: false, value: '' })
}
}, [defaultValue])
- //Introduce component styling
const styleClasses = rankingStyleClasses(props)
const onSelectionChange = e => {
@@ -38,23 +35,17 @@ export default function Ranking(props) {
}
setResponse(e.target.value)
if (!onDone) {
- return console.warn(
- `Unhandled rank selection: ${e.target.value}. Please pass a handler function via the onDone prop.`
- )
+ return console.warn(`Unhandled rank selection: ${e.target.value}. Please pass a handler function via the onDone prop.`)
} else onDone({ valid: true, value: e.target.value })
}
return (
-
+
{responseOptions.map(option => {
return (