diff --git a/.storybook/decorators.js b/.storybook/decorators.js index ff04f30..c5f25bd 100644 --- a/.storybook/decorators.js +++ b/.storybook/decorators.js @@ -14,6 +14,30 @@ export const withThemeUi = (Story) => ( ) +const customParticipationTheme = Object.assign({}, compdemAdminTheme) +customParticipationTheme.fonts.body = "Arial" +customParticipationTheme.buttons.primary.border = 0 +customParticipationTheme.buttons.primary.borderRadius = 4 +customParticipationTheme.buttons.secondary = { + ...customParticipationTheme.buttons.primary, + color: 'darkGray', + bg: 'lightGray', + fontFamily: 'body', + cursor: 'pointer' +} +customParticipationTheme["letterSpacings"] = { + button: 0.75, +} +customParticipationTheme.colors["polisBlue"] = "#03a9f4" +customParticipationTheme.colors["darkGray"] = "rgb(100,100,100)" +customParticipationTheme.colors["lightGray"] = "rgb(235,235,235)" + +export const withParticipationThemeUi = (Story) => ( + + + +) + export const withDelibThemeUi = (Story) => ( diff --git a/package-lock.json b/package-lock.json index c8bdab1..43edf99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@radix-ui/react-tabs": "^1.1.1", "@react-spring/web": "^9.7.4", "color": "~4.2.3", "d3-force": "~1.2.1", @@ -982,6 +983,289 @@ "react": ">=16" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz", + "integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@react-spring/animated": { "version": "9.7.4", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.4.tgz", diff --git a/package.json b/package.json index 481f93b..08fa68c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "storybook-branch-switcher": "^0.5.0" }, "dependencies": { + "@radix-ui/react-tabs": "^1.1.1", "@react-spring/web": "^9.7.4", "color": "~4.2.3", "d3-force": "~1.2.1", diff --git a/stories/2.OurComponents.mdx b/stories/2.OurComponents.mdx index 48d4260..e4c0024 100644 --- a/stories/2.OurComponents.mdx +++ b/stories/2.OurComponents.mdx @@ -18,7 +18,7 @@ As a helpful resource, here is a summary of existing components (each a **clicka - `CurateV2` (Replaces `Curate`) -- `TidCarouselV2` +- `TidCarouselV2Animated` and `TidCarouselV2Static` (Replaces `TidCarousel`) - `SelectionWidgetV2` diff --git a/stories/compdem/client-participation/CivicTechTO/CurateV2.js b/stories/compdem/client-participation/CivicTechTO/CurateV2.js index de47d91..9af4426 100644 --- a/stories/compdem/client-participation/CivicTechTO/CurateV2.js +++ b/stories/compdem/client-participation/CivicTechTO/CurateV2.js @@ -1,76 +1,110 @@ +/** @jsx jsx */ +import { jsx, Box } from 'theme-ui' + import React from 'react' +import * as Tabs from "@radix-ui/react-tabs" 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, +export const CurateV2Button = React.forwardRef(({isSelected, handleClick, style, children, ...rest}, ref) => { + const styles = { + button: { + ...style, + fontSize: 14, + letterSpacing: 0.75, + variant: isSelected ? "buttons.primary" : "buttons.secondary", + textShadow: isSelected ? "0 0 .65px #fff" : null, + }, } return ( - + ) -} +}) -const CurateV2 = ({selectedTidCuration, handleCurateButtonClick = () => {}, math}) => { +const CurateV2 = React.forwardRef(({selectedTidCuration, handleCurateButtonClick = () => {}, math, isAccessible = false}, ref) => { const GROUP_COUNT = math["group-clusters"].length const styles = { container: { display: "flex", - flexDirection: "column", - rowGap: 5, + flexDirection: ["column-reverse", "row"], + gap: "5px", + rowGap: "5px", }, groupContainer: { display: "flex", - gap: 5, + flex: [1, "1 1 calc(2*100% / 3)"], + gap: "5px", }, groupButton: { + height: 35, flex: 1, }, + majorityContainer: { + flex: [1, "1 1 calc(100% / 3)"], + }, majorityButton: { + height: 35, 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 - -
+ isAccessible + ? ( + + +
+ + handleCurateButtonClick(globals.tidCuration.majority)} + isSelected={selectedTidCuration === globals.tidCuration.majority} + style={styles.majorityButton} + children="Diverse Majority Opinion" + /> + +
+
+ {groups.slice(0, GROUP_COUNT).map((groupLabel, index) => ( + + handleCurateButtonClick(index)} + isSelected={selectedTidCuration === index} + style={styles.groupButton} + children={groupLabel} + /> + + ))} +
+
+
+ ) + : ( +
+
+ handleCurateButtonClick(globals.tidCuration.majority)} + isSelected={selectedTidCuration === globals.tidCuration.majority} + style={styles.majorityButton} + children="Diverse Majority Opinion" + /> +
+
+ {groups.slice(0, GROUP_COUNT).map((groupLabel, index) => ( + handleCurateButtonClick(index)} + isSelected={selectedTidCuration === index} + style={styles.groupButton} + children={groupLabel} + /> + ))} +
+
+ ) ) -} +}) export default CurateV2 diff --git a/stories/compdem/client-participation/CivicTechTO/CurateV2.stories.js b/stories/compdem/client-participation/CivicTechTO/CurateV2.stories.js index 0051484..f2430ed 100644 --- a/stories/compdem/client-participation/CivicTechTO/CurateV2.stories.js +++ b/stories/compdem/client-participation/CivicTechTO/CurateV2.stories.js @@ -4,11 +4,13 @@ import * as globals from '../../../../codebases/compdem/client-participation/vis import Strings from '../../../../codebases/compdem/client-participation/js/strings/en_us' import CurateV2 from './CurateV2' import { getMath } from '../../../../.storybook/utils' +import { withParticipationThemeUi } from '../../../../.storybook/decorators' const mathResults = getMath() export default { component: CurateV2, + decorators: [withParticipationThemeUi], argTypes: { groupCount: { options: [2, 3, 4], @@ -30,6 +32,7 @@ const Template = ({ groupCount, ...args }) => { export const Interactive = Template.bind({}) Interactive.args = { groupCount: 4, + isAccessible: true, Strings: { majorityOpinion: Strings.majorityOpinion, group_123: Strings.group_123 @@ -39,6 +42,7 @@ Interactive.args = { export const Unselected = Template.bind({}) Unselected.args = { + isAccessible: true, selectedTidCuration: null, Strings: { majorityOpinion: Strings.majorityOpinion, diff --git a/stories/compdem/client-participation/CivicTechTO/ExploreTidV2.js b/stories/compdem/client-participation/CivicTechTO/ExploreTidV2.js new file mode 100644 index 0000000..64b3f08 --- /dev/null +++ b/stories/compdem/client-participation/CivicTechTO/ExploreTidV2.js @@ -0,0 +1,309 @@ +import _ from "lodash"; +import React from "react"; +import * as globals from '../../../../codebases/compdem/client-participation/vis2/components/globals' + +const checkmark = "M1299 813l-422 422q-19 19-45 19t-45-19l-294-294q-19-19-19-45t19-45l102-102q19-19 45-19t45 19l147 147 275-275q19-19 45-19t45 19l102 102q19 19 19 45t-19 45zm141 83q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"; +const ban = "M1440 893q0-161-87-295l-754 753q137 89 297 89 111 0 211.5-43.5t173.5-116.5 116-174.5 43-212.5zm-999 299l755-754q-135-91-300-91-148 0-273 73t-198 199-73 274q0 162 89 299zm1223-299q0 157-61 300t-163.5 246-245 164-298.5 61-298.5-61-245-164-163.5-246-61-300 61-299.5 163.5-245.5 245-164 298.5-61 298.5 61 245 164 163.5 245.5 61 299.5z"; + +export const DataSentence = ({math, selectedTidCuration, selectedComment, repfulFor, Strings}) => { + + let markup = null; + + if (_.isNumber(selectedTidCuration)) { + const gid = selectedTidCuration; + const tid = selectedComment.tid; + const groupVotes = math["group-votes"][gid]; + + const repness = _.find(math.repness[gid], (r) => { return r.tid === selectedComment.tid }) + let repfulForAgree = repness["repful-for"] === "agree"; + const v = groupVotes.votes[tid]; + const denominator = v.S; // (seen) + if (repness["best-agree"] && (v.A > 0)) { + repfulForAgree = true; + } + // const denominator = info.count; // or maybe v.S (seen) + // const percent = repfulForAgree ? + // " " + ((v.A / denominator * 100) >> 0) : + // " " + ((v.D / denominator * 100) >> 0); + const percent = repfulForAgree ? ((v.A / denominator * 100) >> 0) : ((v.D / denominator * 100) >> 0); + + + // var count = repfulForAgree ? v.A : v.D; + // var createdString = (new Date(c.get("created") * 1)).toString().match(/(.*?) [0-9]+:/)[1]; + + var s = repfulForAgree ? Strings.pctAgreedOfGroupLong : Strings.pctDisagreedOfGroupLong; + s = s.replace("{{pct}}", percent); + s = s.replace("{{group}}", globals.groupLabels[selectedTidCuration]); + s = s.replace("{{comment_id}}", tid); + + markup = ( +
+ + + +

+ {s} +

+
+ ) + } else if (selectedTidCuration === globals.tidCuration.majority) { + const repfulForAgree = _.find(math.consensus.agree, (r) => { return r.tid === selectedComment.tid }); + const repfulForDisagree = _.find(math.consensus.disagree, (r) => { return r.tid === selectedComment.tid }); + const repness = repfulForAgree || repfulForDisagree; + + const percent = (repness["n-success"] / repness["n-trials"] * 100) >> 0; + + let s = repfulForAgree ? Strings.pctAgreedLong : Strings.pctDisagreedLong; + s = s.replace("{{pct}}", percent); + s = s.replace("{{comment_id}}", selectedComment.tid); + + markup = ( +
+ + + +

+ {s} +

+
+ ) + } + + return markup; +} + +export class ExploreTid extends React.Component { + + handleAgree() { + this.props.onVoteClicked({ + tid: this.props.selectedComment.tid, + vote: window.polisTypes.reactions.pull, + }); + } + handleDisagree() { + this.props.onVoteClicked({ + tid: this.props.selectedComment.tid, + vote: window.polisTypes.reactions.push, + }); + } + handlePass() { + this.props.onVoteClicked({ + tid: this.props.selectedComment.tid, + vote: window.polisTypes.reactions.pass, + }); + } + + createChangeVotesElements() { + let currentVote = null; + if (this.props.selectedComment) { + let selectedTid = this.props.selectedComment.tid; + let voteForSelectedComment = _.find(this.props.votesByMe, (v) => { + return v.tid === selectedTid; + }); + currentVote = voteForSelectedComment && voteForSelectedComment.vote; + } + + let agreeButton = ( + ); + + let disagreeButton = ( + + ); + + let passButton = ( + + ); + + // Conditionally show change votes buttons + let buttons = null; + if (window.preload.firstConv.is_active) { + if (!_.isNumber(currentVote)) { + buttons = {agreeButton} {disagreeButton} {passButton} + } else if (currentVote === window.polisTypes.reactions.pass) { + buttons = Change vote: {agreeButton} {disagreeButton} + } else if (currentVote === window.polisTypes.reactions.pull) { + buttons = Change vote: {disagreeButton} {passButton} + } else if (currentVote === window.polisTypes.reactions.push) { + buttons = Change vote: {agreeButton} {passButton} + } + } + + let changeVotesElements = null; + if (!_.isNumber(currentVote)) { + changeVotesElements = {buttons} + } else if (currentVote === window.polisTypes.reactions.pass) { + changeVotesElements = You passed. {buttons} + } else if (currentVote === window.polisTypes.reactions.pull) { + changeVotesElements = You agreed. {buttons} + } else if (currentVote === window.polisTypes.reactions.push) { + changeVotesElements = You disagreed. {buttons} + } + + return ( +
+ {changeVotesElements} +
+ ) + } + render() { + if (!this.props.selectedComment) {return null} + let translatedText = null; + if (this.props.selectedComment.translatedText) { + let lang = navigator.language; + if (lang.indexOf('-') > 0) { + lang = lang.split('-')[0]; + // Do not need undefined check, will just not appear + translatedText = this.props.selectedComment.translatedText[lang]; + } + } + return ( +
+

+ {this.props.selectedComment ? "#" + this.props.selectedComment.tid : null} +

+
+

768 ? 400 : 245, + fontSize: 18, + fontFamily: "Georgia, serif", + }}> + {this.props.selectedComment ? this.props.selectedComment.txt : null} + {translatedText ?


: null} + {translatedText ? translatedText : null} +

+ + {/*this.createChangeVotesElements()*/} +
+
+ ) + } +} + +export default ExploreTid; + + +// +// {/* +// +// +// */} +// {/* +// +// */} diff --git a/stories/compdem/client-participation/CivicTechTO/SelectionWidgetV2.stories.js b/stories/compdem/client-participation/CivicTechTO/SelectionWidgetV2.stories.js index 327aec7..a89a221 100644 --- a/stories/compdem/client-participation/CivicTechTO/SelectionWidgetV2.stories.js +++ b/stories/compdem/client-participation/CivicTechTO/SelectionWidgetV2.stories.js @@ -3,14 +3,16 @@ 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 { withParticipationThemeUi } from '../../../../.storybook/decorators' +import TidCarouselV2Animated from './TidCarouselV2Animated' +import TidCarouselV2Static from './TidCarouselV2Static' import CurateV2 from './CurateV2' -import ExploreTid from '../../../../codebases/compdem/client-participation/vis2/components/exploreTid' +import ExploreTidV2 from './ExploreTidV2' const mathResult = getMath() const commentsData = getComments() -const SelectionWidgetV2 = ({math}) => { +const SelectionWidgetV2 = React.forwardRef(({isStatic, isAccessible, math}, ref) => { const [selectedTidCuration, setSelectedTidCuration] = useState(globals.tidCuration.majority) const [selectedComment, setSelectedComment] = useState(null) @@ -38,7 +40,7 @@ const SelectionWidgetV2 = ({math}) => { setSelectedTidCuration(tidCuration) } - const handleCommentClick = (c) => { + const handleCommentClick = (c) => () => { setSelectedComment(c) action("Clicked")(c) } @@ -51,18 +53,18 @@ const SelectionWidgetV2 = ({math}) => { rowGap: 5, } } - + const TidCarouselComponent = isStatic ? TidCarouselV2Static : TidCarouselV2Animated return ( -
+
- - { />
) -} +}) export default { component: SelectionWidgetV2, + decorators: [withParticipationThemeUi], + argTypes: { + isStatic: { + type: "boolean", + } + } } const StickToBottom = ({ children }) => ( @@ -101,5 +109,7 @@ const Template = (args) => { export const Interactive = Template.bind({}) Interactive.args = { + isStatic: true, + isAccessible: true, math: mathResult, } diff --git a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2.js b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2.js deleted file mode 100644 index 6ae9c05..0000000 --- a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2.js +++ /dev/null @@ -1,97 +0,0 @@ -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/CivicTechTO/TidCarouselV2Animated.js b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Animated.js new file mode 100644 index 0000000..484ecfe --- /dev/null +++ b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Animated.js @@ -0,0 +1,174 @@ +/** @jsx jsx */ +import { jsx, Box } from 'theme-ui' + +import React from 'react' +import * as Tabs from "@radix-ui/react-tabs" +import { animated, useTransition } from '@react-spring/web' +import useMeasure from 'react-use-measure' + +const AnimatedBox = animated(Box) + +function usePrevious(value) { + const ref = React.useRef(); + React.useEffect(() => { + ref.current = value; //assign the value of ref to the argument + },[value]); //this code will run when the value of 'value' changes + return ref.current; //in the end, return the current ref value. +} + +export const TidCarouselButton = React.forwardRef(({ label, isShown, isSelected, handleClick, containerWidth, style, ...rest }, ref) => { + // Allows selected style to persist during transition out. + let wasSelected = usePrevious(isSelected) + + const styles = { + button: { + ...style, + padding: 0, + overflow: "hidden", + fontSize: 14, + letterSpacing: 0.75, + // Only want to style based on previous select state when + // a button is no longer shown (aka on its was out) + variant: (isSelected || (wasSelected && !isShown)) ? "buttons.primary" : "buttons.secondary", + textShadow: (isSelected || (wasSelected && !isShown)) ? "0 0 .65px white" : null, + }, + span: { + // 1s is rought estimate, but react-spring uses forces, not duration. + transition: "transform 1s ease-in-out", + transform: isShown ? "scaleX(1)" : "scaleX(0.5)", + // Needed in order to transform. + display: "inline-block", + }, + } + const transition = useTransition(isShown, { + from: { + width: 0, + marginRight: 0, + opacity: 1, + }, + enter: { + // TODO: Why is this such a funny number? Would expect 4 to work. + width: containerWidth/5-4.8, + marginRight: 0, + opacity: 1, + }, + leave: { + width: 0, + marginRight: -5, + opacity: 0, + }, + }) + return ( + transition((style, isShownTransition) => ( + isShownTransition && + + {label} + + + )) + ) +}) + +const TidCarouselV2Animated = React.forwardRef(({ + selectedTidCuration, + allComments, + commentsToShow, + selectedComment, + handleCommentClick, + isAccessible = false, + Strings, +}, ref) => { + // TODO: Why doesn't this line avoid infinite renders when null? + if ( selectedTidCuration === null ) return null + // TODO: If we ever need to use the forwardRef, we'll need another package. + // See: https://github.com/pmndrs/react-use-measure?tab=readme-ov-file#multiple-refs + const [localRef, bounds] = useMeasure() + + allComments = allComments.sort((a, b) => a.tid - b.tid) + + const buttonHeight = 25 + const gap = 5 + const getRows = cols => { + const maxStatements = 10 + return Math.ceil(maxStatements/cols) + } + // Example: calc(20%-4px) + const getButtonWidthCalc = cols => `calc(${100/cols}% - ${gap*((cols-1)/cols)}px)` + const getContainerHeight = cols => buttonHeight*getRows(cols) + gap*(getRows(cols)-1) + + const styles = { + container: { + height: getContainerHeight(5), + display: "flex", + flexWrap: "wrap", + gap: `${gap}px`, + justifyContent: "flex-start", + marginRight: -30, + }, + button: { + height: buttonHeight, + // flex: `1 0 ${getButtonWidthCalc(5)}`, + // maxWidth: getButtonWidthCalc(5), + }, + } + + return ( + // padding and margins here are to ensure focus glow is visible with overflow: hidden. +
+ {isAccessible + ? ( + + + {!bounds.width || allComments.map(c => ( + + cts.tid == c.tid)} + /> + + ))} + + {/* {commentsToShowTids.map(tid => ( + Statement {tid}... + ))} */} + + ) + : ( +
+ {/* ref not available on first render, so only render map after bounds exists. */} + {!bounds.width || allComments.map(c => ( + cts.tid == c.tid)} + /> + ))} +
+ ) + } +
+ ) +}) + +export default TidCarouselV2Animated diff --git a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2.stories.js b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Animated.stories.js similarity index 74% rename from stories/compdem/client-participation/CivicTechTO/TidCarouselV2.stories.js rename to stories/compdem/client-participation/CivicTechTO/TidCarouselV2Animated.stories.js index fd56fa2..29cf7fe 100644 --- a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2.stories.js +++ b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Animated.stories.js @@ -2,16 +2,19 @@ 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' +import TidCarouselV2Animated from './TidCarouselV2Animated' +import { withParticipationThemeUi } from '../../../../.storybook/decorators' const mathResult = getMath() const commentsData = getComments() -commentsData.sort((a,b) => a.tid - b.tid) export default { - component: TidCarouselV2, + component: TidCarouselV2Animated, + decorators: [withParticipationThemeUi], argTypes: { selectedTidCuration: { + // TODO: Figure out why null does infinite renders. + // options: [null, 'majority', 0, 1, 2, 3], options: ['majority', 0, 1, 2, 3], control: { type: 'inline-radio' }, }, @@ -30,7 +33,7 @@ const Template = (args) => { } ) - const handleCommentClick = (c) => { + const handleCommentClick = (c) => () => { setSelectedComment(c) action("Clicked")(c) } @@ -41,7 +44,7 @@ const Template = (args) => { if (!commentsToShow.map(c => c.tid).includes(selectedComment?.tid)) { handleCommentClick(commentsToShow[0]) } - return { export const Interactive = Template.bind({}) Interactive.args = { + isAccessible: true, selectedTidCuration: 1, allComments: commentsData, Strings, diff --git a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.js b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.js new file mode 100644 index 0000000..a57af0f --- /dev/null +++ b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.js @@ -0,0 +1,97 @@ +/** @jsx jsx */ +import { jsx, Box } from 'theme-ui' +import * as Tabs from "@radix-ui/react-tabs" + +import React from "react" + +const TidCarouselButton = React.forwardRef(({ isSelected, handleClick, style, children, ...rest }, ref) => { + const styles = { + button: { + ...style, + fontSize: 14, + letterSpacing: 0.75, + variant: isSelected ? "buttons.primary" : "buttons.secondary", + textShadow: isSelected ? "0 0 .65px white" : null, + } + } + return ( + + {children} + + ) +}) + +const TidCarouselV2Static = React.forwardRef(({ + selectedTidCuration, + // allComments, + commentsToShow, + selectedComment, + handleCommentClick, + isAccessible = false, + Strings, +}, ref) => { + commentsToShow.sort((a, b) => a.tid - b.tid) + + const buttonHeight = 25 + const gap = 5 + const getRows = cols => { + const maxStatements = 10 + return Math.ceil(maxStatements/cols) + } + // Example: calc(20%-4px) + const getButtonWidthCalc = cols => `calc(${100/cols}% - ${gap*((cols-1)/cols)}px)` + const getContainerHeight = cols => buttonHeight*getRows(cols) + gap*(getRows(cols)-1) + const styles = { + container: { + height: [getContainerHeight(5), getContainerHeight(10)], + display: "flex", + flexWrap: "wrap", + gap: `${gap}px`, + justifyContent: "flex-start", + }, + button: { + height: buttonHeight, + flex: [`1 0 ${getButtonWidthCalc(5)}`, `1 0 ${getButtonWidthCalc(10)}`], + maxWidth: [getButtonWidthCalc(5), getButtonWidthCalc(10)], + }, + } + return ( + isAccessible + ? ( +
+ + + {commentsToShow.map(c => ( + + + + ))} + + {/* {commentsToShowTids.map(tid => ( + Statement {tid}... + ))} */} + +
+ ) + : ( +
+ {commentsToShow.map(c => ( + + ))} +
+ ) + ) +}) + +export default TidCarouselV2Static diff --git a/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.stories.js b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.stories.js new file mode 100644 index 0000000..32dcba0 --- /dev/null +++ b/stories/compdem/client-participation/CivicTechTO/TidCarouselV2Static.stories.js @@ -0,0 +1,129 @@ +import React, { useState } from 'react' +import { action } from '@storybook/addon-actions' +import TidCarouselV2Static from './TidCarouselV2Static' +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 { withParticipationThemeUi } from '../../../../.storybook/decorators' + +const mathResults = getMath() +const commentsData = getComments() + +const pluckNBetweenLowerUpper = (n, lower, upper) => { + let numbers = [] + while (numbers.length < n) { + let candidate = Math.floor(Math.random() * (upper - lower)) + (lower + 1) + if (!numbers.includes(candidate)) { + numbers.push(candidate) + } + } + // Ascending integer sort. + numbers.sort((a, b) => a - b) + + return numbers +} + +export default { + component: TidCarouselV2Static, + decorators: [withParticipationThemeUi], +} + +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 = { + isAccessible: true, + selectedTidCuration: 0, + Strings, +} +Interactive.argTypes = { + selectedTidCuration: { + options: [null, 'majority', 0, 1, 2, 3], + control: { type: 'inline-radio' }, + }, +} + +export const Empty = Template.bind({}) +Empty.args = { + isAccessible: true, + commentsToShow: [], + selectedTidCuration: null, + selectedComment: null, + // TODO: Pretty sure this is janky. It should be simply action("Clicked") + // and the prop in tidCarousel should be: + // onClick={() => this.props.handleCommentClick(c)} + // not just + // onClick={this.props.handleCommentClick(c)} + handleCommentClick: (c) => () => action("Clicked")(c), + Strings, +} + +export const OneStatement = Template.bind({}) +OneStatement.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,11), +} + +export const FiveStatements = Template.bind({}) +FiveStatements.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,15), +} + +export const SixStatements = Template.bind({}) +SixStatements.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,16), +} + +export const TenStatements = Template.bind({}) +TenStatements.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,20), +} + +export const Selected = Template.bind({}) +Selected.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,20), + selectedComment: commentsData[11], +} + +export const UpToDoubleDigits = Template.bind({}) +UpToDoubleDigits.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(10, 0, 100).map(i => ({ tid: i })), +} + +export const UpToTripleDigits = Template.bind({}) +UpToTripleDigits.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(10, 0, 400).map(i => ({ tid: i })), +} diff --git a/stories/compdem/client-participation/TidCarousel.stories.js b/stories/compdem/client-participation/TidCarousel.stories.js index dbf4751..fa7e7c6 100644 --- a/stories/compdem/client-participation/TidCarousel.stories.js +++ b/stories/compdem/client-participation/TidCarousel.stories.js @@ -6,8 +6,7 @@ import Strings from '../../../codebases/compdem/client-participation/js/strings/ import { getMath, getComments } from '../../../.storybook/utils' const mathResults = getMath() -const commentsData = getComments() -commentsData.sort((a,b) => a.tid - b.tid) +const commentsData = getComments().sort((a, b) => a.tid - b.tid) const pluckNBetweenLowerUpper = (n, lower, upper) => { let numbers = [] @@ -19,18 +18,12 @@ const pluckNBetweenLowerUpper = (n, lower, upper) => { } // Ascending integer sort. numbers.sort((a, b) => a - b) - + return numbers } export default { component: TidCarousel, - argTypes: { - selectedTidCuration: { - options: [null, "majority", 0, 1, 2, 3], - control: { type: 'inline-radio' }, - }, - }, } const Template = (args) => { @@ -54,7 +47,11 @@ const Template = (args) => { ?.map(i => i.tid).includes(c.tid) ) - return + return } export const Interactive = Template.bind({}) @@ -62,10 +59,16 @@ Interactive.args = { selectedTidCuration: 0, Strings, } +Interactive.argTypes = { + selectedTidCuration: { + options: [null, "majority", 0, 1, 2, 3], + control: { type: 'inline-radio' }, + }, +} export const Default = Template.bind({}) Default.args = { - selectedTidCuration: 1, + selectedTidCuration: undefined, commentsToShow: commentsData.slice(10,20), selectedComment: null, // TODO: Pretty sure this is janky. It should be simply action("Clicked") @@ -77,6 +80,51 @@ Default.args = { Strings, } +export const Empty = Template.bind({}) +Empty.args = { + commentsToShow: [], + selectedTidCuration: globals.tidCuration.majority, + selectedComment: null, + // TODO: Pretty sure this is janky. It should be simply action("Clicked") + // and the prop in tidCarousel should be: + // onClick={() => this.props.handleCommentClick(c)} + // not just + // onClick={this.props.handleCommentClick(c)} + handleCommentClick: (c) => () => action("Clicked")(c), + Strings, +} + +export const OneStatement = Template.bind({}) +OneStatement.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(1, 0, 25).map(i => ({ tid: i })), +} + +export const FiveStatements = Template.bind({}) +FiveStatements.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(5, 0, 25).map(i => ({ tid: i })), +} + +export const SixStatements = Template.bind({}) +SixStatements.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(6, 0, 25).map(i => ({ tid: i })), +} + +export const TenStatements = Template.bind({}) +TenStatements.args = { + ...Empty.args, + commentsToShow: pluckNBetweenLowerUpper(10, 0, 25).map(i => ({ tid: i })), +} + +export const Selected = Template.bind({}) +Selected.args = { + ...Empty.args, + commentsToShow: commentsData.slice(10,20), + selectedComment: commentsData[11], +} + // TODO: Load dataset with hundreds/thousands of comments. //export const DoubleToTripleDigits = Template.bind({}) //DoubleToTripleDigits.args = { @@ -84,32 +132,24 @@ Default.args = { // commentsToShow: commentsData.slice(95,105), //} -export const StatementSelected = Template.bind({}) -StatementSelected.args = { - ...Default.args, - selectedComment: { tid: commentsData[11].tid } -} +// export const Pagination = Template.bind({}) +// Pagination.args = { +// ...Default.args, +// commentsToShow: commentsData.slice(10,40), +// } -export const FewStatements = Template.bind({}) -FewStatements.args = { +export const UpToDoubleDigits = Template.bind({}) +UpToDoubleDigits.args = { ...Default.args, - commentsToShow: commentsData.slice(10,15), + commentsToShow: pluckNBetweenLowerUpper(10, 0, 100).map(i => ({ tid: i })), } -export const Pagination = Template.bind({}) -Pagination.args = { +export const UpToTripleDigits = Template.bind({}) +UpToTripleDigits.args = { ...Default.args, - commentsToShow: commentsData.slice(10,40), + commentsToShow: pluckNBetweenLowerUpper(10, 0, 400).map(i => ({ tid: i })), } -export const UpToDoubleDigits = Template.bind({}) -UpToDoubleDigits.args = { - ...Default.args, - commentsToShow: [ - ...pluckNBetweenLowerUpper(10, 0, commentsData.length-1).map(i => commentsData[i]) - ] -} - export const NoGroupSelectedSoHidden = Template.bind({}) NoGroupSelectedSoHidden.args = { ...Default.args,