diff --git a/.storybook/utils.js b/.storybook/utils.js index 10bc27d..250bd88 100644 --- a/.storybook/utils.js +++ b/.storybook/utils.js @@ -1,5 +1,6 @@ import * as globals from "../codebases/compdem/client-report/src/components/globals"; import participationData from './data/3ntrtcehas-participation-init.json' +import commentsData from './data/3ntrtcehas-comments.json' export const getMath = () => { return JSON.parse(participationData.pca) @@ -20,6 +21,10 @@ export const getConversation = () => { return participationData.conversation } +export const getComments = () => { + return commentsData +} + export const getVoteColors = () => ({ agree: globals.brandColors.agree, disagree: globals.brandColors.disagree, diff --git a/package-lock.json b/package-lock.json index 9d23208..a1ab636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@react-spring/web": "^9.7.4", "color": "~4.2.3", "d3-force": "~1.2.1", "hull.js": "^0.2.11", "radium": "^0.26.2", "react-easy-emoji": "^1.8.1", + "react-use-measure": "^2.1.1", "theme-ui": "^0.3.5", "victory-core": "~36.6.8" }, @@ -978,6 +980,72 @@ "react": ">=16" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", + "integrity": "sha512-7As+8Pty2QlemJ9O5ecsuPKjmO0NKvmVkRR1n6mEotFgWar8FKuQt2xgxz3RTgxcccghpx1YdS1FCdElQNexmQ==", + "dependencies": { + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.4.tgz", + "integrity": "sha512-GzjA44niEJBFUe9jN3zubRDDDP2E4tBlhNlSIkTChiNf9p4ZQlgXBg50qbXfSXHQPHak/ExYxwhipKVsQ/sUTw==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.4.tgz", + "integrity": "sha512-mqDI6rW0Ca8IdryOMiXRhMtVGiEGLIO89vIOyFQXRIwwIMX30HLya24g9z4olDvFyeDW3+kibiKwtZnA4xhldA==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.4.tgz", + "integrity": "sha512-bEPI7cQp94dOtCFSEYpxvLxj0+xQfB5r9Ru1h8OMycsIq7zFZon1G0sHrBLaLQIWeMCllc4tVDYRTLIRv70C8w==", + "dependencies": { + "@react-spring/rafz": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.4.tgz", + "integrity": "sha512-iQVztO09ZVfsletMiY+DpT/JRiBntdsdJ4uqk3UJFhrhS8mIC9ZOZbmfGSRs/kdbNPQkVyzucceDicQ/3Mlj9g==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.4", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.4.tgz", + "integrity": "sha512-UMvCZp7I5HCVIleSa4BwbNxynqvj+mJjG2m20VO2yPoi2pnCYANy58flvz9v/YcXTAvsmL655FV3pm5fbr6akA==", + "dependencies": { + "@react-spring/animated": "~9.7.4", + "@react-spring/core": "~9.7.4", + "@react-spring/shared": "~9.7.4", + "@react-spring/types": "~9.7.4" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@remix-run/router": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", @@ -3724,6 +3792,11 @@ "node": ">=12" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -6228,7 +6301,6 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -6371,6 +6443,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-use-measure": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", + "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", + "dependencies": { + "debounce": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6593,7 +6677,6 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/package.json b/package.json index 553c5cb..bef8416 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "storybook-branch-switcher": "github:patcon/storybook-branch-switcher#patcon/dev" }, "dependencies": { + "@react-spring/web": "^9.7.4", "color": "~4.2.3", "d3-force": "~1.2.1", "hull.js": "^0.2.11", "radium": "^0.26.2", "react-easy-emoji": "^1.8.1", + "react-use-measure": "^2.1.1", "theme-ui": "^0.3.5", "victory-core": "~36.6.8" } diff --git a/stories/OurComponents.mdx b/stories/OurComponents.mdx new file mode 100644 index 0000000..aa7946c --- /dev/null +++ b/stories/OurComponents.mdx @@ -0,0 +1,19 @@ +import LinkTo from '@storybook/addon-links/react'; +import { Meta } from '@storybook/blocks'; + + + +# Our Custom Components + +The vast majority of the components in this Storybook are unmodified from either: +1. the upstream Polis codebase maintained by Computational Democracy Project, or +2. the forked Polis codebases maintained by others. + +As a helpful resource, these are the components (each a **clickable `Link`**) that we ourselves have modified or experimented with: + +- `CurateV2` + (Replaces `Curate`) +- `TidCarouselV2` + (Replaces `TidCarousel`) +- `SelectionWidgetV2` + (Replaces no single component) diff --git a/stories/Overview.mdx b/stories/Overview.mdx index abebe17..9e78a86 100644 --- a/stories/Overview.mdx +++ b/stories/Overview.mdx @@ -1,3 +1,7 @@ +import { Meta } from '@storybook/blocks'; + + + # Overview: Polis-Storybook 🚧 This overview page is considered a work-in-progress until this line is removed diff --git a/stories/compdem/client-participation/CurateV2.js b/stories/compdem/client-participation/CurateV2.js new file mode 100644 index 0000000..63b10e7 --- /dev/null +++ b/stories/compdem/client-participation/CurateV2.js @@ -0,0 +1,76 @@ +import React from 'react' +import * as globals from '../../../codebases/compdem/client-participation/vis2/components/globals' + +export const CurateV2Button = ({isSelected, onCurateButtonClick, style, children}) => { + const colors = { + polisBlue: "#03a9f4", + darkGray: "rgb(100,100,100)", + lightGray: "rgb(235,235,235)", + } + const buttonStyle = { + ...style, + border: 0, + cursor: "pointer", + borderRadius: 4, + fontSize: 14, + padding: "6px 12px", + letterSpacing: 0.75, + // fontWeight: isSelected ? 700 : 500, + textShadow: isSelected ? "0 0 .65px #fff" : null, + backgroundColor: isSelected ? colors.polisBlue : colors.lightGray, + color: isSelected ? "white" : colors.darkGray, + } + return ( + + ) +} + +const CurateV2 = ({selectedTidCuration, handleCurateButtonClick = () => {}, math}) => { + const GROUP_COUNT = math["group-clusters"].length + const styles = { + container: { + display: "flex", + flexDirection: "column", + rowGap: 5, + }, + groupContainer: { + display: "flex", + gap: 5, + }, + groupButton: { + flex: 1, + }, + majorityButton: { + width: "100%", + }, + } + const groups = ['A', 'B', 'C', 'D', 'E'] + + return( +
+
+ {groups.slice(0, GROUP_COUNT).map((groupLabel, index) => ( + handleCurateButtonClick(index)} + isSelected={selectedTidCuration === index} + style={styles.groupButton} + > + {groupLabel} + + ))} +
+ handleCurateButtonClick(globals.tidCuration.majority)} + isSelected={selectedTidCuration === globals.tidCuration.majority} + style={styles.majorityButton} + > + Diverse Majority Opinion + +
+ ) +} + +export default CurateV2 diff --git a/stories/compdem/client-participation/CurateV2.stories.js b/stories/compdem/client-participation/CurateV2.stories.js new file mode 100644 index 0000000..f6dd61c --- /dev/null +++ b/stories/compdem/client-participation/CurateV2.stories.js @@ -0,0 +1,61 @@ +import React, { useState } from 'react' +import { action } from '@storybook/addon-actions' +import * as globals from '../../../codebases/compdem/client-participation/vis2/components/globals' +import Strings from '../../../codebases/compdem/client-participation/js/strings/en_us' +import CurateV2 from './CurateV2' +import { getMath } from '../../../.storybook/utils' + +const mathResults = getMath() + +export default { + component: CurateV2, + argTypes: { + groupCount: { + options: [2, 3, 4], + control: { type: 'inline-radio' }, + } + } +} + +const Template = ({ groupCount, ...args }) => { + args.math["group-clusters"] = mathResults["group-clusters"].slice(0, groupCount) + const [selectedTidCuration, setSelectedTidCuration] = useState(globals.tidCuration.majority) + const handleCurateButtonClick = (tidCuration) => { + action("Clicked")(tidCuration) + setSelectedTidCuration(tidCuration) + } + return +} + +export const Interactive = Template.bind({}) +Interactive.args = { + groupCount: 4, + Strings: { + majorityOpinion: Strings.majorityOpinion, + group_123: Strings.group_123 + }, + math: mathResults, +} + +export const Unselected = Template.bind({}) +Unselected.args = { + selectedTidCuration: null, + Strings: { + majorityOpinion: Strings.majorityOpinion, + group_123: Strings.group_123 + }, + handleCurateButtonClick: action('Clicked'), + math: mathResults, +} + +export const MajoritySelected = Template.bind({}) +MajoritySelected.args = { + ...Unselected.args, + selectedTidCuration: globals.tidCuration.majority +} + +export const GroupSelected = Template.bind({}) +GroupSelected.args = { + ...Unselected.args, + selectedTidCuration: 1 +} diff --git a/stories/compdem/client-participation/Graph.stories.js b/stories/compdem/client-participation/Graph.stories.js index e15975d..de15845 100644 --- a/stories/compdem/client-participation/Graph.stories.js +++ b/stories/compdem/client-participation/Graph.stories.js @@ -1,10 +1,10 @@ import React from 'react' import Graph from '../../../codebases/compdem/client-participation/vis2/components/graph' -import { getMath } from '../../../.storybook/utils' +import { getComments, getMath } from '../../../.storybook/utils' import Strings from '../../../codebases/compdem/client-participation/js/strings/en_us' -import commentsData from '../../../.storybook/data/3ntrtcehas-comments.json' import { action } from '@storybook/addon-actions' +const commentsData = getComments() const mathResult = getMath() export default { diff --git a/stories/compdem/client-participation/SelectionWidgetV2.stories.js b/stories/compdem/client-participation/SelectionWidgetV2.stories.js new file mode 100644 index 0000000..8d4c023 --- /dev/null +++ b/stories/compdem/client-participation/SelectionWidgetV2.stories.js @@ -0,0 +1,82 @@ +import React, { useState } from 'react' +import { action } from '@storybook/addon-actions' +import * as globals from '../../../codebases/compdem/client-participation/vis2/components/globals' +import Strings from '../../../codebases/compdem/client-participation/js/strings/en_us' +import { getMath, getComments } from '../../../.storybook/utils' +import TidCarouselV2 from './TidCarouselV2' +import CurateV2 from './CurateV2' + +const mathResult = getMath() +const commentsData = getComments() + +const SelectionWidgetV2 = ({math}) => { + const [selectedTidCuration, setSelectedTidCuration] = useState(globals.tidCuration.majority) + const [selectedComment, setSelectedComment] = useState(null) + + const commentsByGroup = Object.assign({}, + math.repness, + { + "majority": [ + ...math.consensus.agree, + ...math.consensus.disagree, + ], + } + ) + + const commentsToShow = commentsData + .filter(c => commentsByGroup[selectedTidCuration] + ?.map(i => i.tid).includes(c.tid) + ) + + if (!commentsToShow.map(c => c.tid).includes(selectedComment?.tid)) { + setSelectedComment(commentsToShow[0]) + } + + const handleCurateButtonClick = (tidCuration) => { + action("Clicked")(tidCuration) + setSelectedTidCuration(tidCuration) + } + + const handleCommentClick = (c) => { + setSelectedComment(c) + action("Clicked")(c) + } + + const styles = { + container: { + flex: 1, + display: "flex", + flexDirection: "column", + rowGap: 5, + } + } + return ( +
+ + +
+ ) +} + +export default { + component: SelectionWidgetV2, +} + +const Template = (args) => { + // TODO: Figure out how to make this sticky at the bottom + return
+ +
+} + +export const Interactive = Template.bind({}) +Interactive.args = { + math: mathResult, +} diff --git a/stories/compdem/client-participation/TidCarousel.stories.js b/stories/compdem/client-participation/TidCarousel.stories.js index cbbf7d8..dbf4751 100644 --- a/stories/compdem/client-participation/TidCarousel.stories.js +++ b/stories/compdem/client-participation/TidCarousel.stories.js @@ -1,10 +1,12 @@ -import React from 'react' +import React, { useState } from 'react' import { action } from '@storybook/addon-actions' import TidCarousel from '../../../codebases/compdem/client-participation/vis2/components/tidCarousel' +import * as globals from '../../../codebases/compdem/client-participation/vis2/components/globals' import Strings from '../../../codebases/compdem/client-participation/js/strings/en_us' +import { getMath, getComments } from '../../../.storybook/utils' -import commentsData from '../../../.storybook/data/3ntrtcehas-comments.json' - +const mathResults = getMath() +const commentsData = getComments() commentsData.sort((a,b) => a.tid - b.tid) const pluckNBetweenLowerUpper = (n, lower, upper) => { @@ -22,10 +24,44 @@ const pluckNBetweenLowerUpper = (n, lower, upper) => { } export default { - component: TidCarousel + component: TidCarousel, + argTypes: { + selectedTidCuration: { + options: [null, "majority", 0, 1, 2, 3], + control: { type: 'inline-radio' }, + }, + }, } -const Template = (args) => +const Template = (args) => { + const [selectedComment, setSelectedComment] = useState(null) + const handleCommentClick = (comment) => () => { + action("Clicked")(comment) + setSelectedComment(comment) + } + + const commentsByGroup = Object.assign({}, + mathResults.repness, + { + "majority": [ + ...mathResults.consensus.agree, + ...mathResults.consensus.disagree, + ], + } + ) + const commentsToShow = commentsData + .filter(c => commentsByGroup[args.selectedTidCuration] + ?.map(i => i.tid).includes(c.tid) + ) + + return +} + +export const Interactive = Template.bind({}) +Interactive.args = { + selectedTidCuration: 0, + Strings, +} export const Default = Template.bind({}) Default.args = { @@ -37,7 +73,7 @@ Default.args = { // onClick={() => this.props.handleCommentClick(c)} // not just // onClick={this.props.handleCommentClick(c)} - handleCommentClick: () => action("Clicked"), + handleCommentClick: (c) => () => action("Clicked")(c), Strings, } diff --git a/stories/compdem/client-participation/TidCarouselV2.js b/stories/compdem/client-participation/TidCarouselV2.js new file mode 100644 index 0000000..6ae9c05 --- /dev/null +++ b/stories/compdem/client-participation/TidCarouselV2.js @@ -0,0 +1,97 @@ +import React from 'react' +import { animated, useTransition } from '@react-spring/web' +import useMeasure from 'react-use-measure' + +export const TidCarouselButton = ({ label, isShown, isSelected, handleClick, containerWidth }) => { + const transition = useTransition(isShown, { + from: { + width: 0, + marginRight: 0, + opacity: 1, + }, + enter: { + width: containerWidth/5-5, + marginRight: 0, + opacity: 1, + }, + leave: { + width: 0, + marginRight: -5, + opacity: 0, + }, + }) + return ( + transition((style, isShownTransition) => ( + isShownTransition && + + {label} + + + )) + ) +} + +const TidCarouselV2 = ({ + selectedTidCuration, + allComments, + commentsToShow, + selectedComment, + handleCommentClick, + Strings, +}) => { + if ( selectedTidCuration === null ) return null + const [ref, bounds] = useMeasure() + + allComments = allComments.sort((a, b) => a.tid - b.tid) + const commentsToShowTids = commentsToShow.map(c => c.tid) + + // ref not available on first render, so only render map after bounds exists. + return ( +
+ {!bounds.width || allComments.map((c, i) => ( + handleCommentClick(c)} + isSelected={selectedComment && selectedComment.tid === c.tid} + isShown={commentsToShowTids.includes(c.tid)} + /> + ))} +
+ ) +} + +export default TidCarouselV2 diff --git a/stories/compdem/client-participation/TidCarouselV2.stories.js b/stories/compdem/client-participation/TidCarouselV2.stories.js new file mode 100644 index 0000000..67c99d6 --- /dev/null +++ b/stories/compdem/client-participation/TidCarouselV2.stories.js @@ -0,0 +1,56 @@ +import React from 'react' +import { action } from '@storybook/addon-actions' +import Strings from '../../../codebases/compdem/client-participation/js/strings/en_us' +import { getComments, getMath } from '../../../.storybook/utils' +import TidCarouselV2 from './TidCarouselV2' + +const mathResult = getMath() +const commentsData = getComments() +commentsData.sort((a,b) => a.tid - b.tid) + +export default { + component: TidCarouselV2, + argTypes: { + selectedTidCuration: { + options: ['majority', 0, 1, 2, 3], + control: { type: 'inline-radio' }, + }, + }, +} + +const Template = (args) => { + const [selectedComment, setSelectedComment] = React.useState(null) + const commentsByGroup = Object.assign({}, + mathResult.repness, + { + "majority": [ + ...mathResult.consensus.agree, + ...mathResult.consensus.disagree, + ], + } + ) + + const handleCommentClick = (c) => { + setSelectedComment(c) + action("Clicked")(c) + } + const commentsToShow = commentsData + .filter(c => commentsByGroup[args.selectedTidCuration] + ?.map(i => i.tid).includes(c.tid) + ) + if (!commentsToShow.map(c => c.tid).includes(selectedComment?.tid)) { + handleCommentClick(commentsToShow[0]) + } + return +} + +export const Interactive = Template.bind({}) +Interactive.args = { + selectedTidCuration: 1, + allComments: commentsData, + Strings, +} diff --git a/stories/compdem/client-participation/TidCarouselV3.stories.js b/stories/compdem/client-participation/TidCarouselV3.stories.js new file mode 100644 index 0000000..ebee5ac --- /dev/null +++ b/stories/compdem/client-participation/TidCarouselV3.stories.js @@ -0,0 +1,94 @@ +import React, { useRef, useEffect, useState } from 'react' + +const TidCarouselButtonV3 = ({label}) => ( +
{label}
+) + +function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +} + +const TidCarouselV3 = ({ numbers = [] }) => { + const prevNumbers = usePrevious(numbers) || [] + + useEffect(() => { + console.log("numbers: ", numbers); + console.log("prevNumbers: ", prevNumbers); + }, [numbers, prevNumbers]); + + const allNumbers = [...new Set(numbers.concat(prevNumbers))].sort((a,b) => a - b) + const enteringNumbers = numbers.filter(n => !prevNumbers.includes(n)) + const exitingNumbers = prevNumbers.filter(n => !numbers.includes(n)) + + return( + <> +
new numbers: {JSON.stringify(numbers)}
+
old numbers: {JSON.stringify(prevNumbers)}
+
all numbers: {JSON.stringify(allNumbers)}
+
entering numbers: {JSON.stringify(enteringNumbers)}
+
exiting numbers: {JSON.stringify(exitingNumbers)}
+
+ {allNumbers.map(n => { + const baseStyle = { + background: "#03a9f4", + color: "white", + fontWeight: 500, + borderRadius: "30%", + flex: 1, + alignContent: "center", + textAlign: "center", + height: 30, + } + if (enteringNumbers.includes(n)) { + return
{n}
+ } else if (exitingNumbers.includes(n)) { + return
{n}
+ } else { + return
{n}
+ } + })} +
+ + ) +} + +export default { + component: TidCarouselV3, + argTypes: { + group: { + options: ['A', 'B', 'C'], + control: { type: 'inline-radio' }, + }, + }, +} + +const Template = (args) => { + const NUMBERS_DATA = { + A: [2,4,5,18,49], + B: [2,4,5,18,22], + C: [3,9,17,25,33], + } + return +} + +export const Default = Template.bind({}) +