├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── pull_request_template.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── README-root │ ├── filepath-after.png │ ├── filepath-before.png │ ├── test-output.png │ ├── ultratrimmedDemo.gif │ ├── zustand-test-filepath-1.png │ ├── zustand-test-filepath-2.png │ └── zustand-test-snapshot-2.png └── logo │ └── Chromogen.png ├── demo-todo ├── .babelrc ├── LICENSE ├── README.md ├── __tests__ │ └── initialTestTest.js ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── App.jsx │ │ ├── Quotes.jsx │ │ ├── ReadOnlyTodoItem.jsx │ │ ├── SearchBar.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoItemCreator.jsx │ │ ├── TodoList.jsx │ │ ├── TodoListFilters.jsx │ │ └── TodoQuickCheck.jsx │ ├── favicon.ico │ ├── index.html │ ├── index.js │ ├── store │ │ ├── atoms.js │ │ └── store.js │ └── styles │ │ └── styles.css └── webpack.config.js ├── demo-zustand-todo ├── .babelrc ├── LICENSE ├── __tests__ │ └── sampleTest.js ├── chromogen-4.0.4.tgz ├── package.json ├── src │ ├── components │ │ ├── App.jsx │ │ ├── Quotes.jsx │ │ ├── ReadOnlyTodoItem.jsx │ │ ├── SearchBar.jsx │ │ ├── TodoItem.jsx │ │ ├── TodoItemCreator.jsx │ │ ├── TodoList.jsx │ │ ├── TodoListFilters.jsx │ │ └── TodoQuickCheck.jsx │ ├── favicon.ico │ ├── index.html │ ├── index.js │ ├── store │ │ └── store.js │ └── styles │ │ └── styles.css └── webpack.config.js ├── jenkins ├── Jenkinsfile └── scripts │ ├── deliver.sh │ ├── kill.sh │ └── test.sh ├── package-lock.json ├── package.json └── package ├── LICENSE ├── README.md ├── babel.config.js ├── index.ts ├── package-lock.json ├── package.json ├── recoil_generator ├── __tests__ │ ├── api.test.js │ ├── component-utils.test.js │ ├── component.test.js │ ├── core-utils.test.jx │ ├── output-utils.test.js │ ├── output.test.js │ └── utils.test.js └── src │ ├── api │ ├── api.ts │ ├── core-utils.ts │ └── family-utils.ts │ ├── component │ ├── ChromogenObserver.tsx │ └── component-utils.ts │ ├── output │ ├── output-utils.ts │ └── output.ts │ ├── types.ts │ └── utils │ ├── ledger.ts │ ├── store.ts │ └── utils.ts ├── tsconfig.json └── zustand_generator ├── __tests__ ├── api.test.js └── output-utils.test.js └── src ├── GlobalStyle.ts ├── api └── api.ts ├── component ├── Buttons │ ├── RecordingButton.tsx │ ├── RecordingVariations │ │ ├── Record.tsx │ │ └── Start.tsx │ └── SecondaryButton.tsx ├── ChromogenZustandObserver.tsx ├── Editor.tsx ├── EditorTab.tsx ├── Header.tsx ├── Icons.tsx ├── Numbers.tsx ├── Resizing │ └── Resizer.tsx ├── component-utils.ts └── panel.tsx ├── output ├── output-utils.ts └── output.ts ├── types.ts └── utils ├── ledger.ts ├── store.ts └── utils.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "es2020": true, 7 | "jest": true 8 | }, 9 | "globals": { 10 | "chrome": "readonly" 11 | }, 12 | "extends": [ 13 | "airbnb", 14 | // added below for npm install -D eslint-config-airbnb-typescript --legacy-peer-deps 15 | "airbnb-typescript", 16 | "prettier", 17 | "prettier/react" 18 | ], 19 | "plugins": ["prettier"], 20 | "parserOptions": { 21 | "ecmaVersion": 2022, 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | }, 26 | "project": "./package/tsconfig.json", 27 | "tsconfigRootDir": "__dirname" 28 | }, 29 | "settings": { 30 | "import/extensions": [ 31 | ".js", 32 | ".jsx", 33 | ".ts", 34 | ".tsx" 35 | ], 36 | "import/parsers": { 37 | "@typescript-eslint/parser": [ 38 | ".ts", 39 | ".tsx" 40 | ] 41 | }, 42 | "import/resolver": { 43 | "typescript": { 44 | "directory": "./package/tsconfig.json" 45 | }, 46 | "node": { 47 | "extensions": [ 48 | ".js", 49 | ".jsx", 50 | ".ts", 51 | ".tsx" 52 | ] 53 | } 54 | }, 55 | "rules": { 56 | "prettier/prettier": ["warn"], 57 | "arrow-body-style": ["error", "as-needed"], 58 | "default-case-last": "error", 59 | "default-param-last": ["error"], 60 | "func-style": ["off", "expression"], 61 | "no-constant-condition": "error", 62 | "no-useless-call": "error", 63 | "prefer-exponentiation-operator": "error", 64 | "prefer-regex-literals": "error", 65 | "quotes": [ 66 | "error", 67 | "single", 68 | { 69 | "avoidEscape": true, 70 | "allowTemplateLiterals": false 71 | } 72 | ], 73 | "import/prefer-default-export": "off", 74 | "import/extensions": [ 75 | "error", 76 | "ignorePackages", 77 | { 78 | "js": "never", 79 | "jsx": "never", 80 | "ts": "never", 81 | "tsx": "never" 82 | } 83 | ], 84 | "react/jsx-filename-extension": ["off"], 85 | "react/function-component-definition": [ 86 | "error", 87 | { 88 | "namedComponents": "arrow-function", 89 | "unnamedComponents": "arrow-function" 90 | } 91 | ], 92 | "react/jsx-handler-names": [ 93 | "error", 94 | { 95 | "eventHandlerPrefix": "handle", 96 | "eventHandlerPropPrefix": "on" 97 | } 98 | ], 99 | "react/jsx-key": "error", 100 | "react/jsx-no-useless-fragment": "error", 101 | "react/jsx-sort-props": [ 102 | "error", 103 | { 104 | "callbacksLast": true, 105 | "shorthandFirst": true, 106 | "shorthandLast": false, 107 | "ignoreCase": true, 108 | "noSortAlphabetically": false, 109 | "reservedFirst": true 110 | } 111 | ], 112 | "react/no-adjacent-inline-elements": "error", 113 | "react/no-direct-mutation-state": "error", 114 | "react/no-multi-comp": "error", 115 | "react/prop-types": [ 116 | "error", 117 | { "skipUndeclared": true } 118 | ] 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/package" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Types of changes 2 | 3 | - [ ] Bugfix (change which fixes an issue) 4 | - [ ] New feature (change which adds functionality) 5 | - [ ] Refactor (change which changes the codebase without affecting its external behavior) 6 | - [ ] Non-breaking change (fix or feature that would causes existing functionality to work as expected) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to __not__ work as expected) 8 | ## Purpose 9 | 10 | ## Approach 11 | 12 | ## Resources 13 | 14 | ## Screenshot(s) 15 | 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | 4 | # System files 5 | .DS_Store 6 | .vscode 7 | 8 | # Tests 9 | coverage 10 | 11 | # Package 12 | package/build 13 | 14 | # Demo App 15 | chromogen.test.js 16 | package-lock.json 17 | TODO.md -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always", 13 | "endOfLine": "lf" 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | env: 5 | - TEST_DIR=package 6 | before_install: 7 | - cd $TEST_DIR 8 | install: 9 | - npm install 10 | script: 11 | - npm run test 12 | - npm run coveralls 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at chromogen.app@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jenkins/jenkins:2.375.3 2 | USER root 3 | RUN apt-get update && apt-get install -y lsb-release 4 | RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \ 5 | https://download.docker.com/linux/debian/gpg 6 | RUN echo "deb [arch=$(dpkg --print-architecture) \ 7 | signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \ 8 | https://download.docker.com/linux/debian \ 9 | $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list 10 | RUN apt-get update && apt-get install -y docker-ce-cli 11 | USER jenkins 12 | RUN jenkins-plugin-cli --plugins "blueocean docker-workflow" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/README-root/filepath-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/filepath-after.png -------------------------------------------------------------------------------- /assets/README-root/filepath-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/filepath-before.png -------------------------------------------------------------------------------- /assets/README-root/test-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/test-output.png -------------------------------------------------------------------------------- /assets/README-root/ultratrimmedDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/ultratrimmedDemo.gif -------------------------------------------------------------------------------- /assets/README-root/zustand-test-filepath-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-filepath-1.png -------------------------------------------------------------------------------- /assets/README-root/zustand-test-filepath-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-filepath-2.png -------------------------------------------------------------------------------- /assets/README-root/zustand-test-snapshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/README-root/zustand-test-snapshot-2.png -------------------------------------------------------------------------------- /assets/logo/Chromogen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/assets/logo/Chromogen.png -------------------------------------------------------------------------------- /demo-todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | //"react-hot-loader/babel" 8 | ] 9 | } -------------------------------------------------------------------------------- /demo-todo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michelle Holland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo-todo/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # The official demo app for [Chromogen](https://github.com/oslabs-beta/Chromogen). 4 | 5 | ![demo app interface](../assets/README-demo/demo-app.png) 6 | 7 |
8 | 9 | ## Selector Implementations 10 | - Readonly: 11 | 1. displayed todo list items, based on filter selection (sort & active/complete) 12 | 2. stats (priority count and active/complete counts) 13 | 3. displayed todo list empty / non-empty 14 | - Writeable: 15 | 1. "all complete" checkbox toggle 16 | 1. reset filter states 17 | - Promise: 18 | 1. quote text 19 | - Async / Await: 20 | 1. xkcd comic 21 | - selectorFamily (_in progress_): 22 | 1. search bar -------------------------------------------------------------------------------- /demo-todo/__tests__/initialTestTest.js: -------------------------------------------------------------------------------- 1 | import { renderRecoilHook, act } from 'react-recoil-hooks-testing-library'; 2 | import { useRecoilValue, useRecoilState } from 'recoil'; 3 | import { 4 | filteredTodoListState, 5 | sortedTodoListState, 6 | todoListSortedStats, 7 | todoListStatsState, 8 | filteredListContentState, 9 | allCompleteState, 10 | refreshFilterState, 11 | searchBarSelectorFam, 12 | 13 | } from '../src/store/store'; 14 | import { 15 | todoListState, 16 | todoListFilterState, 17 | todoListSortState, 18 | quoteNumberState, 19 | searchResultState, 20 | 21 | } from '../src/store/atoms'; 22 | 23 | // Suppress 'Batcher' warnings from React / Recoil conflict 24 | console.error = jest.fn(); 25 | 26 | // Hook to return atom/selector values and/or modifiers for react-recoil-hooks-testing-library 27 | const useStoreHook = () => { 28 | // atoms 29 | const [todoListStateValue, settodoListState] = useRecoilState(todoListState); 30 | const [todoListFilterStateValue, settodoListFilterState] = useRecoilState(todoListFilterState); 31 | const [todoListSortStateValue, settodoListSortState] = useRecoilState(todoListSortState); 32 | const [quoteNumberStateValue, setquoteNumberState] = useRecoilState(quoteNumberState); 33 | const [searchResultStateValue, setsearchResultState] = useRecoilState(searchResultState); 34 | 35 | // writeable selectors 36 | const [allCompleteStateValue, setallCompleteState] = useRecoilState(allCompleteState); 37 | const [refreshFilterStateValue, setrefreshFilterState] = useRecoilState(refreshFilterState); 38 | 39 | // read-only selectors 40 | const filteredTodoListStateValue = useRecoilValue(filteredTodoListState); 41 | const sortedTodoListStateValue = useRecoilValue(sortedTodoListState); 42 | const todoListSortedStatsValue = useRecoilValue(todoListSortedStats); 43 | const todoListStatsStateValue = useRecoilValue(todoListStatsState); 44 | const filteredListContentStateValue = useRecoilValue(filteredListContentState); 45 | 46 | // atom families 47 | 48 | // writeable selector families 49 | 50 | // read-only selector families 51 | 52 | 53 | 54 | 55 | return { 56 | todoListStateValue, 57 | settodoListState, 58 | todoListFilterStateValue, 59 | settodoListFilterState, 60 | todoListSortStateValue, 61 | settodoListSortState, 62 | quoteNumberStateValue, 63 | setquoteNumberState, 64 | searchResultStateValue, 65 | setsearchResultState, 66 | allCompleteStateValue, 67 | setallCompleteState, 68 | refreshFilterStateValue, 69 | setrefreshFilterState, 70 | filteredTodoListStateValue, 71 | sortedTodoListStateValue, 72 | todoListSortedStatsValue, 73 | todoListStatsStateValue, 74 | filteredListContentStateValue, 75 | }; 76 | }; 77 | 78 | describe('INITIAL RENDER', () => { 79 | const { result } = renderRecoilHook(useStoreHook); 80 | 81 | it('filteredTodoListState should initialize correctly', () => { 82 | expect(result.current.filteredTodoListStateValue).toStrictEqual([]); 83 | }); 84 | 85 | it('sortedTodoListState should initialize correctly', () => { 86 | expect(result.current.sortedTodoListStateValue).toStrictEqual([]); 87 | }); 88 | 89 | it('allCompleteState should initialize correctly', () => { 90 | expect(result.current.allCompleteStateValue).toStrictEqual(true); 91 | }); 92 | 93 | it('filteredListContentState should initialize correctly', () => { 94 | expect(result.current.filteredListContentStateValue).toStrictEqual(false); 95 | }); 96 | 97 | it('todoListSortedStats should initialize correctly', () => { 98 | expect(result.current.todoListSortedStatsValue).toStrictEqual({}); 99 | }); 100 | 101 | it('todoListStatsState should initialize correctly', () => { 102 | expect(result.current.todoListStatsStateValue).toStrictEqual({ "totalNum": 0, "totalCompletedNum": 0, "totalUncompletedNum": 0, "percentCompleted": 0 }); 103 | }); 104 | 105 | 106 | }); 107 | 108 | describe('SELECTORS', () => { 109 | it('todoListSortedStats should properly derive state when todoListState updates', () => { 110 | const { result } = renderRecoilHook(useStoreHook); 111 | 112 | act(() => { 113 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }]); 114 | 115 | result.current.settodoListFilterState("Show All"); 116 | 117 | result.current.settodoListSortState(false); 118 | 119 | result.current.setquoteNumberState(23); 120 | 121 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); 122 | 123 | 124 | 125 | }); 126 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 1 }); 127 | 128 | }); 129 | 130 | it('todoListSortedStats should properly derive state when todoListState updates', () => { 131 | const { result } = renderRecoilHook(useStoreHook); 132 | 133 | act(() => { 134 | result.current.settodoListState([{ "id": 1, "text": "tennis", "priority": "low", "isComplete": false }, { "id": 2, "text": "chinese chicken", "priority": "low", "isComplete": false }]); 135 | 136 | result.current.settodoListFilterState("Show All"); 137 | 138 | result.current.settodoListSortState(false); 139 | 140 | result.current.setquoteNumberState(23); 141 | 142 | result.current.setsearchResultState({ "all": { "searchTerm": "", "results": [] }, "high": { "searchTerm": "", "results": [] }, "medium": { "searchTerm": "", "results": [] }, "low": { "searchTerm": "", "results": [] } }); 143 | 144 | 145 | 146 | }); 147 | expect(result.current.todoListSortedStatsValue).toStrictEqual({ "low": 2 }); 148 | 149 | }); 150 | 151 | }); 152 | 153 | describe('SETTERS', () => { 154 | }); -------------------------------------------------------------------------------- /demo-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-todo", 3 | "version": "1.0.0", 4 | "description": "demo todo app for Chromogen using React + Recoil", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --open", 8 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --verbose" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "recoil", 13 | "chromogen", 14 | "demo", 15 | "example", 16 | "todo" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/open-source-labs/Chromogen.git" 21 | }, 22 | "contributors": [ 23 | { 24 | "name": "Michelle Holland", 25 | "url": "https://github.com/michellebholland/" 26 | }, 27 | { 28 | "name": "Jim Chen", 29 | "url": "https://github.com/chenchingk" 30 | }, 31 | { 32 | "name": "Andy Wang", 33 | "url": "https://github.com/andywang23" 34 | }, 35 | { 36 | "name": "Connor Rose Delisle", 37 | "url": "https://github.com/connorrose" 38 | } 39 | ], 40 | "license": "MIT", 41 | "devDependencies": { 42 | "@babel/core": "^7.11.1", 43 | "@babel/plugin-transform-runtime": "^7.11.0", 44 | "@babel/preset-env": "^7.19.1", 45 | "@babel/preset-react": "^7.10.4", 46 | "@testing-library/react": "^13.1.1", 47 | "babel-loader": "^8.1.0", 48 | "chromogen": "^4.0.4", 49 | "css-loader": "^4.2.1", 50 | "identity-obj-proxy": "^3.0.0", 51 | "jest": "^26.4.2", 52 | "style-loader": "^1.2.1", 53 | "webpack": "^5.74.0", 54 | "webpack-cli": "^3.3.12", 55 | "webpack-dev-server": "^4.11.1" 56 | }, 57 | "peerDependencies": { 58 | "typescript": "^4.0.3" 59 | }, 60 | "dependencies": { 61 | "@babel/runtime": "^7.11.2", 62 | "@emotion/react": "^11.10.4", 63 | "@emotion/styled": "^11.10.4", 64 | "@mui/icons-material": "^5.10.6", 65 | "@mui/material": "^5.10.6", 66 | "babel-jest": "^26.3.0", 67 | "react": "^18.0.0", 68 | "react-dom": "^18.0.0", 69 | "react-recoil-hooks-testing-library": "^0.1.0", 70 | "react-test-renderer": "^18.1.0", 71 | "recoil": "0.7.2", 72 | "typescript": "^4.0.3" 73 | }, 74 | "jest": { 75 | "moduleNameMapper": { 76 | "\\.(css|less)$": "identity-obj-proxy" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo-todo/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot } from 'recoil'; 3 | import { ChromogenObserver } from 'chromogen'; 4 | import TodoList from './TodoList'; 5 | import * as selectors from '../store/store'; 6 | import * as atoms from '../store/atoms'; 7 | 8 | const App = () => ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /demo-todo/src/components/Quotes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue, useSetRecoilState } from 'recoil'; 3 | import { quoteTextState, xkcdState } from '../store/store'; 4 | import { quoteNumberState } from '../store/atoms'; 5 | 6 | const Quotes = () => { 7 | const setQuoteNumber = useSetRecoilState(quoteNumberState); 8 | const quoteText = useRecoilValue(quoteTextState); 9 | 10 | return ( 11 | <> 12 |
13 |

{quoteText}

14 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Quotes; 23 | -------------------------------------------------------------------------------- /demo-todo/src/components/ReadOnlyTodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import '../styles/styles.css'; 4 | import { todoListState } from '../store/atoms'; 5 | import { useRecoilValue } from 'recoil'; 6 | 7 | const ReadOnlyTodoItem = ({ item }) => { 8 | const checkBoxClasses = { 9 | low: 'lowPriority', 10 | medium: 'mediumPriority', 11 | high: 'highPriority', 12 | }; 13 | 14 | const todoList = useRecoilValue(todoListState); 15 | 16 | return todoList.find((todo) => todo.id === item.id) ? ( 17 |
18 | 19 | todo.id === item.id).isComplete} 22 | color="default" 23 | inputProps={{ 'aria-label': 'primary checkbox' }} 24 | style={{ cursor: 'default' }} 25 | /> 26 |
27 | ) : null; 28 | }; 29 | export default ReadOnlyTodoItem; 30 | -------------------------------------------------------------------------------- /demo-todo/src/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import { searchBarSelectorFam } from '../store/store'; 4 | 5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem'; 6 | 7 | const SearchBar = () => { 8 | const [searchFilter, setSearchFilter] = useState('all'); 9 | const [searchText, setSearchText] = useState(''); 10 | const [searchState, setSearchState] = useRecoilState(searchBarSelectorFam(searchFilter)); 11 | 12 | const onSearchTextChange = (e) => { 13 | setSearchText(e.target.value); 14 | setSearchState(e.target.value); 15 | }; 16 | const onSelectChange = (e) => { 17 | setSearchText(''); 18 | setSearchFilter(e.target.value); 19 | }; 20 | 21 | return ( 22 |
23 | 31 | 37 |
38 | {searchState.results.map((result, idx) => ( 39 | 40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default SearchBar; 47 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | import Checkbox from '@mui/material/Checkbox'; 4 | import { todoListState } from '../store/atoms'; 5 | import '../styles/styles.css'; 6 | 7 | function replaceItemAtIndex(arr, index, newValue) { 8 | return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; 9 | } 10 | 11 | function removeItemAtIndex(arr, index) { 12 | return [...arr.slice(0, index), ...arr.slice(index + 1)]; 13 | } 14 | 15 | const TodoItem = ({ item }) => { 16 | const [todoList, setTodoList] = useRecoilState(todoListState); 17 | const index = todoList.findIndex((listItem) => listItem === item); 18 | 19 | const editItemText = ({ target: { value } }) => { 20 | const newList = replaceItemAtIndex(todoList, index, { 21 | ...item, 22 | text: value, 23 | }); 24 | setTodoList(newList); 25 | }; 26 | const toggleItemCompletion = () => { 27 | const newList = replaceItemAtIndex(todoList, index, { 28 | ...item, 29 | isComplete: !item.isComplete, 30 | }); 31 | setTodoList(newList); 32 | }; 33 | const deleteItem = () => { 34 | const newList = removeItemAtIndex(todoList, index); 35 | setTodoList(newList); 36 | }; 37 | 38 | const checkBoxClasses = { 39 | low: 'lowPriority', 40 | medium: 'mediumPriority', 41 | high: 'highPriority', 42 | }; 43 | 44 | return ( 45 |
46 | 47 | 54 | 57 |
58 | ); 59 | }; 60 | export default TodoItem; 61 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoItemCreator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState } from 'react'; 3 | import RadioGroup from '@mui/material/RadioGroup'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormLabel from '@mui/material/FormLabel'; 7 | import Radio from '@mui/material/Radio'; 8 | import { useSetRecoilState } from 'recoil'; 9 | import { todoListState } from '../store/atoms'; 10 | 11 | // utility for creating unique Id 12 | let id = 0; 13 | const getId = () => { 14 | id += 1; 15 | return id; 16 | }; 17 | 18 | const TodoItemCreator = () => { 19 | const [inputValue, setInputValue] = useState(''); 20 | const [priorityValue, setPriorityValue] = useState('low'); 21 | const setTodoList = useSetRecoilState(todoListState); 22 | 23 | const addItem = () => { 24 | setTodoList((oldTodoList) => [ 25 | ...oldTodoList, 26 | { 27 | id: getId(), 28 | text: inputValue, 29 | priority: priorityValue, 30 | isComplete: false, 31 | }, 32 | ]); 33 | setInputValue(''); 34 | setPriorityValue('low'); 35 | }; 36 | 37 | const onChange = ({ target: { value } }) => { 38 | setInputValue(value); 39 | }; 40 | 41 | const handleChange = (event) => { 42 | setPriorityValue(event.target.value); 43 | }; 44 | 45 | /* MUI Radio Button styles */ 46 | const GreenRadio = (props) => ; 47 | 48 | const YellowRadio = (props) => ; 49 | 50 | const RedRadio = (props) => ; 51 | 52 | return ( 53 |
54 | 61 | 62 | 63 | 64 | 71 | } value="high" /> 72 | } value="medium" /> 73 | } value="low" /> 74 | 75 | 76 | 77 | 78 | 81 |
82 | ); 83 | }; 84 | 85 | export default TodoItemCreator; 86 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { sortedTodoListState } from '../store/store'; 4 | import TodoItem from './TodoItem'; 5 | import TodoItemCreator from './TodoItemCreator'; 6 | import TodoListFilters from './TodoListFilters'; 7 | import TodoQuickCheck from './TodoQuickCheck'; 8 | import Quotes from './Quotes'; 9 | import SearchBar from './SearchBar'; 10 | import '../styles/styles.css'; 11 | 12 | const TodoList = () => { 13 | const todoList = useRecoilValue(sortedTodoListState); 14 | 15 | return ( 16 |
17 |
18 | Loading...}> 19 | 20 | 21 |
22 | 23 |
24 |

Totally Todos!

25 | 26 |
27 | 28 | 29 | {todoList.map((todoItem) => ( 30 | 31 | ))} 32 | 33 |
34 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default TodoList; 42 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoListFilters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SortIcon from '@mui/icons-material/Sort'; 3 | import EqualizerIcon from '@mui/icons-material/Equalizer'; 4 | import RefreshIcon from '@mui/icons-material/Refresh'; 5 | import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; 6 | import { todoListStatsState, todoListSortedStats, refreshFilterState } from '../store/store'; 7 | import { todoListFilterState, todoListSortState } from '../store/atoms'; 8 | 9 | const TodoListFilters = () => { 10 | const [filter, setFilter] = useRecoilState(todoListFilterState); 11 | // selector - grabs totals for each category 12 | const { high, medium, low } = useRecoilValue(todoListSortedStats); 13 | // selector *writeable - resets sort and filter 14 | const resetFilters = useResetRecoilState(refreshFilterState); 15 | // selector - toggles sort on and off 16 | const [sort, setSort] = useRecoilState(todoListSortState); 17 | // toggle priority stats display 18 | const [displayStats, setDisplayStats] = useState(false); 19 | // selector - totals for each filter 20 | const { totalNum, totalCompletedNum, totalUncompletedNum } = useRecoilValue(todoListStatsState); 21 | const updateFilter = ({ target: { value } }) => setFilter(value); 22 | 23 | const toggleSort = () => setSort(!sort); 24 | const toggleDisplayStats = () => setDisplayStats(!displayStats); 25 | const reset = () => { 26 | setDisplayStats(false); // displayStats is local state 27 | resetFilters(); 28 | }; 29 | 30 | const sortIconColor = { 31 | true: 'sortedWhite', 32 | false: 'aqua', 33 | }; 34 | 35 | return ( 36 |
    37 | 47 | 57 | 66 | 69 | 70 | 81 | 84 |
85 | ); 86 | }; 87 | 88 | export default TodoListFilters; 89 | -------------------------------------------------------------------------------- /demo-todo/src/components/TodoQuickCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilState, useRecoilValue } from 'recoil'; 3 | import Checkbox from '@mui/material/Checkbox'; 4 | import { allCompleteState, filteredListContentState } from '../store/store'; 5 | 6 | const TodoQuickCheck = () => { 7 | const [allComplete, setAllComplete] = useRecoilState(allCompleteState); 8 | const display = useRecoilValue(filteredListContentState); 9 | 10 | return ( 11 | display && ( 12 |
13 | setAllComplete(!allComplete)} 19 | /> 20 | all 21 |
22 | ) 23 | ); 24 | }; 25 | 26 | export default TodoQuickCheck; 27 | -------------------------------------------------------------------------------- /demo-todo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-todo/src/favicon.ico -------------------------------------------------------------------------------- /demo-todo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Chromogen To-Do Demo 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /demo-todo/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | //import { render } from 'react-dom'; 4 | import App from './components/App'; 5 | import { createRoot } from'react-dom/client'; 6 | 7 | const root = createRoot(document.getElementById('app')); 8 | 9 | root.render( 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /demo-todo/src/store/atoms.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'chromogen'; 2 | 3 | /* ----- ATOMS ----- */ 4 | 5 | // unsorted, unfiltered todo list 6 | const todoListState = atom({ 7 | key: 'mismatchTodoList', 8 | default: [], // array of objects - each object has id, text, isComplete, and priority props 9 | }); 10 | 11 | // filter select 12 | const todoListFilterState = atom({ 13 | key: 'todoListFilterState', 14 | default: 'Show All', 15 | }); 16 | 17 | // toggle sort 18 | const todoListSortState = atom({ 19 | key: 'todoListSortState', 20 | default: false, 21 | }); 22 | 23 | // random number for fetching quote & comic 24 | const quoteNumberState = atom({ 25 | key: 'quoteNumberState', 26 | default: Math.floor(Math.random() * 1643), 27 | }); 28 | 29 | const searchResultState = atom({ 30 | key: 'searchResultState', 31 | default: { 32 | all: { 33 | searchTerm: '', 34 | results: [], 35 | }, 36 | high: { 37 | searchTerm: '', 38 | results: [], 39 | }, 40 | medium: { 41 | searchTerm: '', 42 | results: [], 43 | }, 44 | low: { 45 | searchTerm: '', 46 | results: [], 47 | }, 48 | }, 49 | }); 50 | 51 | export { 52 | todoListState, 53 | todoListFilterState, 54 | todoListSortState, 55 | quoteNumberState, 56 | searchResultState, 57 | }; 58 | -------------------------------------------------------------------------------- /demo-todo/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { selector, selectorFamily } from 'chromogen'; 2 | import { 3 | todoListState, 4 | todoListFilterState, 5 | todoListSortState, 6 | quoteNumberState, 7 | searchResultState, 8 | } from './atoms'; 9 | 10 | /* ----- SELECTORS ---- */ 11 | 12 | // filtered todo list 13 | const filteredTodoListState = selector({ 14 | key: 'filteredTodoListState', 15 | get: ({ get }) => { 16 | const filter = get(todoListFilterState); 17 | const list = get(todoListState); 18 | 19 | switch (filter) { 20 | case 'Show Completed': 21 | return list.filter((item) => item.isComplete); 22 | case 'Show Uncompleted': 23 | return list.filter((item) => !item.isComplete); 24 | default: 25 | return list; 26 | } 27 | }, 28 | }); 29 | 30 | // sorted todo list 31 | const sortedTodoListState = selector({ 32 | key: 'mismatchSortedTodoList', 33 | get: ({ get }) => { 34 | const sort = get(todoListSortState); 35 | const list = get(filteredTodoListState); 36 | const high = list.filter((item) => item.priority === 'high'); 37 | const medium = list.filter((item) => item.priority === 'medium'); 38 | const low = list.filter((item) => item.priority === 'low'); 39 | return sort === false ? list : [...high, ...medium, ...low]; 40 | }, 41 | }); 42 | 43 | // priority stats 44 | const todoListSortedStats = selector({ 45 | key: 'todoListSortedStats', 46 | get: ({ get }) => { 47 | const list = get(sortedTodoListState); 48 | return list.reduce((acc, cv) => { 49 | acc[cv.priority] = cv.priority in acc ? acc[cv.priority] + 1 : 1; 50 | return acc; 51 | }, {}); 52 | }, 53 | }); 54 | 55 | // completion (filter) stats 56 | const todoListStatsState = selector({ 57 | key: 'todoListStatsState', 58 | get: ({ get }) => { 59 | const list = get(todoListState); 60 | const totalNum = list.length; 61 | const totalCompletedNum = list.filter((todo) => todo.isComplete).length; 62 | const totalUncompletedNum = totalNum - totalCompletedNum; 63 | const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; 64 | return { 65 | totalNum, 66 | totalCompletedNum, 67 | totalUncompletedNum, 68 | percentCompleted, 69 | }; 70 | }, 71 | }); 72 | 73 | // is filtered list non-empty? (determines whether check-all displays) 74 | const filteredListContentState = selector({ 75 | key: 'filteredListContentState', 76 | get: ({ get }) => !!get(filteredTodoListState).length, 77 | }); 78 | 79 | // WRITEABLE GET/SET SELECTOR - (un)check all filtered items 80 | const allCompleteState = selector({ 81 | key: 'mismatchAllComplete', 82 | // if any item in filteredList is not complete, allComplete is false 83 | get: ({ get }) => !get(filteredTodoListState).some(({ isComplete }) => !isComplete), 84 | set: ({ get, set }, newValue) => { 85 | // update ONLY items in filtered list 86 | const lookupTable = {}; 87 | get(todoListState).forEach((item) => { 88 | lookupTable[item.id] = item; 89 | }); 90 | get(filteredTodoListState).forEach((item) => { 91 | lookupTable[item.id] = { 92 | ...item, 93 | isComplete: newValue, 94 | }; 95 | }); 96 | set(todoListState, Object.values(lookupTable)); 97 | }, 98 | }); 99 | 100 | // WRITEABLE RESET SELECTOR - undo sort + filter 101 | const refreshFilterState = selector({ 102 | key: 'refreshFilterState', 103 | get: () => null, 104 | set: ({ reset }) => { 105 | reset(todoListSortState); 106 | reset(todoListFilterState); 107 | }, 108 | }); 109 | 110 | // PROMISE-BASED SELECTOR - fetch quote text 111 | const quoteTextState = selector({ 112 | key: 'quoteTextState', 113 | get: ({ get }) => { 114 | const quoteNumber = get(quoteNumberState); 115 | return fetch('https://type.fit/api/quotes') 116 | .then((response) => response.json()) 117 | .then((data) => { 118 | const quote = data[quoteNumber]; 119 | return `"${quote.text}"\n\t- ${quote.author || 'unknown'}`; 120 | }) 121 | .catch((err) => { 122 | console.error(err); 123 | return 'No quote available'; 124 | }); 125 | }, 126 | }); 127 | 128 | // ASYNC SELECTOR - fetch comic img 129 | // const xkcdState = selector({ 130 | // key: 'xkcdState', 131 | // get: async ({ get }) => { 132 | // const quoteNumber = get(quoteNumberState); 133 | // try { 134 | // // Fetch much be proxied through cors-anywhere to test on localhost 135 | // const response = await fetch( 136 | // `https://cors-anywhere.herokuapp.com/http://xkcd.com/${quoteNumber}/info.0.json`, 137 | // ); 138 | // const { img } = await response.json(); 139 | // return img; 140 | // } catch (err) { 141 | // // Fallback comic 142 | // return 'https://imgs.xkcd.com/comics/api.png'; 143 | // } 144 | // }, 145 | // }); 146 | 147 | const searchBarSelectorFam = selectorFamily({ 148 | key: 'searchBarSelectorFam', 149 | get: 150 | (searchFilter) => 151 | ({ get }) => 152 | get(searchResultState)[searchFilter], 153 | set: 154 | (searchFilter) => 155 | ({ get, set }, searchTerm) => { 156 | set(searchResultState, (prevState) => { 157 | const newResults = get(todoListState).filter((todo) => { 158 | if (searchTerm !== '' && todo.text.includes(searchTerm)) 159 | return searchFilter === 'all' ? true : todo.priority === searchFilter; 160 | return false; 161 | }); 162 | return { ...prevState, [searchFilter]: { searchTerm, results: newResults } }; 163 | }); 164 | }, 165 | }); 166 | 167 | export { 168 | filteredTodoListState, 169 | filteredListContentState, 170 | todoListStatsState, 171 | allCompleteState, 172 | sortedTodoListState, 173 | todoListSortedStats, 174 | refreshFilterState, 175 | quoteTextState, 176 | //xkcdState, 177 | searchBarSelectorFam, 178 | }; 179 | -------------------------------------------------------------------------------- /demo-todo/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* -------------General Styles---------------- */ 2 | html { 3 | margin: 0; 4 | background-color: rgb(48, 48, 48); 5 | color: whitesmoke; 6 | font-family: 'Palanquin', sans-serif; 7 | overflow: hidden; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | font-size: 2.3rem; 13 | color: #af6358; 14 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); 15 | letter-spacing: 3px; 16 | font-style: italic; 17 | } 18 | input, 19 | button, 20 | select { 21 | background-color: rgb(63, 63, 63); 22 | border: 1px solid lightgray; 23 | border: none; 24 | padding: 20px; 25 | border-radius: 4px; 26 | color: whitesmoke; 27 | font-size: 16px; 28 | letter-spacing: 1px; 29 | } 30 | 31 | /* remove browser defaults */ 32 | button:focus { 33 | outline: none; 34 | } 35 | input:focus { 36 | outline: none; 37 | } 38 | /* ------------TodoList------------- */ 39 | 40 | /* topmost container */ 41 | .mainContainer { 42 | display: grid; 43 | height: 100vh; 44 | width: 100vw; 45 | grid-template-rows: 15fr 33fr 33fr; 46 | } 47 | 48 | /* overall row container for todo list display */ 49 | .todosDisplayRow { 50 | margin: 0 auto; 51 | width: 700px; 52 | } 53 | 54 | /* container for entire list display */ 55 | .todosContainer { 56 | background-color: rgb(63, 63, 63); 57 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 58 | border-radius: 5px; 59 | padding: 5px 12px 0 12px; 60 | } 61 | 62 | /* -------------TodoItemCreator---------------- */ 63 | 64 | .itemCreator button { 65 | padding: 0px; 66 | } 67 | 68 | input::placeholder { 69 | font-style: italic; 70 | letter-spacing: 1.5px; 71 | } 72 | 73 | .itemCreator input { 74 | padding: 0 0 0 5%; 75 | width: 70%; 76 | height: 60px; 77 | } 78 | 79 | #radioContainer svg { 80 | margin-top: 11px; 81 | opacity: 0.7; 82 | } 83 | 84 | /* -------------TodoItem---------------- */ 85 | 86 | .itemContainer, 87 | .lowPriority, 88 | .mediumPriority, 89 | .highPriority { 90 | display: grid; 91 | grid-template-columns: 79fr 14fr 7fr; 92 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 93 | } 94 | 95 | /* dynamic checkbox color */ 96 | .highPriority svg { 97 | color: #ef5350; 98 | opacity: 0.7; 99 | margin-right: 28px; 100 | } 101 | .mediumPriority svg { 102 | color: #ffee58; 103 | opacity: 0.7; 104 | margin-right: 28px; 105 | } 106 | .lowPriority svg { 107 | color: #66bb6a; 108 | opacity: 0.7; 109 | margin-right: 28px; 110 | } 111 | 112 | #todoItem button { 113 | margin-left: 7px; 114 | } 115 | 116 | /* -------------TodoListFilter---------------- */ 117 | 118 | ul { 119 | display: grid; 120 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; 121 | margin: 0; 122 | padding: 0px; 123 | width: 100%; 124 | } 125 | 126 | .filter-button { 127 | margin-top: 10px; 128 | margin-bottom: 10px; 129 | border-right: 1px solid rgba(143, 143, 143, 0.26); 130 | padding: 10px 20px; 131 | border-radius: 0px 0px 4px 4px; 132 | } 133 | 134 | /* dynamic sort icon color */ 135 | #sortedWhite svg { 136 | color: whitesmoke; 137 | } 138 | #unsortedGray svg { 139 | color: rgba(245, 245, 245, 0.336); 140 | } 141 | #unsortedGray { 142 | width: 64px; 143 | } 144 | 145 | #statsSpan { 146 | display: grid; 147 | grid-template-columns: 30fr 30fr 30fr; 148 | align-items: center; 149 | } 150 | #highSpan { 151 | color: #ef5350; 152 | opacity: 0.7; 153 | margin-right: 4px; 154 | } 155 | #mediumSpan { 156 | color: #ffee58; 157 | opacity: 0.7; 158 | margin-right: 4px; 159 | } 160 | #lowSpan { 161 | color: #66bb6a; 162 | opacity: 0.7; 163 | } 164 | 165 | /* filter stats (number) */ 166 | button span { 167 | opacity: 0.6; 168 | } 169 | 170 | /* ----------QuoteBox---------- */ 171 | 172 | #quoteContainer { 173 | display: flex; 174 | flex-direction: column; 175 | justify-content: space-between; 176 | } 177 | 178 | #quoteContainer p { 179 | white-space: pre-wrap; 180 | } 181 | 182 | .quoteBox { 183 | display: flex; 184 | flex-direction: row; 185 | justify-content: space-between; 186 | background-color: rgb(63, 63, 63); 187 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 188 | border-radius: 5px; 189 | margin: auto; 190 | padding: 12px; 191 | } 192 | 193 | .quoteBox img { 194 | margin: 20px; 195 | height: 150px; 196 | width: 150px; 197 | } 198 | 199 | .quoteBox button { 200 | width: 150px; 201 | } 202 | 203 | .quoteBox button:hover { 204 | border: 1px solid whitesmoke; 205 | cursor: pointer; 206 | } 207 | 208 | /* ---------- TodoQuickCheck ---------- */ 209 | 210 | #quickCheck { 211 | font-family: Arial, Helvetica, sans-serif; 212 | } 213 | 214 | #quickCheck svg { 215 | color: #af6358; 216 | } 217 | 218 | /* ---------- Search ---------- */ 219 | .searchContainer { 220 | margin-top: 100px; 221 | background-color: rgb(63, 63, 63); 222 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 223 | border-radius: 5px; 224 | padding: 5px 12px 12px 12px; 225 | display: grid; 226 | grid-template-columns: 70% 30%; 227 | } 228 | 229 | .searchField { 230 | border-bottom: solid 1px whitesmoke; 231 | border-radius: 0; 232 | } 233 | 234 | .prioritySelect { 235 | grid-column-start: 2; 236 | border-bottom: solid 1px whitesmoke; 237 | border-radius: 0; 238 | } 239 | 240 | .searchResults { 241 | grid-column-start: span 3; 242 | } 243 | -------------------------------------------------------------------------------- /demo-todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, './src/index.js'), 5 | output: { 6 | filename: 'bundle.js', 7 | }, 8 | devServer: { 9 | contentBase: path.resolve(__dirname, './src'), 10 | historyApiFallback: true, 11 | }, 12 | mode: process.env.NODE_ENV, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | plugins: ['@babel/transform-runtime'], 22 | }, 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | resolve: { 38 | extensions: ['.js', '.jsx'], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /demo-zustand-todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"], 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | //"react-hot-loader/babel" 8 | ] 9 | } -------------------------------------------------------------------------------- /demo-zustand-todo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michelle Holland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo-zustand-todo/__tests__/sampleTest.js: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import useStore from '../src/store/store'; 3 | 4 | describe('INITIAL RENDER', () => { 5 | const { result } = renderHook(useStore); 6 | 7 | it('todoListState should initialize correctly', () => { 8 | expect(result.current.todoListState).toStrictEqual([]); 9 | }); 10 | 11 | it('todoListFilterState should initialize correctly', () => { 12 | expect(result.current.todoListFilterState).toStrictEqual('Show All'); 13 | }); 14 | 15 | it('todoListSortState should initialize correctly', () => { 16 | expect(result.current.todoListSortState).toStrictEqual(false); 17 | }); 18 | 19 | it('quoteText should initialize correctly', () => { 20 | expect(result.current.quoteText).toStrictEqual(''); 21 | }); 22 | 23 | it('quoteNumber should initialize correctly', () => { 24 | expect(result.current.quoteNumber).toStrictEqual(0); 25 | }); 26 | 27 | it('checkBox should initialize correctly', () => { 28 | expect(result.current.checkBox).toStrictEqual(false); 29 | }); 30 | 31 | it('searchResultState should initialize correctly', () => { 32 | expect(result.current.searchResultState).toStrictEqual({ 33 | all: { searchTerm: '', results: [] }, 34 | high: { searchTerm: '', results: [] }, 35 | medium: { searchTerm: '', results: [] }, 36 | low: { searchTerm: '', results: [] }, 37 | }); 38 | }); 39 | }); 40 | 41 | describe('STATE CHANGES', () => { 42 | const { result } = renderHook(useStore); 43 | 44 | it('checkBox & quoteText & todoListState should update correctly', () => { 45 | const { result } = renderHook(useStore); 46 | 47 | act(() => { 48 | result.current.setCheckBox(); 49 | result.current.setCheckBox(); 50 | result.current.changeQuoteText( 51 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', 52 | ); 53 | result.current.addTodoListItem({ id: 2, text: 'tennis', priority: 'low', isComplete: false }); 54 | }); 55 | 56 | expect(result.current.checkBox).toStrictEqual(true); 57 | expect(result.current.quoteText).toStrictEqual( 58 | '"Your ability to learn faster than your competition is your only sustainable competitive advantage."\n\t- Arie de Gues', 59 | ); 60 | expect(result.current.todoListState).toStrictEqual([ 61 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 62 | ]); 63 | }); 64 | it('checkBox & todoListState should update correctly', () => { 65 | const { result } = renderHook(useStore); 66 | 67 | act(() => { 68 | result.current.setCheckBox(); 69 | result.current.setCheckBox(); 70 | result.current.addTodoListItem({ id: 3, text: 'hockey', priority: 'low', isComplete: false }); 71 | result.current.setCheckBox(); 72 | }); 73 | 74 | expect(result.current.checkBox).toStrictEqual(false); 75 | expect(result.current.todoListState).toStrictEqual([ 76 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 77 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 78 | ]); 79 | }); 80 | it('todoListState should update correctly', () => { 81 | const { result } = renderHook(useStore); 82 | 83 | act(() => { 84 | result.current.addTodoListItem({ id: 4, text: 'hocka', priority: 'low', isComplete: false }); 85 | result.current.setCheckBox(); 86 | }); 87 | 88 | expect(result.current.todoListState).toStrictEqual([ 89 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 90 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 91 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 92 | ]); 93 | }); 94 | it('todoListState & searchResultState should update correctly', () => { 95 | const { result } = renderHook(useStore); 96 | 97 | act(() => { 98 | result.current.addTodoListItem({ id: 5, text: 'canoe', priority: 'low', isComplete: false }); 99 | result.current.setCheckBox(); 100 | result.current.setSearchState('c', 'all'); 101 | }); 102 | 103 | expect(result.current.todoListState).toStrictEqual([ 104 | { id: 2, text: 'tennis', priority: 'low', isComplete: false }, 105 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 106 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 107 | { id: 5, text: 'canoe', priority: 'low', isComplete: false }, 108 | ]); 109 | expect(result.current.searchResultState).toStrictEqual({ 110 | all: { 111 | searchTerm: 'c', 112 | results: [ 113 | { id: 3, text: 'hockey', priority: 'low', isComplete: false }, 114 | { id: 4, text: 'hocka', priority: 'low', isComplete: false }, 115 | { id: 5, text: 'canoe', priority: 'low', isComplete: false }, 116 | ], 117 | }, 118 | high: { searchTerm: '', results: [] }, 119 | medium: { searchTerm: '', results: [] }, 120 | low: { searchTerm: '', results: [] }, 121 | }); 122 | }); 123 | it('searchResultState should update correctly', () => { 124 | const { result } = renderHook(useStore); 125 | 126 | act(() => { 127 | result.current.setSearchState('ca', 'all'); 128 | }); 129 | 130 | expect(result.current.searchResultState).toStrictEqual({ 131 | all: { 132 | searchTerm: 'ca', 133 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 134 | }, 135 | high: { searchTerm: '', results: [] }, 136 | medium: { searchTerm: '', results: [] }, 137 | low: { searchTerm: '', results: [] }, 138 | }); 139 | }); 140 | it('searchResultState should update correctly', () => { 141 | const { result } = renderHook(useStore); 142 | 143 | act(() => { 144 | result.current.setSearchState('can', 'all'); 145 | }); 146 | 147 | expect(result.current.searchResultState).toStrictEqual({ 148 | all: { 149 | searchTerm: 'can', 150 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 151 | }, 152 | high: { searchTerm: '', results: [] }, 153 | medium: { searchTerm: '', results: [] }, 154 | low: { searchTerm: '', results: [] }, 155 | }); 156 | }); 157 | it('searchResultState should update correctly', () => { 158 | const { result } = renderHook(useStore); 159 | 160 | act(() => { 161 | result.current.setSearchState('cano', 'all'); 162 | }); 163 | 164 | expect(result.current.searchResultState).toStrictEqual({ 165 | all: { 166 | searchTerm: 'cano', 167 | results: [{ id: 5, text: 'canoe', priority: 'low', isComplete: false }], 168 | }, 169 | high: { searchTerm: '', results: [] }, 170 | medium: { searchTerm: '', results: [] }, 171 | low: { searchTerm: '', results: [] }, 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /demo-zustand-todo/chromogen-4.0.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-zustand-todo/chromogen-4.0.4.tgz -------------------------------------------------------------------------------- /demo-zustand-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-todo", 3 | "version": "1.0.2", 4 | "description": "demo todo app for Chromogen using React + Recoil", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --open", 8 | "test": "jest --verbose", 9 | "update": " npm run uninstall && npm run install && npm run start", 10 | "uninstall": "npm uninstall chromogen", 11 | "install": "npm install ../package", 12 | "buildPackage": "tsc", 13 | "tarballUpdate": "npm --prefix ../package run build && npm pack ../package && npm uninstall chromogen && npm install ./chromogen-5.0.1.tgz && npm start" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "recoil", 18 | "chromogen", 19 | "demo", 20 | "example", 21 | "todo" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/open-source-labs/Chromogen.git" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "Brach Burdick", 30 | "url": "https://github.com/sirbrachthepale/" 31 | }, 32 | { 33 | "name": "Francois Denavaut", 34 | "url": "https://github.com/dnvt/" 35 | }, 36 | { 37 | "name": "Maggie Kwan", 38 | "url": "https://github.com/maggiekwan/" 39 | }, 40 | { 41 | "name": "Lawrence Liang", 42 | "url": "https://github.com/Lawliang/" 43 | }, 44 | { 45 | "name": "Michelle Holland", 46 | "url": "https://github.com/michellebholland/" 47 | }, 48 | { 49 | "name": "Jim Chen", 50 | "url": "https://github.com/chenchingk" 51 | }, 52 | { 53 | "name": "Andy Wang", 54 | "url": "https://github.com/andywang23" 55 | }, 56 | { 57 | "name": "Connor Rose Delisle", 58 | "url": "https://github.com/connorrose" 59 | } 60 | ], 61 | "license": "MIT", 62 | "devDependencies": { 63 | "@babel/core": "^7.11.1", 64 | "@babel/plugin-transform-runtime": "^7.11.0", 65 | "@babel/preset-env": "^7.11.0", 66 | "@babel/preset-react": "^7.10.4", 67 | "@testing-library/react": "^13.1.1", 68 | "babel-loader": "^8.1.0", 69 | "css-loader": "^4.2.1", 70 | "identity-obj-proxy": "^3.0.0", 71 | "jest": "^26.4.2", 72 | "prettier": "^2.7.1", 73 | "style-loader": "^1.2.1", 74 | "webpack": "^4.44.1", 75 | "webpack-cli": "^3.3.12", 76 | "webpack-dev-server": "^3.11.0" 77 | }, 78 | "peerDependencies": { 79 | "typescript": "^4.0.3" 80 | }, 81 | "dependencies": { 82 | "@babel/runtime": "^7.11.2", 83 | "@emotion/react": "^11.10.5", 84 | "@emotion/styled": "^11.10.4", 85 | "@mui/icons-material": "^5.10.6", 86 | "@mui/material": "^5.10.6", 87 | "babel-jest": "^26.3.0", 88 | "chromogen": "file:chromogen-4.0.4.tgz", 89 | "file-loader": "^6.2.0", 90 | "react": "^18.0.0", 91 | "react-dom": "^18.0.0", 92 | "react-test-renderer": "^18.1.0", 93 | "recoil": "0.7.5", 94 | "typescript": "^4.0.3", 95 | "url-loader": "^4.1.1", 96 | "zustand": "^4.1.1" 97 | }, 98 | "jest": { 99 | "moduleNameMapper": { 100 | "\\.(css|less)$": "identity-obj-proxy" 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChromogenZustandObserver } from 'chromogen'; 3 | import TodoList from './TodoList'; 4 | import '../styles/styles.css'; 5 | 6 | const App = () => ( 7 | <> 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/Quotes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import shallow from 'zustand/shallow'; 3 | import useToDoStore from '../store/store'; 4 | import { useEffect } from 'react'; 5 | 6 | const selector = (state) => ({ 7 | changeQuoteText: state.changeQuoteText, 8 | quoteText: state.quoteText, 9 | }); 10 | 11 | const Quotes = () => { 12 | const { changeQuoteText, quoteText } = useToDoStore(selector, shallow); 13 | 14 | const fetchMe = () => { 15 | let randomNum = Math.floor(Math.random() * 1643); 16 | 17 | fetch('https://type.fit/api/quotes') 18 | .then((response) => response.json()) 19 | .then((data) => { 20 | const quote = data[randomNum]; 21 | changeQuoteText(`"${quote.text}"\n\t- ${quote.author || 'unknown'}`); 22 | }) 23 | .catch((err) => { 24 | console.error(err); 25 | return 'No quote available'; 26 | }); 27 | }; 28 | 29 | useEffect(() => fetchMe(), []); 30 | 31 | return ( 32 | <> 33 |
34 |

{quoteText}

35 | fetchMe()}>New Quote 36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Quotes; 42 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/ReadOnlyTodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import '../styles/styles.css'; 4 | import useToDoStore from '../store/store'; 5 | 6 | const ReadOnlyTodoItem = ({ item }) => { 7 | const checkBoxClasses = { 8 | low: 'lowPriority', 9 | medium: 'mediumPriority', 10 | high: 'highPriority', 11 | }; 12 | 13 | const todoList = useToDoStore((state) => state.todoListState); 14 | 15 | return todoList.find((todo) => todo.id === item.id) ? ( 16 |
17 | 18 | todo.id === item.id).isComplete} 21 | color="default" 22 | inputProps={{ 'aria-label': 'primary checkbox' }} 23 | style={{ cursor: 'default' }} 24 | /> 25 |
26 | ) : null; 27 | }; 28 | export default ReadOnlyTodoItem; 29 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import useToDoStore from '../store/store'; 3 | import shallow from 'zustand/shallow'; 4 | 5 | import ReadOnlyTodoItem from './ReadOnlyTodoItem'; 6 | 7 | const selector = (state) => ({ 8 | searchResultState: state.searchResultState, 9 | setSearchState: state.setSearchState, 10 | }); 11 | 12 | const SearchBar = () => { 13 | const [searchFilter, setSearchFilter] = useState('all'); 14 | const [searchText, setSearchText] = useState(''); 15 | const { searchResultState, setSearchState } = useToDoStore(selector, shallow); 16 | const searchResults = searchResultState[searchFilter]; 17 | 18 | const onSearchTextChange = (e) => { 19 | setSearchText(e.target.value); 20 | setSearchState(e.target.value, searchFilter); 21 | }; 22 | const onSelectChange = (e) => { 23 | setSearchText(''); 24 | setSearchFilter(e.target.value); 25 | }; 26 | 27 | return ( 28 |
29 | 37 | 47 |
48 | {searchResults.results.map((result, idx) => ( 49 | 50 | ))} 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default SearchBar; 57 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import '../styles/styles.css'; 4 | import shallow from 'zustand/shallow'; 5 | import useToDoStore from '../store/store'; 6 | 7 | const selector = (state) => ({ 8 | todoListState: state.todoListState, 9 | deleteTodoListItem: state.deleteTodoListItem, 10 | editItemText: state.editItemText, 11 | toggleItemCompletion: state.toggleItemCompletion, 12 | }); 13 | 14 | const TodoItem = ({ item }) => { 15 | const { deleteTodoListItem, editItemText, toggleItemCompletion } = useToDoStore( 16 | selector, 17 | shallow, 18 | ); 19 | 20 | const checkBoxClasses = { 21 | low: 'lowPriority', 22 | medium: 'mediumPriority', 23 | high: 'highPriority', 24 | }; 25 | 26 | return ( 27 |
28 | editItemText(e.target.value, item.id)} 32 | /> 33 | toggleItemCompletion(item.id)} 39 | /> 40 | 43 |
44 | ); 45 | }; 46 | export default TodoItem; 47 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoItemCreator.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-props-no-spreading */ 2 | import React, { useState } from 'react'; 3 | import RadioGroup from '@mui/material/RadioGroup'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormLabel from '@mui/material/FormLabel'; 7 | import Radio from '@mui/material/Radio'; 8 | import useToDoStore from '../store/store'; 9 | 10 | const selector = (state) => state.addTodoListItem; 11 | 12 | // utility for creating unique Id 13 | let id = 1; 14 | const getId = () => { 15 | id += 1; 16 | return id; 17 | }; 18 | 19 | const TodoItemCreator = () => { 20 | const [inputValue, setInputValue] = useState(''); 21 | const [priorityValue, setPriorityValue] = useState('low'); 22 | const addTodoListItem = useToDoStore(selector); 23 | 24 | const addItem = () => { 25 | addTodoListItem({ 26 | id: getId(), 27 | text: inputValue, 28 | priority: priorityValue, 29 | isComplete: false, 30 | }); 31 | setInputValue(''); 32 | setPriorityValue('low'); 33 | }; 34 | 35 | const onChange = ({ target: { value } }) => { 36 | setInputValue(value); 37 | }; 38 | 39 | const handleChange = (event) => { 40 | setPriorityValue(event.target.value); 41 | }; 42 | 43 | /* MUI Radio Button styles */ 44 | const GreenRadio = (props) => ; 45 | 46 | const YellowRadio = (props) => ; 47 | 48 | const RedRadio = (props) => ; 49 | 50 | return ( 51 |
52 | 59 | 60 | 61 | 62 | 69 | } value="high" /> 70 | } value="medium" /> 71 | } value="low" /> 72 | 73 | 74 | 75 | 76 | 79 |
80 | ); 81 | }; 82 | 83 | export default TodoItemCreator; 84 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoItem from './TodoItem'; 3 | import TodoItemCreator from './TodoItemCreator'; 4 | import TodoListFilters from './TodoListFilters'; 5 | import TodoQuickCheck from './TodoQuickCheck'; 6 | import Quotes from './Quotes'; 7 | import SearchBar from './SearchBar'; 8 | import '../styles/styles.css'; 9 | import shallow from 'zustand/shallow'; 10 | import useToDoStore from '../store/store'; 11 | 12 | const selector = (state) => ({ 13 | todoListState: state.todoListState, 14 | todoListFilterState: state.todoListFilterState, 15 | todoListSortState: state.todoListSortState, 16 | }); 17 | 18 | const filterList = (list, filter) => { 19 | switch (filter) { 20 | case 'Show Completed': 21 | return list.filter((item) => item.isComplete); 22 | case 'Show Uncompleted': 23 | return list.filter((item) => !item.isComplete); 24 | default: 25 | return list; 26 | } 27 | }; 28 | 29 | const sortList = (list, sortingMethod) => { 30 | if (!sortingMethod) return list; 31 | const high = list.filter((item) => item.priority === 'high'); 32 | const medium = list.filter((item) => item.priority === 'medium'); 33 | const low = list.filter((item) => item.priority === 'low'); 34 | return [...high, ...medium, ...low]; 35 | }; 36 | 37 | const TodoList = () => { 38 | const { todoListState, todoListFilterState, todoListSortState } = useToDoStore(selector, shallow); 39 | const todoList = sortList(filterList(todoListState, todoListFilterState), todoListSortState); 40 | 41 | return ( 42 |
43 |
44 |
45 | 50 |
51 |
52 | Loading...}> 53 | 54 | 55 |
56 |
57 |

To-Do List

58 |
59 | 60 | 61 | {todoList.map((todoItem) => ( 62 | 63 | ))} 64 | 65 |
66 |
67 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default TodoList; 75 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoListFilters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SortIcon from '@mui/icons-material/Sort'; 3 | import EqualizerIcon from '@mui/icons-material/Equalizer'; 4 | import RefreshIcon from '@mui/icons-material/Refresh'; 5 | import useToDoStore from '../store/store'; 6 | import shallow from 'zustand/shallow'; 7 | 8 | const selector = (state) => ({ 9 | todoListFilterState: state.todoListFilterState, 10 | todoListState: state.todoListState, 11 | resetFiltersAndSorted: state.resetFiltersAndSorted, 12 | todoListSortState: state.todoListSortState, 13 | toggleSort: state.toggleSort, 14 | setFilter: state.setFilter, 15 | }); 16 | 17 | const TodoListFilters = () => { 18 | const { 19 | todoListFilterState, 20 | todoListState, 21 | resetFiltersAndSorted, 22 | todoListSortState, 23 | toggleSort, 24 | setFilter, 25 | } = useToDoStore(selector, shallow); 26 | 27 | // // selector - grabs totals for each category 28 | const { high, medium, low } = todoListState.reduce((acc, cur) => { 29 | acc[cur.priority] = (acc[cur.priority] ?? 0) + 1; 30 | return acc; 31 | }, {}); 32 | 33 | // // toggle priority stats display 34 | const [displayStats, setDisplayStats] = useState(false); 35 | 36 | // // selector - totals for each filter 37 | const totalNum = todoListState.length; 38 | const totalCompletedNum = todoListState.filter((todo) => todo.isComplete).length; 39 | const totalUncompletedNum = todoListState.filter((todo) => !todo.isComplete).length; 40 | 41 | const updateFilter = ({ target: { value } }) => setFilter(value); 42 | 43 | const toggleDisplayStats = () => setDisplayStats(!displayStats); 44 | const reset = () => { 45 | setDisplayStats(false); // displayStats is local state 46 | resetFiltersAndSorted(); 47 | }; 48 | 49 | const sortIconColor = { 50 | true: 'sortedWhite', 51 | false: 'unsortedGray', 52 | }; 53 | 54 | return ( 55 |
    56 | 66 | 76 | 85 | 88 | 89 | 100 | 103 |
104 | ); 105 | }; 106 | 107 | export default TodoListFilters; 108 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/components/TodoQuickCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@mui/material/Checkbox'; 3 | import shallow from 'zustand/shallow'; 4 | import useToDoStore from '../store/store'; 5 | import { useEffect } from 'react'; 6 | 7 | const selector = (state) => ({ 8 | setAllComplete: state.setAllComplete, 9 | checkBox: state.checkBox, 10 | setCheckBox: state.setCheckBox, 11 | }); 12 | 13 | const TodoQuickCheck = () => { 14 | const { setAllComplete, checkBox, setCheckBox } = useToDoStore(selector, shallow); 15 | 16 | useEffect(() => setCheckBox()); 17 | 18 | return ( 19 |
20 | setAllComplete()} 26 | /> 27 | All 28 |
29 | ); 30 | }; 31 | 32 | export default TodoQuickCheck; 33 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Chromogen/6b2085e43c65c20a90916b22255b2f1606411682/demo-zustand-todo/src/favicon.ico -------------------------------------------------------------------------------- /demo-zustand-todo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Chromogen Zustand Demo To-Do 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | import React from 'react'; 3 | import App from './components/App'; 4 | import { createRoot } from 'react-dom/client'; 5 | 6 | const root = createRoot(document.getElementById('app')); 7 | 8 | root.render(); 9 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { chromogenZustandMiddleware } from 'chromogen'; 2 | import { create } from 'zustand'; 3 | 4 | const useToDoStore = create( 5 | chromogenZustandMiddleware((set) => ({ 6 | todoListState: [], 7 | 8 | todoListFilterState: 'Show All', 9 | 10 | todoListSortState: false, 11 | 12 | resetFiltersAndSorted: () => 13 | set( 14 | () => ({ todoListFilterState: 'Show All', todoListSortState: false }), 15 | false, 16 | 'resetFiltersAndSorted', 17 | ), 18 | 19 | toggleSort: () => 20 | set((state) => ({ todoListSortState: !state.todoListSortState }), false, 'toggleSort'), 21 | 22 | setFilter: (filter) => set(() => ({ todoListFilterState: filter }), false, 'setFilter'), 23 | 24 | quoteText: '', 25 | 26 | changeQuoteText: (text) => set(() => ({ quoteText: text }), false, 'changeQuoteText', text), 27 | 28 | quoteNumber: 0, 29 | 30 | changeQuoteNumber: () => 31 | set(() => ({ quoteNumber: Math.floor(Math.random() * 1643) }), false, 'changeQuoteNumber'), 32 | 33 | setAllComplete: () => 34 | set( 35 | (state) => ({ 36 | todoListState: state.todoListState.some((todo) => todo.isComplete === false) 37 | ? state.todoListState.map((todo) => { 38 | return { ...todo, isComplete: true }; 39 | }) 40 | : state.todoListState.map((todo) => { 41 | return { ...todo, isComplete: false }; 42 | }), 43 | }), 44 | false, 45 | 'setAllComplete', 46 | ), 47 | 48 | checkBox: false, 49 | 50 | setCheckBox: () => 51 | set( 52 | (state) => ({ 53 | checkBox: state.todoListState.some((todo) => todo.isComplete === false) ? false : true, 54 | }), 55 | false, 56 | 'setCheckBox', 57 | ), 58 | 59 | addTodoListItem: (todo) => 60 | set( 61 | (state) => ({ todoListState: [...state.todoListState, todo] }), 62 | false, 63 | 'addTodoListItem', 64 | todo, 65 | ), 66 | 67 | deleteTodoListItem: (id) => 68 | set( 69 | (state) => ({ todoListState: state.todoListState.filter((todo) => todo.id !== id) }), 70 | false, 71 | 'deleteTodoListItem', 72 | id, 73 | ), 74 | 75 | editItemText: (text, id) => 76 | set( 77 | (state) => ({ 78 | todoListState: state.todoListState.map((todo) => { 79 | if (todo.id === id) { 80 | return { ...todo, text: text }; 81 | } else { 82 | return todo; 83 | } 84 | }), 85 | }), 86 | false, 87 | 'editItemText', 88 | text, 89 | id, 90 | ), 91 | 92 | toggleItemCompletion: (id) => 93 | set( 94 | (state) => ({ 95 | todoListState: state.todoListState.map((todo) => { 96 | if (todo.id === id) { 97 | return { ...todo, isComplete: !todo.isComplete }; 98 | } else { 99 | return todo; 100 | } 101 | }), 102 | }), 103 | false, 104 | 'toggleItemCompletion', 105 | id, 106 | ), 107 | 108 | searchResultState: { 109 | all: { 110 | searchTerm: '', 111 | results: [], 112 | }, 113 | high: { 114 | searchTerm: '', 115 | results: [], 116 | }, 117 | medium: { 118 | searchTerm: '', 119 | results: [], 120 | }, 121 | low: { 122 | searchTerm: '', 123 | results: [], 124 | }, 125 | }, 126 | 127 | setSearchState: (searchTerm, priority) => 128 | set( 129 | (state) => { 130 | if (searchTerm === '') 131 | return { 132 | searchResultState: { 133 | ...state.searchResultState, 134 | [priority]: { searchTerm, results: [] }, 135 | }, 136 | }; 137 | let results = [...state.todoListState].filter((todo) => todo.text.includes(searchTerm)); 138 | if (priority !== 'all') results = results.filter((todo) => todo.priority === priority); 139 | return { 140 | searchResultState: { ...state.searchResultState, [priority]: { searchTerm, results } }, 141 | }; 142 | }, 143 | false, 144 | 'setSearchState', 145 | searchTerm, 146 | priority, 147 | ), 148 | })), 149 | ); 150 | 151 | export default useToDoStore; 152 | -------------------------------------------------------------------------------- /demo-zustand-todo/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | * { 7 | margin: 0; 8 | } 9 | html, 10 | body, 11 | #root, /* for create-react-app */ 12 | #__next /* for Next.js */ { 13 | height: 100%; 14 | } 15 | body { 16 | font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | -webkit-font-smoothing: antialiased; 18 | } 19 | img, 20 | picture, 21 | video, 22 | canvas, 23 | svg { 24 | display: block; 25 | max-width: 100%; 26 | } 27 | input, 28 | button, 29 | textarea, 30 | select { 31 | font: inherit; 32 | } 33 | p, 34 | h1, 35 | h2, 36 | h3, 37 | h4, 38 | h5, 39 | h6 { 40 | overflow-wrap: break-word; 41 | } 42 | #root, 43 | #__next { 44 | isolation: isolate; 45 | } 46 | 47 | /* -------------General Styles---------------- */ 48 | html { 49 | margin: 0; 50 | background-color: #2e3237; 51 | color: #9ee6f7; 52 | overflow-x: hidden; 53 | } 54 | 55 | h1 { 56 | text-align: center; 57 | font-size: 2.3rem; 58 | color: #9ee6f7; 59 | text-shadow: 1px 2px 2px rgba(250, 250, 250, 0.267); 60 | letter-spacing: 3px; 61 | } 62 | input, 63 | button, 64 | select { 65 | background-color: transparent; 66 | /* border: 1px solid lightgray; */ 67 | border: none; 68 | padding: 20px 0; 69 | border-radius: 4px; 70 | color: #9ee6f7; 71 | font-size: 18px; 72 | letter-spacing: 1px; 73 | } 74 | 75 | /* remove browser defaults */ 76 | button:focus { 77 | outline: none; 78 | } 79 | input:focus { 80 | outline: none; 81 | } 82 | /* ------------TodoList------------- */ 83 | 84 | /* topmost container */ 85 | .mainContainer { 86 | display: flex; 87 | flex-direction: column; 88 | height: 100vh; 89 | width: 100vw; 90 | overflow-y: scroll; 91 | } 92 | 93 | .wrapper { 94 | display: flex; 95 | margin: 0 auto; 96 | padding-inline: 32px; 97 | width: 100%; 98 | max-width: 800px; 99 | height: 100%; 100 | flex-direction: column; 101 | padding-bottom: 24px; 102 | } 103 | 104 | /* overall row container for todo list display */ 105 | .todosDisplayRow { 106 | margin: 0 auto; 107 | width: 100%; 108 | height: 100%; 109 | color: #9ee6f7; 110 | } 111 | 112 | a { 113 | color: #9ee6f7; 114 | text-decoration: none; 115 | } 116 | 117 | a:hover { 118 | text-decoration: underline; 119 | } 120 | 121 | .todosDisplayRow h1 { 122 | padding-block: 40px; 123 | } 124 | 125 | /* container for entire list display */ 126 | .todosContainer { 127 | background-color: rgb(255, 255, 255, 0.1); 128 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 129 | border-radius: 5px; 130 | padding: 5px 12px 0 12px; 131 | } 132 | 133 | /* -------------TodoItemCreator---------------- */ 134 | 135 | .itemCreator { 136 | display: flex; 137 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 138 | } 139 | 140 | .itemCreator button { 141 | padding: 0 24px; 142 | } 143 | 144 | input::placeholder { 145 | letter-spacing: 1.5px; 146 | } 147 | 148 | .itemCreator input { 149 | padding: 0 0 0 12px; 150 | flex-grow: 1; 151 | height: 60px; 152 | } 153 | 154 | #radioContainer svg { 155 | margin-top: 11px; 156 | opacity: 0.7; 157 | } 158 | 159 | label.MuiFormControlLabel-root { 160 | margin-left: 0; 161 | margin-right: 0; 162 | } 163 | 164 | /* -------------TodoItem---------------- */ 165 | 166 | .itemContainer, 167 | .lowPriority, 168 | .mediumPriority, 169 | .highPriority { 170 | display: grid; 171 | grid-template-columns: 79fr 14fr 7fr; 172 | border-bottom: 1px solid rgba(245, 245, 245, 0.336); 173 | } 174 | 175 | /* dynamic checkbox color */ 176 | .highPriority svg { 177 | color: #f10101; 178 | opacity: 0.7; 179 | margin-right: 28px; 180 | } 181 | .mediumPriority svg { 182 | color: #ffe600; 183 | opacity: 0.7; 184 | margin-right: 28px; 185 | } 186 | .lowPriority svg { 187 | color: #05fb11; 188 | opacity: 0.7; 189 | margin-right: 28px; 190 | } 191 | 192 | #todoItem button { 193 | margin-left: 7px; 194 | } 195 | 196 | /* -------------TodoListFilter---------------- */ 197 | 198 | ul { 199 | display: grid; 200 | grid-template-columns: 27fr 27fr 27fr 6fr 6fr 6fr; 201 | margin: 0; 202 | padding: 0px; 203 | width: 100%; 204 | } 205 | 206 | .filter-button { 207 | margin-top: 10px; 208 | margin-bottom: 10px; 209 | border-right: 1px solid rgba(234, 230, 230, 0.26); 210 | padding: 10px 20px; 211 | border-radius: 0px 0px 4px 4px; 212 | } 213 | 214 | /* dynamic sort icon color */ 215 | #sortedWhite svg { 216 | color: whitesmoke; 217 | } 218 | #unsortedGray svg { 219 | color: rgba(245, 245, 245, 0.336); 220 | } 221 | #unsortedGray { 222 | width: 64px; 223 | } 224 | 225 | #statsSpan { 226 | display: grid; 227 | grid-template-columns: 30fr 30fr 30fr; 228 | align-items: center; 229 | } 230 | #highSpan { 231 | color: #ef5350; 232 | opacity: 0.7; 233 | margin-right: 4px; 234 | } 235 | #mediumSpan { 236 | color: #ffee58; 237 | opacity: 0.7; 238 | margin-right: 4px; 239 | } 240 | #lowSpan { 241 | color: #66bb6a; 242 | opacity: 0.7; 243 | } 244 | 245 | /* filter stats (number) */ 246 | button span { 247 | opacity: 0.6; 248 | } 249 | 250 | /* ----------QuoteBox---------- */ 251 | 252 | #quoteContainer { 253 | display: flex; 254 | flex-direction: column; 255 | width: 100%; 256 | /* justify-content: space-between; */ 257 | font-size: 18px; 258 | } 259 | 260 | #quoteContainer p { 261 | white-space: pre-wrap; 262 | padding-bottom: 24px; 263 | } 264 | 265 | .quoteBox { 266 | display: flex; 267 | flex-direction: row; 268 | justify-content: space-between; 269 | border: 1px solid rgba(234, 230, 230, 0.1); 270 | /* background-color: rgb(63, 63, 63); */ 271 | /* box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); */ 272 | border-radius: 5px; 273 | padding: 16px 24px; 274 | } 275 | 276 | .quoteBox img { 277 | margin: 20px; 278 | height: 150px; 279 | width: 150px; 280 | } 281 | 282 | .quoteBox button { 283 | width: 150px; 284 | padding: 24 0; 285 | text-align: left; 286 | } 287 | 288 | .quoteBox button:hover { 289 | cursor: pointer; 290 | /* background-color: rgba(255, 255, 255, 0.1); */ 291 | } 292 | 293 | /* .quoteBox button:active { 294 | background-color: #75acb990; 295 | } */ 296 | 297 | /* ---------- TodoQuickCheck ---------- */ 298 | 299 | /* #quickCheck { 300 | } */ 301 | 302 | #quickCheck svg { 303 | color: #9ee6f7; 304 | } 305 | 306 | button { 307 | cursor: pointer; 308 | } 309 | 310 | /* ---------- Search ---------- */ 311 | .searchContainer { 312 | display: sticky; 313 | margin-top: 100px; 314 | background-color: rgb(255, 255, 255, 0.1); 315 | box-shadow: 0px 0px 35px 20px rgba(10, 10, 10, 0.096); 316 | border-radius: 5px; 317 | padding: 5px 12px 12px 12px; 318 | display: grid; 319 | grid-template-columns: 70% 30%; 320 | } 321 | 322 | .searchField { 323 | border-bottom: solid 1px whitesmoke; 324 | border-radius: 0; 325 | padding-left: 12px; 326 | } 327 | 328 | .prioritySelect { 329 | grid-column-start: 2; 330 | border-bottom: solid 1px whitesmoke; 331 | border-radius: 0; 332 | -webkit-appearance-select: none; 333 | } 334 | 335 | .searchResults { 336 | grid-column-start: span 3; 337 | } 338 | 339 | #newChromogenLogo { 340 | display: flex; 341 | justify-content: center; 342 | width: 400px; 343 | height: 300px; 344 | } 345 | 346 | select { 347 | -webkit-appearance-select: none; 348 | /* background: url("data:image/svg+xml;utf8,") 349 | no-repeat; */ 350 | } 351 | 352 | /* */ 353 | 354 | .w-tc-editor[data-color-mode*='dark'], 355 | [data-color-mode*='dark'] .w-tc-editor, 356 | [data-color-mode*='dark'] .w-tc-editor-var, 357 | body[data-color-mode*='dark'] { 358 | --color-fg-default: #ddd; 359 | --color-canvas-subtle: #161b22; 360 | --color-prettylights-syntax-comment: #818c97; 361 | --color-prettylights-syntax-entity-tag: #ed81b0; 362 | --color-prettylights-syntax-entity: #d2a8ff; 363 | --color-prettylights-syntax-sublimelinter-gutter-mark: #ddd; 364 | --color-prettylights-syntax-constant: #ed8876; 365 | --color-prettylights-syntax-string: #68afc8; 366 | --color-prettylights-syntax-keyword: #ed81b0; 367 | --color-prettylights-syntax-markup-bold: #c9d1d9; 368 | } 369 | -------------------------------------------------------------------------------- /demo-zustand-todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, './src/index.js'), 5 | output: { 6 | filename: 'bundle.js', 7 | }, 8 | devServer: { 9 | contentBase: path.resolve(__dirname, './src'), 10 | historyApiFallback: true, 11 | }, 12 | mode: process.env.NODE_ENV, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | plugins: ['@babel/transform-runtime'], 22 | }, 23 | }, 24 | { 25 | test: /\.css$/, 26 | use: [ 27 | { 28 | loader: 'style-loader', 29 | }, 30 | { 31 | loader: 'css-loader', 32 | }, 33 | ], 34 | }, 35 | { 36 | test: /\.(png|jpg)$/, 37 | use: ['file-loader', 'url-loader?limit=8192'], 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | extensions: ['.js', '.jsx'], 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /jenkins/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | image 'node:lts-buster-slim' 5 | args '-p 3003:3003' 6 | } 7 | } 8 | environment { 9 | CI = 'true' 10 | } 11 | stages { 12 | stage('Build') { 13 | steps { 14 | sh 'npm --prefix ./package install' 15 | } 16 | } 17 | stage('Test') { 18 | steps { 19 | sh './jenkins/scripts/test.sh' 20 | } 21 | } 22 | stage('Deliver') { 23 | steps { 24 | sh './jenkins/scripts/deliver.sh' 25 | input message: 'Finished using the web site? (Click "Proceed" to continue)' 26 | sh './jenkins/scripts/kill.sh' 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jenkins/scripts/deliver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following "npm" command builds your Node.js/React application for' 4 | echo 'production in the local "build" directory (i.e. within the' 5 | echo '"/var/jenkins_home/workspace/simple-node-js-react-app" directory),' 6 | echo 'correctly bundles React in production mode and optimizes the build for' 7 | echo 'the best performance.' 8 | set -x 9 | npm run build 10 | set +x 11 | 12 | echo 'The following "npm" command runs your Node.js/React application in' 13 | echo 'development mode and makes the application available for web browsing.' 14 | echo 'The "npm start" command has a trailing ampersand so that the command runs' 15 | echo 'as a background process (i.e. asynchronously). Otherwise, this command' 16 | echo 'can pause running builds of CI/CD applications indefinitely. "npm start"' 17 | echo 'is followed by another command that retrieves the process ID (PID) value' 18 | echo 'of the previously run process (i.e. "npm start") and writes this value to' 19 | echo 'the file ".pidfile".' 20 | set -x 21 | npm --prefix ./package run symlink & 22 | sleep 1 23 | echo $! > .pidfile 24 | set +x 25 | 26 | echo 'Now...' 27 | echo 'Visit http://localhost:3003 to see your Node.js/React application in action.' 28 | echo '(This is why you specified the "args ''-p 3003:3003''" parameter when you' 29 | echo 'created your initial Pipeline as a Jenkinsfile..)' 30 | -------------------------------------------------------------------------------- /jenkins/scripts/kill.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following command terminates the "npm start" process using its PID' 4 | echo '(written to ".pidfile"), all of which were conducted when "deliver.sh"' 5 | echo 'was executed.' 6 | set -x 7 | kill $(cat .pidfile) 8 | -------------------------------------------------------------------------------- /jenkins/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | echo 'The following "npm" command (if executed) installs the "cross-env"' 4 | echo 'dependency into the local "node_modules" directory, which will ultimately' 5 | echo 'be stored in the Jenkins home directory. As described in' 6 | echo 'https://docs.npmjs.com/cli/install, the "--save-dev" flag causes the' 7 | echo '"cross-env" dependency to be installed as "devDependencies". For the' 8 | echo 'purposes of this tutorial, this flag is not important. However, when' 9 | echo 'installing this dependency, it would typically be done so using this' 10 | echo 'flag. For a comprehensive explanation about "devDependencies", see' 11 | echo 'https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencies.' 12 | set -x 13 | npm install --save-dev cross-env 14 | set +x 15 | 16 | echo 'The following "npm" command tests that your simple Node.js/React' 17 | echo 'application renders satisfactorily. This command actually invokes the test' 18 | echo 'runner Jest (https://facebook.github.io/jest/).' 19 | set -x 20 | npm --prefix ./package run test 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen-root", 3 | "version": "5.0.1", 4 | "description": "simple, interaction-driven test generator for Recoil and Zustand apps", 5 | "scripts": { 6 | "ci-all": "(npm ci); (cd ./package && npm ci); (cd ./dev-tool && npm ci); (cd ./demo-todo && npm ci);", 7 | "build": "echo \"ERROR! Nothing to build in root directory.\n\"" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/oslabs-beta/Chromogen.git" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Brach Burdick", 16 | "url": "https://github.com/sirbrachthepale/" 17 | }, 18 | { 19 | "name": "Francois Denavaut", 20 | "url": "https://github.com/dnvt/" 21 | }, 22 | { 23 | "name": "Maggie Kwan", 24 | "url": "https://github.com/maggiekwan/" 25 | }, 26 | { 27 | "name": "Lawrence Liang", 28 | "url": "https://github.com/Lawliang/" 29 | }, 30 | { 31 | "name": "Michelle Holland", 32 | "url": "https://github.com/michellebholland/" 33 | }, 34 | { 35 | "name": "Jim Chen", 36 | "url": "https://github.com/chenchingk" 37 | }, 38 | { 39 | "name": "Andy Wang", 40 | "url": "https://github.com/andywang23" 41 | }, 42 | { 43 | "name": "Connor Rose Delisle", 44 | "url": "https://github.com/connorrose" 45 | }, 46 | { 47 | "name": "Amy Yee", 48 | "url": "https://github.com/amyy98" 49 | }, 50 | { 51 | "name": "Cameron Greer", 52 | "url": "https://github.com/cgreer011" 53 | }, 54 | { 55 | "name": "Jinseon Shin", 56 | "url": "https://github.com/wlstjs" 57 | }, 58 | { 59 | "name": "Nicholas Shay", 60 | "url": "https://github.com/nicholasjs" 61 | }, 62 | { 63 | "name": "Ryan Tumel", 64 | "url": "https://github.com/rtumel123" 65 | }, 66 | { 67 | "name": "Marcellies Pettiford", 68 | "url": "https://github.com/mp-04" 69 | }, 70 | { 71 | "name": "Sung Kim", 72 | "url": "https://github.com/smk53664" 73 | }, 74 | { 75 | "name": "Lina Lee", 76 | "url": "https://github.com/lina4lee" 77 | }, 78 | { 79 | "name": "Erica Oh", 80 | "url": "https://github.com/ericaysoh" 81 | }, 82 | { 83 | "name": "Dani Almaraz", 84 | "url": "https://github.com/dtalmaraz" 85 | }, 86 | { 87 | "name": "Craig Boswell", 88 | "url": "https://github.com/crgb0s" 89 | }, 90 | { 91 | "name": "Hussein Ahmed", 92 | "url": "https://github.com/Hali3030" 93 | }, 94 | { 95 | "name": "Ian Kila", 96 | "url": "https://github.com/iannkila" 97 | }, 98 | { 99 | "name": "Yuehao Wong", 100 | "url": "https://github.com/yuehaowong" 101 | } 102 | ], 103 | "license": "MIT", 104 | "bugs": { 105 | "url": "https://github.com/oslabs-beta/Chromogen/issues" 106 | }, 107 | "homepage": "https://github.com/oslabs-beta/Chromogen#readme", 108 | "dependencies": { 109 | "@uiw/react-textarea-code-editor": "^2.1.1", 110 | "dependency-cruiser": "^12.9.0", 111 | "react": "^18.0.0", 112 | "react-dom": "^18.0.0", 113 | "recoil": "^0.7.2", 114 | "redux": "^4.0.5", 115 | "styled-components": "^5.3.6", 116 | "zustand": "^4.1.1" 117 | }, 118 | "devDependencies": { 119 | "@babel/core": "^7.11.6", 120 | "@babel/preset-env": "^7.11.5", 121 | "@babel/preset-react": "^7.10.4", 122 | "@babel/preset-typescript": "^7.10.4", 123 | "@testing-library/react": "^13.1.1", 124 | "@types/node": "^14.11.2", 125 | "@types/react": "^18.0.6", 126 | "@types/react-dom": "^18.0.2", 127 | "@types/styled-components": "^5.1.26", 128 | "babel-jest": "^26.3.0", 129 | "coveralls": "^3.1.0", 130 | "css-loader": "^6.7.3", 131 | "eslint-config-airbnb-typescript": "^17.0.0", 132 | "jest": "^26.4.2", 133 | "react-test-renderer": "^18.0.0", 134 | "typescript": "^4.0.3" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /package/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 |
2 |

Chromogen

3 | 4 | chromogen logo 10 | 11 | 12 |

A UI-driven Jest test-generation package for Recoil.js selectors and Zustand store hooks.

13 | 14 |
15 | 16 | [![npm version](https://img.shields.io/npm/v/chromogen)](https://www.npmjs.com/package/chromogen) 17 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/open-source-labs/Chromogen/blob/master/LICENSE) 18 |
19 | 20 |
21 |
22 | 23 | **Now Compatible with React V18** 24 | 25 | Chromogen (Now on Version 4.0) is a Jest unit-test generation tool for Zustand Stores and Recoil selectors. It captures state changes during user interaction and auto-generates corresponding test suites. Simply launch your application after following the installation instructions below, interact as a user normally would, and with one click you can download a ready-to-run Jest test file. Alternatively, you can copy the generated tests straight to your clipboard. 26 |


27 | 28 | ## Installation for Zustand Apps 29 | 30 | Before using Chromogen, you'll need to make two changes to your application: 31 | 32 | 1. Import the `` component and render it alongside any other components in `` 33 | 2. Import `chromogenZustandMiddleware` function from Chromogen. This will be used as middleware when setting up your store. 34 | 35 | ### Import the ChromogenZustandObserver component 36 | 37 | Import `ChromogenZustandObserver`. ChromogenZustandObserver can be rendered alongside any other components in ``. 38 | 39 | ```jsx 40 | import React from 'react'; 41 | import { ChromogenZustandObserver } from 'chromogen'; 42 | import TodoList from './TodoList'; 43 | 44 | const App = () => ( 45 | <> 46 | 47 | 48 | 49 | ); 50 | 51 | export default App; 52 | ``` 53 | 54 | Import `chromogenZustandMiddleware`. When you call create, wrap your store function with chromogenZustandMiddleware. **Note**, when using chromogenZustandMiddleware, you'll need to provide some additional arguments into the set function. 55 | 56 | 1. _Overwrite State_ (boolean) - Without middleware, this defaults to `false`, but you'll need to explicitly provide a value when using Chromogen. 57 | 2. _Action Name_ - Used for test generation 58 | 3. _Action Parameters_ - If the action requires input parameters, pass these in after the Action Name. 59 | 60 | ```jsx 61 | import { chromogenZustandMiddleware } from 'chromogen'; 62 | import create from 'zustand'; 63 | 64 | const useStore = create( 65 | chromogenZustandMiddleware((set) => ({ 66 | counter: 0, 67 | color: 'black', 68 | prioritizeTask: ['walking', 5], 69 | addCounter: () => set(() => ({ counter: (counter += 1) }), false, 'addCounter'), 70 | changeColor: (newColor) => set(() => ({ color: newColor }), false, 'changeColor', newColor), 71 | setTaskPriority: (task, priority) => 72 | set(() => ({ prioritizeTask: [task, priority] }), false, 'setTaskPriority', task, priority), 73 | })), 74 | ); 75 | 76 | export default useStore; 77 | ``` 78 | 79 |

80 | 81 | ## Installation for Recoil Apps 82 | 83 | Before running Chromogen, you'll need to make two changes to your application: 84 | 85 | 1. Import the `` component as a child of `` 86 | 1. Import the `atom` and `selector` functions from Chromogen instead of Recoil 87 | 88 | Note: These changes do have a small performance cost, so they should be reverted before deploying to production. 89 | 90 |
91 | 92 | ### Import the ChromogenObserver component 93 | 94 | ChromogenObserver should be included as a direct child of RecoilRoot. It does not need to wrap any other components, and it takes no mandatory props. It utilizes Recoil's TransactionObserver Hook to record snapshots on state change. 95 | 96 | ```jsx 97 | import React from 'react'; 98 | import { RecoilRoot } from 'recoil'; 99 | import { ChromogenObserver } from 'chromogen'; 100 | import MyComponent from './components/MyComponent.jsx'; 101 | 102 | const App = (props) => ( 103 | 104 | 105 | 106 | 107 | ); 108 | 109 | export default App; 110 | ``` 111 | 112 | If you are using pseudo-random key names, such as with _UUID_, you'll need to pass all of your store exports to the ChromogenObserver component as a `store` prop. This will allow Chromogen to use source code variable names in the output file, instead of relying on keys. When all atoms and selectors are exported from a single file, you can pass the imported module directly: 113 | 114 | ```jsx 115 | import * as store from './store'; 116 | // ... 117 | ; 118 | ``` 119 | 120 | If your store utilizes seprate files for various pieces of state, you can pass all of the imports in an array: 121 | 122 | ```jsx 123 | import * as atoms from './store/atoms'; 124 | import * as selectors from './store/selectors'; 125 | import * as misc from './store/arbitraryRecoilState'; 126 | // ... 127 | ; 128 | ``` 129 | 130 |
131 | 132 | ### Import atom & selector functions from Chromogen 133 | 134 | Wherever you import `atom` and/or `selector` functions from Recoil (typically in your `store` file), import them from Chromogen instead. The arguments passed in do **not** need to change in any away, and the return value will still be a normal RecoilAtom or RecoilSelector. Chromogen wraps the native Recoil functions to track which pieces of state have been created, as well as when various selectors are called and what values they return. 135 | 136 | ```js 137 | import { atom, selector } from 'chromogen'; 138 | 139 | export const fooState = atom({ 140 | key: 'fooState', 141 | default: {}, 142 | }); 143 | 144 | export const barState = selector({ 145 | key: 'barState', 146 | get: ({ get }) => { 147 | const derivedState = get(fooState); 148 | return derivedState.baz || 'value does not exist'; 149 | }, 150 | }); 151 | ``` 152 | 153 |

154 | 155 | ## Usage for All Apps 156 | 157 | After following the installation steps above, launch your application as normal. You should see two buttons in the bottom left corner. 158 | 159 |
160 | 161 | ![Buttons](https://github.com/open-source-labs/Chromogen/raw/master/assets/README-root/ultratrimmedDemo.gif) 162 | 163 |
164 | 165 | The pause button on the left is the **pause recording** button. Clicking it will pause recording, so that no tests are generated during subsequent state changes. Pausing is useful for setting up a complex initial state with repetitive actions, where you don't want to test every step of the process. 166 | 167 | The button in the middle is the **download** button. Clicking it will download a new test file that includes _all_ tests generated since the app was last launched or refreshed. 168 | 169 | The button on the right is the **copy-to-clipboard** button. Clicking it will copy your tests, including _all_ tests generated since the app was last launched or refreshed. 170 | 171 | Once you've recorded all the interactions you want to test, click the pause button and then the download button to generate the test file or press copy to copy to your clipboard. You can now drag-and-drop the downloaded file into your app's test directory or paste the code in your new file. **Don't forget to add the source path in your test file** 172 | 173 | You're now ready to run your tests! After running your normal Jest test command, you should see a test suite for `chromogen.test.js`. 174 | 175 | The current tests check whether state has changed after an interaction and checks whether the resulting state change variables have been updated as expected. 176 | 177 |

178 | 179 | Please visit our [main repo](https://github.com/open-source-labs/Chromogen) for more detailed instructions, as well as any bug reports, support issues, or feature requests. 180 | -------------------------------------------------------------------------------- /package/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /package/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { atom, selector, atomFamily, selectorFamily } from './recoil_generator/src/api/api'; 3 | import { ChromogenZustandObserver } from './zustand_generator/src/component/ChromogenZustandObserver'; 4 | import { ChromogenObserver } from './recoil_generator/src/component/ChromogenObserver'; 5 | import { chromogenZustandMiddleware } from './zustand_generator/src/api/api'; 6 | import Editor from './zustand_generator/src/component/Editor'; 7 | 8 | // CHROMGOEN FAMILY APIs ARE CURRENTLY UNSTABLE 9 | export { 10 | atom, 11 | selector, 12 | atomFamily, 13 | selectorFamily, 14 | ChromogenObserver, 15 | chromogenZustandMiddleware, 16 | ChromogenZustandObserver, 17 | Editor, 18 | }; 19 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromogen", 3 | "version": "5.0.1", 4 | "description": "simple, interaction-driven Jest test generator for Recoil and React Hooks apps", 5 | "main": "build/index.js", 6 | "keywords": [ 7 | "react", 8 | "recoil", 9 | "jest", 10 | "testing" 11 | ], 12 | "files": [ 13 | "build" 14 | ], 15 | "scripts": { 16 | "prepublishOnly": "npm run build", 17 | "build": "tsc", 18 | "test": "jest --verbose --coverage", 19 | "localUpdate": "tsc && npm --prefix ../demo-zustand-todo run update", 20 | "tarballUpdate": "npm --prefix ../demo-zustand-todo run symlink", 21 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/open-source-labs/Chromogen.git" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "Brach Burdick", 30 | "url": "https://github.com/sirbrachthepale/" 31 | }, 32 | { 33 | "name": "Francois Denavaut", 34 | "url": "https://github.com/dnvt/" 35 | }, 36 | { 37 | "name": "Maggie Kwan", 38 | "url": "https://github.com/maggiekwan/" 39 | }, 40 | { 41 | "name": "Lawrence Liang", 42 | "url": "https://github.com/Lawliang/" 43 | }, 44 | { 45 | "name": "Michelle Holland", 46 | "url": "https://github.com/michellebholland/" 47 | }, 48 | { 49 | "name": "Jim Chen", 50 | "url": "https://github.com/chenchingk" 51 | }, 52 | { 53 | "name": "Andy Wang", 54 | "url": "https://github.com/andywang23" 55 | }, 56 | { 57 | "name": "Connor Rose Delisle", 58 | "url": "https://github.com/connorrose" 59 | }, 60 | { 61 | "name": "Amy Yee", 62 | "url": "https://github.com/amyy98" 63 | }, 64 | { 65 | "name": "Cameron Greer", 66 | "url": "https://github.com/cgreer011" 67 | }, 68 | { 69 | "name": "Jinseon Shin", 70 | "url": "https://github.com/wlstjs" 71 | }, 72 | { 73 | "name": "Nicholas Shay", 74 | "url": "https://github.com/nicholasjs" 75 | }, 76 | { 77 | "name": "Ryan Tumel", 78 | "url": "https://github.com/rtumel123" 79 | }, 80 | { 81 | "name": "Marcellies Pettiford", 82 | "url": "https://github.com/mp-04" 83 | }, 84 | { 85 | "name": "Sung Kim", 86 | "url": "https://github.com/smk53664" 87 | }, 88 | { 89 | "name": "Lina Lee", 90 | "url": "https://github.com/lina4lee" 91 | }, 92 | { 93 | "name": "Erica Oh", 94 | "url": "https://github.com/ericaysoh" 95 | }, 96 | { 97 | "name": "Dani Almaraz", 98 | "url": "https://github.com/dtalmaraz" 99 | }, 100 | { 101 | "name": "Craig Boswell", 102 | "url": "https://github.com/crgb0s" 103 | }, 104 | { 105 | "name": "Hussein Ahmed", 106 | "url": "https://github.com/Hali3030" 107 | }, 108 | { 109 | "name": "Ian Kila", 110 | "url": "https://github.com/iannkila" 111 | }, 112 | { 113 | "name": "Yuehao Wong", 114 | "url": "https://github.com/yuehaowong" 115 | } 116 | ], 117 | "license": "MIT", 118 | "bugs": { 119 | "url": "https://github.com/open-source-labs/Chromogen/issues" 120 | }, 121 | "homepage": "https://github.com/open-source-labs/Chromogen#readme", 122 | "peerDependencies": { 123 | "jest": ">=24.0.0", 124 | "typescript": ">=3.8.0" 125 | }, 126 | "dependencies": { 127 | "@uiw/react-textarea-code-editor": "^2.1.1", 128 | "dependency-cruiser": "^12.9.0", 129 | "react": "^18.0.0", 130 | "react-dom": "^18.0.0", 131 | "recoil": "^0.7.2", 132 | "redux": "^4.0.5", 133 | "styled-components": "^5.3.6", 134 | "zustand": "^4.1.1" 135 | }, 136 | "devDependencies": { 137 | "@babel/core": "^7.11.6", 138 | "@babel/preset-env": "^7.11.5", 139 | "@babel/preset-react": "^7.10.4", 140 | "@babel/preset-typescript": "^7.10.4", 141 | "@testing-library/react": "^13.1.1", 142 | "@types/node": "^14.11.2", 143 | "@types/react": "^18.0.6", 144 | "@types/react-dom": "^18.0.2", 145 | "@types/styled-components": "^5.1.26", 146 | "babel-jest": "^26.3.0", 147 | "coveralls": "^3.1.0", 148 | "css-loader": "^6.7.3", 149 | "eslint-config-airbnb-typescript": "^17.0.0", 150 | "jest": "^26.6.3", 151 | "react-test-renderer": "^18.0.0", 152 | "typescript": "^4.0.3" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/api.test.js: -------------------------------------------------------------------------------- 1 | import { ledger } from '../src/utils/ledger.ts'; 2 | import { atom, selector, selectorFamily, atomFamily } from '../src/api/api.ts'; 3 | 4 | // testing the atom 5 | describe('atom', () => { 6 | // destructuring atoms from ledger interface in utils folder 7 | const { atoms } = ledger; 8 | it('is a function', () => { 9 | expect(typeof atom).toBe('function'); 10 | }); 11 | 12 | it('should update ledger upon invocation', () => { 13 | // creating a mock atom 14 | atom({ 15 | key: 'exampleAtom', 16 | default: false, 17 | }); 18 | // verifying atoms property (array) on ledger has been updated with input atom 19 | expect(atoms).toHaveLength(1); 20 | }); 21 | 22 | it('should create Recoil atom with correct key name', () => { 23 | // verifying that input atom key matches 'exampleAtom' 24 | expect(atoms[0]).toHaveProperty('key', 'exampleAtom'); 25 | }); 26 | }); 27 | 28 | describe('selector', () => { 29 | // destructuring selectors from ledger object in utils folder 30 | const { selectors } = ledger; 31 | const test = true; 32 | 33 | it('is a function', () => { 34 | // verify selector is a function 35 | expect(typeof selector).toBe('function'); 36 | }); 37 | 38 | it('should update ledger upon invocation', () => { 39 | // creating a mock selector with key, get, set 40 | selector({ 41 | key: 'exampleSelector', 42 | get: () => 'getMethod', 43 | set: () => 'setMethod', 44 | }); 45 | // verify selectors property in ledger has been updated with mock selector 46 | expect(selectors).toHaveLength(1); 47 | }); 48 | // verifying that input selector key matches 'exampleSelector' 49 | it('should capture correct key name', () => { 50 | expect(selectors[0]).toEqual('exampleSelector'); 51 | }); 52 | 53 | xit('should return an object if an input condition evaluates to true', () => { 54 | // verify that selector (recoilSelector in this context) invocation returns an object 55 | expect(typeof selector(test)).toBe('object'); 56 | }); 57 | }); 58 | 59 | describe('atomFamily', () => { 60 | it('should return a function', () => { 61 | // create a mock atomFamily 62 | const familyFactory = atomFamily({ 63 | key: 'familyKey', 64 | default: (param) => param.toString(), 65 | }); 66 | // verify that familyFactory is a function 67 | expect(typeof familyFactory).toEqual('function'); 68 | }); 69 | }); 70 | 71 | describe('selectorFamily', () => { 72 | // truthy parameter 73 | const test = true; 74 | it('should return a function', () => { 75 | // create a mock selectorFamily 76 | const familyFactory = selectorFamily({ 77 | key: 'familyKey', 78 | get: () => () => 'some value', 79 | set: () => () => undefined, 80 | default: (param) => param.toString(), 81 | }); 82 | // verify that familyFactory is a function 83 | expect(typeof familyFactory).toEqual('function'); 84 | }); 85 | it('should return an object if an input condition evaluates to true', () => { 86 | // verify that selectorFamily (recoilSelectorFamily in this context) invocation returns an object 87 | expect(typeof selectorFamily(test)).toBe('function'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/component-utils.test.js: -------------------------------------------------------------------------------- 1 | import { ledger } from '../src/utils/ledger.ts'; 2 | import {generateFile} from '../src/component/component-utils'; 3 | 4 | // Testing generateFile 5 | xdescribe('generateFile', () => { 6 | const setFile = 0; 7 | const array = [[], [], []]; 8 | let storeMap = new Map(array); 9 | 10 | const { 11 | atoms, 12 | selectors, 13 | setters, 14 | atomFamilies, 15 | selectorFamilies, 16 | initialRender, 17 | initialRenderFamilies, 18 | transactions, 19 | setTransactions, 20 | } = ledger; 21 | 22 | // We expect our generate file to not be the falsy return statement, which is the entirety of the ledger, with atoms being the new user input 23 | generateFile(setFile, storeMap); 24 | }); 25 | -------------------------------------------------------------------------------- /package/recoil_generator/__tests__/component.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot, useRecoilState } from 'recoil'; 3 | import { render } from '@testing-library/react'; 4 | import { ChromogenObserver } from '../src/component/ChromogenObserver.tsx'; 5 | import { ledger } from '../src/utils/ledger.ts'; 6 | import { atom } from '../src/api/api.ts'; 7 | 8 | // import {shallow} from 'enzyme'; 9 | // import {mount} from 'enzyme'; 10 | 11 | describe('chromogenObserver', () => { 12 | global.URL = { 13 | createObjectURL: () => 'http://mockURL.com', 14 | }; 15 | 16 | beforeEach(() => { 17 | console.error = jest.fn(); 18 | // creating a mockAtom 19 | const mockAtom = atom({ key: 'mockAtom', default: true }); 20 | // create a functional mockComponent 21 | const MockComponent = () => { 22 | // declaring a React Hook using mockAtom as recoilState 23 | const [mock, setMock] = useRecoilState(mockAtom); 24 | // render a mock-button that toggles mock recoilState onclick 25 | return 132 | 133 | ); 134 | }; 135 | 136 | export default Record; 137 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Buttons/RecordingVariations/Start.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const Start = (props): JSX.Element => { 4 | //hover 5 | const [isHover, setIsHover] = useState(false); 6 | const handleMouseEnter = () => { 7 | setIsHover(true); 8 | }; 9 | const handleMouseLeave = () => { 10 | setIsHover(false); 11 | }; 12 | 13 | const startButtonShape: React.CSSProperties = { 14 | display: 'flex', 15 | width: '252px', 16 | flexDirection: 'row', 17 | alignItems: 'center', 18 | position: 'absolute', 19 | justifyContent: 'center', 20 | height: '48px', 21 | // left: '1482px', 22 | // top: '1081px', 23 | borderRadius: '42px', 24 | padding: '14px 24px', 25 | columnGap: '16px', 26 | background: '#212121', 27 | border: '1px solid rgba(243, 246, 248, 0.1)', 28 | cursor: 'pointer', 29 | bottom: '20px', 30 | }; 31 | 32 | const startButtonHover: React.CSSProperties = { 33 | display: 'flex', 34 | flexDirection: 'row', 35 | alignItems: 'center', 36 | position: 'absolute', 37 | width: '252px', 38 | height: '48px', 39 | borderRadius: '42px', 40 | padding: '14px 24px', 41 | columnGap: '16px', 42 | justifyContent: 'center', 43 | background: '#1C1C1C', 44 | border: '1px solid rgba(243, 246, 248, 0.1)', 45 | cursor: 'pointer', 46 | bottom: '20px', 47 | }; 48 | 49 | const startIcon: React.CSSProperties = { 50 | width: '0', 51 | height: '0', 52 | borderTop: '8px solid transparent', 53 | borderBottom: '8px solid transparent', 54 | borderLeft: '16px solid rgba(243, 246, 248, 0.8)', 55 | flex: 'none', 56 | order: '0', 57 | flexGrow: '0', 58 | borderRadius: '2px', 59 | }; 60 | 61 | const startText: React.CSSProperties = { 62 | fontSize: '14px', 63 | lineHeight: '16px', 64 | color: '#F3F6F8', 65 | opacity: '0.8', 66 | flex: 'none', 67 | order: '1', 68 | flexGrow: '0', 69 | }; 70 | 71 | return ( 72 | 81 | ); 82 | }; 83 | 84 | export default Start; 85 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Buttons/SecondaryButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | icon_arrow, 4 | icon_copy, 5 | icon_download, 6 | icon_retract, 7 | icon_expand, 8 | icon_check, 9 | } from '../Icons'; 10 | 11 | const downloadButton: React.CSSProperties = { 12 | font: `-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue", sans-serif`, 13 | color: 'rgba(243, 246, 248, 0.7)', 14 | backgroundColor: 'rgb(243, 246, 248, 0.03)', 15 | height: '32px', 16 | border: '1px solid rgba(243, 246, 248, 0.05)', 17 | borderRadius: '6px', 18 | display: 'flex', 19 | flexDirection: 'row', 20 | alignItems: 'center', 21 | padding: '6px', 22 | gap: '4px', 23 | }; 24 | 25 | const downloadHover: React.CSSProperties = { 26 | font: `-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue", sans-serif`, 27 | color: 'rgba(243, 246, 248, 0.7)', 28 | backgroundColor: 'rgb(243, 246, 248, 0.1)', 29 | height: '32px', 30 | border: '1px solid rgba(243, 246, 248, 0.05)', 31 | borderRadius: '6px', 32 | display: 'flex', 33 | flexDirection: 'row', 34 | alignItems: 'center', 35 | padding: '6px', 36 | gap: '4px', 37 | }; 38 | 39 | const downloadTitleContainer: React.CSSProperties = { 40 | display: 'flex', 41 | alignItems: 'center', 42 | gap: '6px', 43 | }; 44 | 45 | const downloadIconBox: React.CSSProperties = { 46 | height: '20px', 47 | width: '20px', 48 | display: 'flex', 49 | alignItems: 'center', 50 | justifyContent: 'center', 51 | }; 52 | 53 | const downloadTitleText: React.CSSProperties = { 54 | fontSize: '12px', 55 | fontWeight: 500, 56 | lineHeight: '16px', 57 | color: 'rgba(243, 246, 248, 0.7)', 58 | marginRight: '12px', 59 | }; 60 | 61 | const downloadLine: React.CSSProperties = { 62 | width: '1px', 63 | height: '20px', 64 | background: 'rgba(243, 246, 248, 0.05)', 65 | }; 66 | 67 | export const downloadArrow: React.CSSProperties = { 68 | width: '20px', 69 | height: '20px', 70 | }; 71 | 72 | type Icon = 'download' | 'expand' | 'retract' | 'copy' | 'arrow' | 'check'; 73 | 74 | interface Props { 75 | icon: Icon; 76 | handleClick?: any; 77 | value?: string; 78 | arrow?: boolean; 79 | } 80 | 81 | const arrowIcons = (key: Icon): JSX.Element => { 82 | if (key == 'download') return icon_download; 83 | if (key == 'expand') return icon_expand; 84 | if (key == 'retract') return icon_retract; 85 | if (key == 'copy') return icon_copy; 86 | if (key == 'arrow') return icon_arrow; 87 | if (key == 'check') return icon_check; 88 | else return icon_arrow; 89 | }; 90 | 91 | const ArrowSection: React.FC = () => { 92 | return ( 93 | <> 94 |
95 |
{icon_arrow}
96 | 97 | ); 98 | }; 99 | 100 | const SecondaryButton: React.FC = ({ value, arrow, icon, handleClick }) => { 101 | const [isHover, setIsHover] = useState(false); 102 | const handleMouseEnter = () => { 103 | setIsHover(true); 104 | }; 105 | const handleMouseLeave = () => { 106 | setIsHover(false); 107 | }; 108 | 109 | return ( 110 | 123 | ); 124 | }; 125 | 126 | export default SecondaryButton; 127 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/ChromogenZustandObserver.tsx: -------------------------------------------------------------------------------- 1 | import Editor from './Editor'; 2 | import EditorTab from './EditorTab'; 3 | import React, { useState } from 'react'; 4 | import { generateTests } from './component-utils'; 5 | import GlobalStyle from '../GlobalStyle'; 6 | 7 | const panel: React.CSSProperties = { 8 | display: 'flex', 9 | position: 'relative', 10 | // width: '531.49px' 11 | }; 12 | 13 | interface Props { 14 | children: JSX.Element; 15 | } 16 | 17 | export const ChromogenZustandObserver: React.FC = ({ children }): JSX.Element => { 18 | const [code, setCode] = React.useState(''); 19 | const [storeMap] = React.useState>(new Map()); 20 | const [isHidden, setIsHidden] = useState(false); 21 | 22 | const timer = setInterval(() => { 23 | console.log('Firing'); 24 | setCode(String(generateTests(storeMap))); 25 | }, 1000); 26 | 27 | React.useEffect(() => { 28 | console.log(code); 29 | timer; 30 | }, [timer]); 31 | 32 | // React.useEffect(() => console.log(ledger.transactions[2].changedValues), []); 33 | 34 | return ( 35 |
36 | {children} 37 | {isHidden ? ( 38 | 39 | ) : ( 40 | 41 | )} 42 | 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CodeEditor from '@uiw/react-textarea-code-editor'; 3 | import NumberList from './Numbers'; 4 | import { Header } from './Header'; 5 | import RecordingButton from './Buttons/RecordingButton'; 6 | 7 | const editorStyle: React.CSSProperties = { 8 | display: 'flex', 9 | flexDirection: 'column', 10 | height: '100%', 11 | overflow: 'auto', 12 | borderLeft: '1px solid rgba(243,246,248,.1)', 13 | backgroundColor: '#1C1C1C', 14 | width: '50vw', 15 | }; 16 | const codePanel: React.CSSProperties = { 17 | display: 'flex', 18 | // flexGrow: 1, 19 | overflowY: 'scroll', 20 | height: 'calc(100vh - 56px)', 21 | }; 22 | 23 | interface Props { 24 | code: string; 25 | setIsHidden: Function; 26 | isHidden: boolean; 27 | } 28 | const Editorfield = ({ code, isHidden, setIsHidden }: Props): JSX.Element => { 29 | const [, setInnerCode] = useState(code); 30 | let breakLine = 0; 31 | 32 | for (let curr = 0; curr < code.length; curr++) { 33 | if (code[curr] == '\n') breakLine++; 34 | } 35 | 36 | console.log(breakLine); 37 | 38 | return ( 39 |
40 |
41 |
42 | 43 | setInnerCode(evn.target.value)} 49 | padding={15} 50 | style={{ 51 | maxWidth: 1000, 52 | width: 'calc(100% - 60px)', 53 | maxHeight: '100vh', 54 | overflow: 'visible', 55 | fontSize: 12, 56 | backgroundColor: '#1c1c1c', 57 | fontFamily: 58 | 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace', 59 | }} 60 | /> 61 |
62 | 63 |
64 | ); 65 | }; 66 | 67 | export default Editorfield; 68 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/EditorTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SecondaryButton from './Buttons/SecondaryButton'; 3 | 4 | type Props = { 5 | setIsHidden: Function; 6 | isHidden: boolean; 7 | }; 8 | 9 | const EditorTab = (props: Props): JSX.Element => { 10 | const { isHidden, setIsHidden } = props; 11 | return ( 12 |
13 | setIsHidden(!isHidden)} /> 14 |
15 | ); 16 | }; 17 | 18 | export default EditorTab; 19 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import SecondaryButton from './Buttons/SecondaryButton'; 3 | import { generateFile, generateTests } from './component-utils'; 4 | 5 | const toolBar: React.CSSProperties = { 6 | display: 'flex', 7 | height: '56px', 8 | width: '100%', 9 | padding: '8px 16px', 10 | alignItems: 'center', 11 | gap: '8px', 12 | borderBottom: `1px solid rgba(243,246,248,.05)`, 13 | }; 14 | 15 | const toolBarLogoBox: React.CSSProperties = { 16 | height: '40px', 17 | width: '40px', 18 | display: 'flex', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }; 22 | 23 | const toolBarTitleContainer: React.CSSProperties = { 24 | display: 'flex', 25 | flexDirection: 'column', 26 | color: '#fff', 27 | lineHeight: '16px', 28 | opacity: 0.9, 29 | marginLeft: '8px', 30 | rowGap: '4px', 31 | flexGrow: 1, 32 | }; 33 | 34 | const toolBarTitle: React.CSSProperties = { 35 | fontSize: '16px', 36 | fontWeight: 500, 37 | fontFamily: 'Inter !important', 38 | }; 39 | 40 | const toolBarDescription: React.CSSProperties = { 41 | fontSize: '12px', 42 | fontWeight: 400, 43 | }; 44 | 45 | export const copyButton: React.CSSProperties = { 46 | height: '32px', 47 | width: '32px', 48 | backgroundColor: 'rgb(243, 246, 248, 0.03)', 49 | border: '1px solid rgba(243, 246, 248, 0.05)', 50 | borderRadius: '6px', 51 | display: 'flex', 52 | alignItems: 'center', 53 | padding: '6px', 54 | }; 55 | 56 | export const minimizeButton: React.CSSProperties = { 57 | height: '24px', 58 | width: '24px', 59 | backgroundColor: 'white', 60 | marginLeft: '10px', 61 | padding: 0, 62 | cursor: 'pointer', 63 | }; 64 | 65 | export const minimizeIcon: React.CSSProperties = {}; 66 | 67 | export const Header = ({ isHidden, setIsHidden }) => { 68 | const [file, setFile] = useState(undefined); 69 | const [copied, setCopied] = useState(false); 70 | 71 | useEffect(() => { 72 | copyOff(); 73 | }, [copied]); 74 | 75 | function copyOff() { 76 | setTimeout(() => { 77 | setCopied(false); 78 | }, 3000); 79 | } 80 | 81 | return ( 82 |
131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const icon_download = ( 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | ); 17 | 18 | export const icon_copy = ( 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | ); 32 | 33 | export const icon_retract = ( 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | ); 47 | 48 | export const icon_expand = ( 49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | ); 62 | 63 | export const icon_arrow = ( 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | 74 | export const icon_check = ( 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | ); 88 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Numbers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const listStyle: React.CSSProperties = { 4 | paddingBlock: '28px', 5 | display: 'flex', 6 | flexDirection: 'column', 7 | alignContent: 'flex-end', 8 | textAlign: 'end', 9 | background: '#1c1c1c', 10 | paddingInline: '16px', 11 | border: '1px solid #1c1c1c', 12 | height: 'auto', 13 | width: '60px', 14 | }; 15 | 16 | const numberStyle: React.CSSProperties = { 17 | fontSize: 12, 18 | color: '#747478', 19 | fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo,monospace', 20 | }; 21 | 22 | const unique = (val: number | string) => ( 23 |

24 | {val} 25 |

26 | ); 27 | 28 | const numerous = (num = 1000) => { 29 | let pointer: number = 1; 30 | let allNumbers: JSX.Element[] = []; 31 | 32 | while (pointer < num) { 33 | allNumbers = [...allNumbers, unique(pointer)]; 34 | pointer++; 35 | } 36 | 37 | console.log(allNumbers); 38 | 39 | return allNumbers; 40 | }; 41 | 42 | const NumberList = ({ number }): JSX.Element =>
{numerous(number)}
; 43 | 44 | export default NumberList; 45 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/Resizing/Resizer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const ResizerStyle = styled.div` 5 | position: absolute; 6 | cursor: ew-resize; 7 | width: 2px; 8 | height: 100%; 9 | z-index: 1; 10 | left: -1; 11 | top: 0; 12 | &:hover { 13 | background: #4848be; 14 | } 15 | `; 16 | 17 | interface ResizerProps { 18 | onResize: Function; 19 | } 20 | 21 | const Resizer: React.FC = ({ onResize }) => { 22 | const [direction, setDirection] = useState(''); 23 | const [mouseDown, setMouseDown] = useState(false); 24 | 25 | useEffect(() => { 26 | const handleMouseMove = (e) => { 27 | if (!direction) return; 28 | onResize(direction, e.movementX, e.movementY); 29 | }; 30 | 31 | if (mouseDown) { 32 | window.addEventListener('mousemove', handleMouseMove); 33 | } 34 | 35 | return () => window.removeEventListener('mousemove', handleMouseMove); 36 | }, [mouseDown, direction, onResize]); 37 | 38 | useEffect(() => { 39 | const handleMouseUp = () => setMouseDown(false); 40 | window.addEventListener('mouseup', handleMouseUp); 41 | 42 | return () => window.removeEventListener('mouseup', handleMouseUp); 43 | }, []); 44 | 45 | const handleMouseDown = (direction) => { 46 | setDirection(direction); 47 | setMouseDown(true); 48 | }; 49 | 50 | return handleMouseDown('left')}>; 51 | }; 52 | 53 | export default Resizer; 54 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/component-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { CSSProperties } from 'react'; 3 | // import type { SerializableParam } from 'recoil'; 4 | import type { Ledger } from '../types'; 5 | 6 | import { ledger } from '../utils/ledger'; 7 | import { output } from '../output/output'; 8 | /* eslint-enable */ 9 | 10 | const buttonStyle: CSSProperties = { 11 | display: 'inline-block', 12 | margin: '8px', 13 | marginLeft: '13px', 14 | padding: '0px', 15 | height: '25px', 16 | width: '65px', 17 | borderRadius: '4px', 18 | justifyContent: 'space-evenly', 19 | border: '1px', 20 | cursor: 'pointer', 21 | color: '#90d1f0', 22 | fontSize: '10px', 23 | }; 24 | 25 | const divStyle: CSSProperties = { 26 | display: 'flex', 27 | position: 'relative', 28 | height: '100%', 29 | top: '0px', 30 | right: '0px', 31 | bottom: '0px', 32 | width: '30vw', 33 | backgroundColor: '#222222', 34 | borderRadius: '4px', 35 | margin: 0, 36 | padding: 0, 37 | zIndex: 999999, 38 | }; 39 | 40 | const playStyle: CSSProperties = { 41 | boxSizing: 'border-box', 42 | marginLeft: '25px', 43 | borderStyle: 'solid', 44 | borderWidth: '7px 0px 7px 14px', 45 | }; 46 | 47 | const pauseStyle: CSSProperties = { 48 | width: '14px', 49 | height: '14px', 50 | borderWidth: '0px 0px 0px 10px', 51 | borderStyle: 'double', 52 | marginLeft: '27px', 53 | }; 54 | 55 | export const styles = { buttonStyle, divStyle, playStyle, pauseStyle }; 56 | 57 | /* 58 | generateFile generates test file & sets download URL 59 | The passed in setFile function updates _file_ state in Chromogen observer 60 | Applying only at point-of-download keeps performance cost low for users who 61 | don't need to pass nodes while creating a moderate performance hit for others 62 | only while downloading, never while interacting with their app. 63 | */ 64 | export const generateFile = (setFile: Function, storeMap: Map): string[] => { 65 | const tests = generateTests(storeMap); 66 | const blob = new Blob(tests); 67 | setFile(URL.createObjectURL(blob)); 68 | return tests; 69 | }; 70 | 71 | /* generateTests is invoked within generateFile, returning our desired test string within an array */ 72 | export const generateTests = (storeMap: Map): string[] => { 73 | const { initialRender, transactions } = ledger; 74 | 75 | const finalLedger: Ledger = 76 | storeMap.size > 0 77 | ? { 78 | initialRender, 79 | transactions: transactions, 80 | } 81 | : { ...ledger }; 82 | 83 | return [output(finalLedger)]; 84 | }; 85 | -------------------------------------------------------------------------------- /package/zustand_generator/src/component/panel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { useState, useEffect } from 'react'; 3 | import { useStore } from '../utils/store'; 4 | import { styles, generateFile, generateTests } from './component-utils'; 5 | /* eslint-enable */ 6 | 7 | /* using a zustand store to keep track of recording state */ 8 | const selector = (state) => ({ 9 | recording: state.recording, 10 | toggleRecording: state.toggleRecording, 11 | }); 12 | 13 | export const Panel: React.FC = () => { 14 | // Initializing as undefined over null to match React typing for AnchorHTML attributes 15 | const [file, setFile] = useState(undefined); 16 | const [storeMap] = useState>(new Map()); 17 | const { recording, toggleRecording } = useStore<{ 18 | recording: boolean; 19 | toggleRecording: Function; 20 | }>(selector); 21 | 22 | // Auto-click download link when a new file is generated (via button click) 23 | useEffect(() => document.getElementById('chromogen-download')!.click(), [file]); 24 | // ! to get around strict null check in tsconfig 25 | 26 | const [pauseColor, setPauseColor] = useState('#90d1f0'); 27 | const pauseBorderStyle = { 28 | borderColor: `${pauseColor}`, 29 | }; 30 | 31 | const [playColor, setPlayColor] = useState('transparent transparent transparent #90d1f0'); 32 | const playBorderStyle = { 33 | borderColor: `${playColor}`, 34 | }; 35 | 36 | return ( 37 | <> 38 | { 39 |
40 |
41 |

Testing!

42 | 71 | 91 | 113 |
114 |
115 | } 116 | 122 | Download Test 123 | 124 | 125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /package/zustand_generator/src/output/output-utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Transaction, InitialRender } from '../types'; 3 | // } from '../types'; 4 | 5 | import { dummyParam } from '../utils/utils'; 6 | 7 | /* eslint-enable */ 8 | 9 | /* ----- HELPER FUNCTIONS ----- */ 10 | 11 | export function importZustandStore(): string { 12 | return `import useStore from '';`; 13 | } 14 | 15 | export function testInitialState(initialRender: InitialRender): string { 16 | return Object.entries(initialRender).reduce((acc, [k, v]) => { 17 | return ( 18 | acc 19 | + `\tit('${k} should initialize correctly', () => {\n\t\texpect(result.current.${k}).toStrictEqual(${JSON.stringify( 20 | v, 21 | )});\n\t});\n\n` 22 | ); 23 | }, ''); 24 | } 25 | 26 | const dummyTransaction = { action: dummyParam, changedValues: {} }; 27 | 28 | //Takes in an array of transactions and returns a full set of tests ("it blocks") for all actions and corresponding state changes 29 | export function testStateChangesAct(transactions: Transaction[]): string { 30 | //Groups transactions together based on whether the transactions impact the same slice of state 31 | //Each "group" of transactions will affect each store parameter 0 or 1 times. 32 | let groupedTransactions: Transaction[][] = [...transactions, dummyTransaction].reduce( 33 | ( 34 | acc: { 35 | groups: Transaction[][]; 36 | currentGroup: Transaction[]; 37 | changedValues: { [nameOfChangedValue: string]: any }; 38 | }, 39 | cur, 40 | ) => { 41 | if ( 42 | Object.keys(cur.changedValues).some((v) => acc.changedValues[v]) 43 | || cur.action === dummyParam 44 | ) { 45 | acc.groups.push(acc.currentGroup); 46 | acc.currentGroup = [cur]; 47 | acc.changedValues = Object.keys(cur.changedValues).reduce((acc, k) => { 48 | acc[k] = true; 49 | return acc; 50 | }, {}); 51 | } else { 52 | acc.currentGroup.push(cur); 53 | Object.keys(cur.changedValues).forEach((k) => (acc.changedValues[k] = true)); 54 | } 55 | return acc; 56 | }, 57 | { groups: [], currentGroup: [], changedValues: {} }, 58 | ).groups; 59 | 60 | //For each group of transactions, we generate an "It block" 61 | return groupedTransactions.reduce( 62 | (acc, group) => { 63 | const { str, actBlock } = generateItBlock(group); 64 | acc.str += str; 65 | acc.actStatements = actBlock; 66 | return acc; 67 | }, 68 | { str: '', actStatements: '' }, 69 | ).str; 70 | } 71 | //Takes in an entry for a slice of state and generates an expect statement asserting that the state properties have correct value in the store 72 | export function testStateChangesExpect([propertyName, newValue]: [string, any]): string { 73 | return `\nexpect(result.current.${propertyName}).toStrictEqual(${JSON.stringify(newValue)});`; 74 | } 75 | 76 | //Takes in a transaction and generates an act statement using the action name and argument(s) 77 | export function generateActLine(t: Transaction): string { 78 | const { action } = t; 79 | const args = t.arguments; 80 | return `\tresult.current.${action}(${args?.map((arg) => JSON.stringify(arg)).join(', ')});\n`; 81 | } 82 | 83 | //Takes in an array of grouped Transactions and returns an It Block (unit test) with all act & 84 | // expect statements for transactions in input 85 | function generateItBlock(transactions: Transaction[]): { str: string; actBlock: string } { 86 | const valuesChanged: string[] = []; 87 | let expectBlock = ''; 88 | 89 | transactions.forEach((t) => 90 | Object.entries(t.changedValues).forEach(([changedValue, newValue]) => { 91 | valuesChanged.push(changedValue); 92 | expectBlock += testStateChangesExpect([changedValue, newValue]); 93 | }), 94 | ); 95 | 96 | let newActBlock = transactions.map(generateActLine).join(''); 97 | 98 | return { 99 | str: `\n\tit('${valuesChanged.join(' & ')} should update correctly', () => { 100 | const { result } = renderHook(useStore); 101 | 102 | act(() => {\n${newActBlock}\n}); 103 | 104 | ${expectBlock} 105 | });`, 106 | actBlock: newActBlock, 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /package/zustand_generator/src/output/output.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { Ledger } from '../types'; 3 | import { importZustandStore, testInitialState, testStateChangesAct } from './output-utils'; 4 | /* eslint-enable */ 5 | 6 | /* ----- MAIN ----- */ 7 | /* Output takes in initialRender and transactions from the ledger and tests them from the functions in output-utils*/ 8 | export const output = ({ initialRender, transactions }: Ledger): string => 9 | ` 10 | import { renderHook, act } from '@testing-library/react'; 11 | ${importZustandStore()} 12 | 13 | describe('INITIAL RENDER', () => { 14 | const { result } = renderHook(useStore); 15 | 16 | ${testInitialState(initialRender)} 17 | }); 18 | 19 | 20 | describe('STATE CHANGES', () => { 21 | const { result } = renderHook(useStore); 22 | 23 | ${testStateChangesAct(transactions)} 24 | });`; 25 | 26 | export const unitOutput = (initialRender: any, action: any): string => { 27 | console.log('within unitOutput. init, action : ', initialRender, action); 28 | let retString = ''; 29 | if (initialRender) { 30 | console.log('within unitOutput initialRender'); 31 | retString += ` 32 | import { renderHook, act } from '@testing-library/react'; 33 | ${importZustandStore()} 34 | describe('INITIAL RENDER', () => { 35 | const { result } = renderHook(useStore); 36 | ${testInitialState(initialRender)} 37 | }); 38 | `; 39 | } else if (action) { 40 | console.log('within unitOutput action'); 41 | retString += ` 42 | describe('STATE CHANGES', () => { 43 | const { result } = renderHook(useStore); 44 | ${testStateChangesAct([action])} 45 | });`; 46 | } 47 | return retString; 48 | }; 49 | 50 | //NOTE: Test output is not linted/formatted in any meaningful way. The Chromogen team recommends formatting tests in line with your personal or organizational preferences; 51 | -------------------------------------------------------------------------------- /package/zustand_generator/src/types.ts: -------------------------------------------------------------------------------- 1 | // ----- EXPORTING TYPES TO BE USED IN SRC/.TSX FILES ----- 2 | 3 | type NotAFunction = { [k: string]: unknown } & ({ bind?: never } | { call?: never }); 4 | 5 | export type InitialRender = { 6 | [stateParam: string]: NotAFunction; 7 | }; 8 | 9 | export interface Transaction { 10 | action: string; 11 | arguments?: T; 12 | changedValues: { 13 | [nameOfChangedValue: string]: NotAFunction; 14 | }; 15 | } 16 | 17 | export interface Ledger { 18 | initialRender: InitialRender; 19 | transactions: Transaction[]; 20 | } 21 | -------------------------------------------------------------------------------- /package/zustand_generator/src/utils/ledger.ts: -------------------------------------------------------------------------------- 1 | import type { Ledger } from '../types'; 2 | 3 | export const ledger: Ledger = { 4 | initialRender: {}, 5 | transactions: [], 6 | }; 7 | -------------------------------------------------------------------------------- /package/zustand_generator/src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | interface RecordingState { 4 | recording: boolean; 5 | toggleRecording: () => void; 6 | } 7 | 8 | /* 9 | Allows for recording to always be on during page load 10 | and the ability to pause recording 11 | */ 12 | export const useStore = create((set) => ({ 13 | recording: true, 14 | toggleRecording: () => { 15 | set((state) => ({ recording: !state.recording }), false); 16 | }, 17 | })); 18 | -------------------------------------------------------------------------------- /package/zustand_generator/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const dummyParam = 'chromogenDummyParam'; 2 | --------------------------------------------------------------------------------
83 |
84 |
85 | 92 | 93 | 94 | 95 | 96 | 101 | 102 | 103 |
104 |
105 |
106 |
Chromogen Tests
107 |
Interact with the app to generate tests
108 |
109 |
115 | generateFile(setFile, new Map())} 120 | /> 121 | 122 | { 125 | navigator.clipboard.writeText(generateTests(new Map())[0]); 126 | setCopied(true); 127 | }} 128 | /> 129 | setIsHidden(!isHidden)} /> 130 |