├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-docs.yml │ ├── build-main.yml │ └── test-lint-and-build.yml ├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── customTheme.js ├── main.js ├── manager.js ├── preview-head.html └── preview.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.html ├── jest.config.js ├── localize.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── App.tsx │ ├── AppConfigContext.tsx │ ├── LanguageConfigContext.tsx │ └── index.ts ├── assets │ ├── LocalizedStrings.tsx │ ├── images │ │ ├── dave-hoefler-reduced-1.jpg │ │ ├── dave-hoefler-reduced-2.jpg │ │ ├── dave-hoefler-reduced-3.jpg │ │ ├── dave-hoefler-reduced-4.jpg │ │ ├── dave-hoefler-reduced-5.jpg │ │ └── index.d.ts │ └── strings │ │ ├── de.ts │ │ ├── en.ts │ │ ├── es.ts │ │ └── index.ts ├── components │ ├── Cards │ │ ├── Base │ │ │ ├── Base.tsx │ │ │ └── index.ts │ │ ├── LegislationCard │ │ │ ├── LegislationCard.stories.tsx │ │ │ ├── LegislationCard.tsx │ │ │ └── index.ts │ │ ├── MeetingCard │ │ │ ├── MeetingCard.stories.tsx │ │ │ ├── MeetingCard.tsx │ │ │ └── index.ts │ │ ├── PersonCard │ │ │ ├── PersonCard.stories.tsx │ │ │ ├── PersonCard.tsx │ │ │ └── index.ts │ │ └── constants.ts │ ├── Details │ │ ├── EventVideo │ │ │ ├── EventVideo.stories.tsx │ │ │ ├── EventVideo.tsx │ │ │ ├── ShareVideo │ │ │ │ ├── ShareVideo.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── vjs-theme-cdp.css │ │ ├── Legislation │ │ │ ├── LegislationHistory.stories.tsx │ │ │ ├── LegislationHistory.tsx │ │ │ ├── LegislationIntroduction.stories.tsx │ │ │ ├── LegislationIntroduction.tsx │ │ │ ├── LegislationLatestVote.stories.tsx │ │ │ ├── LegislationLatestVote.tsx │ │ │ ├── LegislationOverview.stories.tsx │ │ │ ├── LegislationOverview.tsx │ │ │ ├── LegislativeHistoryNode.tsx │ │ │ ├── ProgressBar.tsx │ │ │ ├── VoteBar.tsx │ │ │ └── VoteDistributionGraphic.tsx │ │ ├── MinutesItemsList │ │ │ ├── DocumentsList.tsx │ │ │ ├── MinutesItemsList.stories.tsx │ │ │ ├── MinutesItemsList.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── TranscriptFull │ │ │ ├── TranscriptFull.stories.tsx │ │ │ ├── TranscriptFull.tsx │ │ │ └── index.ts │ │ ├── TranscriptItem │ │ │ ├── TranscriptItem.stories.tsx │ │ │ ├── TranscriptItem.tsx │ │ │ └── index.tsx │ │ └── TranscriptSearch │ │ │ ├── TranscriptItems.tsx │ │ │ ├── TranscriptSearch.stories.tsx │ │ │ ├── TranscriptSearch.tsx │ │ │ └── index.ts │ ├── Filters │ │ ├── EventsFilter │ │ │ ├── EventsFilter.tsx │ │ │ └── index.ts │ │ ├── FilterPopup │ │ │ ├── FilterPopup.stories.tsx │ │ │ ├── FilterPopup.test.tsx │ │ │ ├── FilterPopup.tsx │ │ │ └── index.ts │ │ ├── FiltersContainer │ │ │ ├── FiltersContainer.tsx │ │ │ └── index.ts │ │ ├── SelectDateRange │ │ │ ├── SelectDateRange.stories.tsx │ │ │ ├── SelectDateRange.test.tsx │ │ │ ├── SelectDateRange.tsx │ │ │ ├── getDateText.test.ts │ │ │ ├── getDateText.ts │ │ │ └── index.ts │ │ ├── SelectSorting │ │ │ ├── SelectSorting.stories.tsx │ │ │ ├── SelectSorting.test.tsx │ │ │ ├── SelectSorting.tsx │ │ │ ├── getSortingText.test.ts │ │ │ ├── getSortingText.ts │ │ │ └── index.ts │ │ ├── SelectTextFilterOptions │ │ │ ├── SelectTextFilterOptions.stories.tsx │ │ │ ├── SelectTextFilterOptions.test.tsx │ │ │ ├── SelectTextFilterOptions.tsx │ │ │ ├── getCheckboxText.test.ts │ │ │ ├── getCheckboxText.ts │ │ │ ├── getSelectedOptions.test.ts │ │ │ ├── getSelectedOptions.ts │ │ │ └── index.ts │ │ ├── Shared │ │ │ └── Form.tsx │ │ ├── actions.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ ├── useFilter.test.ts │ │ └── useFilter.ts │ ├── Layout │ │ ├── Footer │ │ │ ├── Footer.stories.tsx │ │ │ ├── Footer.tsx │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Header.stories.tsx │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ ├── HomeSearchBar │ │ │ ├── HomeSearchBar.stories.tsx │ │ │ ├── HomeSearchBar.tsx │ │ │ └── index.tsx │ │ ├── LocalizationWidget │ │ │ ├── LocalizationWidget.stories.tsx │ │ │ ├── LocalizationWidget.tsx │ │ │ └── index.tsx │ │ └── TabbedContainer │ │ │ ├── Tab.tsx │ │ │ ├── TabbedContainer.stories.tsx │ │ │ ├── TabbedContainer.tsx │ │ │ └── index.ts │ ├── Shared │ │ ├── AbsoluteBox.tsx │ │ ├── AbstainIcon.tsx │ │ ├── AdoptedIcon.tsx │ │ ├── ChevronDownIcon.tsx │ │ ├── CollapseIcon.tsx │ │ ├── CopyIcon.tsx │ │ ├── DecisionResult.tsx │ │ ├── DefaultAvatar.tsx │ │ ├── Details.tsx │ │ ├── DocumentTextIcon.tsx │ │ ├── Dot.tsx │ │ ├── ExpandIcon.tsx │ │ ├── FetchCardsStatus.tsx │ │ ├── H2.tsx │ │ ├── InProgressIcon.tsx │ │ ├── MinusIcon.tsx │ │ ├── PageContainer.tsx │ │ ├── PlaceHolder.tsx │ │ ├── PlayIcon.tsx │ │ ├── PlusIcon.tsx │ │ ├── RejectedIcon.tsx │ │ ├── ResponsiveTab.tsx │ │ ├── SearchBar.tsx │ │ ├── SearchPageTitle.tsx │ │ ├── ShareIcon.tsx │ │ ├── ShowMoreCards.tsx │ │ ├── Types │ │ │ ├── IndividualMeetingVote.tsx │ │ │ ├── Matter.tsx │ │ │ └── MeetingVote.tsx │ │ ├── Ul.tsx │ │ ├── index.ts │ │ └── util │ │ │ └── voteDistribution.ts │ └── Tables │ │ ├── EmptyRow │ │ ├── EmptyRow.tsx │ │ └── index.ts │ │ ├── MeetingVotesTable │ │ ├── MeetingVotesTable.stories.tsx │ │ ├── MeetingVotesTable.tsx │ │ └── index.ts │ │ ├── MeetingVotesTableRow │ │ ├── MeetingVotesTableRow.tsx │ │ └── index.ts │ │ ├── ReactiveTable │ │ ├── ReactiveTable.tsx │ │ └── index.ts │ │ ├── ReactiveTableHeader │ │ ├── ReactiveTableHeader.tsx │ │ └── index.ts │ │ ├── ReactiveTableRow │ │ ├── ReactiveTableRow.tsx │ │ └── index.ts │ │ ├── VotingTable │ │ ├── VotingTable.stories.tsx │ │ ├── VotingTable.tsx │ │ └── index.ts │ │ └── VotingTableRow │ │ ├── VotingTableRow.stories.tsx │ │ ├── VotingTableRow.tsx │ │ └── index.ts ├── constants │ ├── ProjectConstants.ts │ └── StyleConstants.ts ├── containers │ ├── CardsContainer │ │ ├── CardsContainer.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── EventContainer │ │ ├── EventContainer.stories.tsx │ │ ├── EventContainer.tsx │ │ ├── EventInfoTabs.tsx │ │ ├── SessionsVideos.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── EventsContainer │ │ ├── EventsContainer.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── FetchDataContainer │ │ ├── FetchDataContainer.tsx │ │ ├── LazyFetchDataContainer.tsx │ │ ├── index.ts │ │ └── useFetchData.ts │ ├── MatterContainer │ │ ├── MatterContainer.tsx │ │ └── index.ts │ ├── PeopleContainer │ │ ├── PeopleContainer.tsx │ │ └── index.ts │ ├── PersonContainer │ │ ├── ContactPerson.tsx │ │ ├── CoverImage.tsx │ │ ├── MattersSponsored.tsx │ │ ├── PersonContainer.stories.tsx │ │ ├── PersonContainer.tsx │ │ ├── PersonRoles.tsx │ │ ├── PersonVotes.tsx │ │ ├── constants.ts │ │ ├── index.ts │ │ └── types.ts │ ├── SearchContainer │ │ ├── SearchContainer.tsx │ │ ├── SearchResultContainer.tsx │ │ ├── index.ts │ │ └── types.ts │ └── SearchEventsContainer │ │ ├── SearchEventsContainer.tsx │ │ ├── index.ts │ │ └── types.ts ├── hooks │ ├── useDocumentTitle.ts │ ├── useFetchModels.ts │ └── useSearchCards.ts ├── index-react.tsx ├── index.ts ├── models │ ├── Body.ts │ ├── Event.ts │ ├── EventMinutesItem.ts │ ├── EventMinutesItemFile.ts │ ├── File.ts │ ├── IndexedEventGram.ts │ ├── IndexedMatterGram.ts │ ├── Matter.ts │ ├── MatterFile.ts │ ├── MatterSponsor.ts │ ├── MatterStatus.ts │ ├── MinutesItem.ts │ ├── Model.ts │ ├── Person.ts │ ├── Role.ts │ ├── Seat.ts │ ├── Session.ts │ ├── Transcript.ts │ ├── TranscriptJson.ts │ ├── Vote.ts │ ├── constants.ts │ └── util │ │ ├── RoleUtilities.ts │ │ └── validateResponseData.ts ├── networking │ ├── BodyService.ts │ ├── EventMinutesItemFileService.ts │ ├── EventMinutesItemService.ts │ ├── EventSearchService.ts │ ├── EventService.ts │ ├── FileService.ts │ ├── IndexedEventGramService.ts │ ├── MatterService.ts │ ├── MatterSponsorService.ts │ ├── MatterStatusService.ts │ ├── ModelService.ts │ ├── NetworkResponse.ts │ ├── NetworkService.ts │ ├── PersonService.ts │ ├── PopulationOptions.ts │ ├── RoleService.ts │ ├── SeatService.ts │ ├── SessionService.ts │ ├── TranscriptJsonService.ts │ ├── TranscriptService.ts │ ├── VoteService.ts │ ├── constants.ts │ └── test │ │ └── ModelService.test.ts ├── pages │ ├── ErrorPage │ │ ├── ErrorPage.tsx │ │ └── index.ts │ ├── EventPage │ │ ├── EventPage.tsx │ │ ├── index.ts │ │ └── utils.ts │ ├── EventsPage │ │ ├── EventsPage.tsx │ │ └── index.ts │ ├── HomePage │ │ ├── HomePage.tsx │ │ └── index.ts │ ├── MatterPage │ │ ├── MatterPage.tsx │ │ └── index.ts │ ├── PeoplePage │ │ ├── PeoplePage.tsx │ │ └── index.ts │ ├── PersonPage │ │ ├── PersonPage.tsx │ │ └── index.ts │ ├── SearchEventsPage │ │ ├── SearchEventsPage.tsx │ │ ├── index.ts │ │ └── types.ts │ └── SearchPage │ │ ├── SearchPage.tsx │ │ ├── index.ts │ │ └── types.ts ├── setupEnzyme.ts ├── stories │ ├── Introduction.stories.mdx │ ├── assets │ │ ├── code-brackets.svg │ │ ├── colors.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── plugin.svg │ │ ├── repo.svg │ │ └── stackalt.svg │ └── model-mocks │ │ ├── body.ts │ │ ├── event.ts │ │ ├── eventMinutesItem.ts │ │ ├── file.ts │ │ ├── imageUrl.ts │ │ ├── indexedMatterGram.ts │ │ ├── matter.ts │ │ ├── matterSponsor.ts │ │ ├── matterStatus.ts │ │ ├── person.ts │ │ ├── role.ts │ │ ├── seat.ts │ │ ├── sentence.ts │ │ └── vote.ts ├── styles │ ├── colors.ts │ ├── fonts.ts │ └── mediaBreakpoints.ts └── utils │ ├── cleanText.ts │ ├── createError.ts │ ├── firestoreTimestampToDate.ts │ ├── getTimeZoneName.ts │ ├── isSubstring.ts │ ├── ordinalSuffix.ts │ ├── padNumWithZero.ts │ ├── secondsToHHMMSS.ts │ └── test │ └── getTimeZoneDate.test.ts ├── tsconfig.base.json ├── tsconfig.json ├── vite.app.config.js └── vite.lib.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [*.bat] 17 | indent_style = tab 18 | end_of_line = crlf 19 | 20 | [LICENSE] 21 | insert_final_newline = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "plugin:react/recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "prettier/@typescript-eslint", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | "env": { 10 | "mocha": true 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "project": "tsconfig.json" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "root": true, 18 | "rules": { 19 | "@typescript-eslint/explicit-function-return-type": "off", 20 | "@typescript-eslint/explicit-member-accessibility": "off", 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/no-var-requires": "off", 23 | "@typescript-eslint/camelcase": "off", 24 | } 25 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve cdp-frontend 4 | labels: bug 5 | --- 6 | 7 | ### Describe the Bug 8 | 9 | _A clear and concise description of what the bug is._ 10 | 11 | ### Reproduction 12 | 13 | _Steps to reproduce the behavior:_ 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ### Expected Behavior 21 | 22 | _A clear and concise description of what you expected to happen._ 23 | 24 | ### Screenshots 25 | 26 | _If applicable, add screenshots to help explain your problem._ 27 | 28 | ### Environment 29 | 30 | _Any additional information about your environment._ 31 | 32 | - OS Version: _[e.g. macOS 11.3.1]_ 33 | - Browser Version: _[e.g. Chrome 89.0]_ 34 | - CDP Frontend Version: _[e.g. 0.5.0]_ 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a feature for cdp-frontend 4 | labels: enhancement 5 | --- 6 | 7 | 13 | 14 | ### Feature Description 15 | 16 | _A clear and concise description of the feature you're requesting._ 17 | 18 | ### Use Case 19 | 20 | _Please provide a use case to help us understand your request in context._ 21 | 22 | ### Solution 23 | 24 | _Please describe your ideal solution._ 25 | 26 | ### Alternatives 27 | 28 | _Please describe any alternatives you've considered, even if you've dismissed them._ 29 | 30 | ### Screenshots/References 31 | 32 | _In the case of a visually oriented feature or component request, providing drawings, screenshots, and/or links to examples of what you're seeking will be very helpful._ 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | ### Link to Relevant Issue 18 | 19 | This pull request resolves # 20 | 21 | ### Description of Changes 22 | 23 | _Include a description of the proposed changes._ 24 | 25 | ### Link to Forked Storybook Site 26 | 27 | _If component changes (especially visual changes) are contained in this PR, we ask that you provide a link to a publicly viewable version of the Storybook docs site, so PR reviewers can see your changes without having to install and view your code locally._ 28 | 29 | _Please see `CONTRIBUTING.md` for directions on how this can be done._ 30 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '16.x' 18 | - name: Install Dependencies 19 | run: | 20 | npm i 21 | - name: Build Docs 22 | run: | 23 | npm run build-storybook-docs 24 | - name: Deploy Docs 25 | uses: JamesIves/github-pages-deploy-action@releases/v3 26 | with: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | BASE_BRANCH: main # The branch the action should deploy from. 29 | BRANCH: gh-pages # The branch the action should deploy to. 30 | FOLDER: storybook-static # The folder the action should deploy. 31 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: Build Main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | # 9 | # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 10 | # Run every Monday at 18:00:00 UTC (Monday at 10:00:00 PST) 11 | - cron: '0 18 * * 1' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Setup Node 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '16.x' 23 | - name: Install Dependencies 24 | run: | 25 | npm i 26 | - name: Run Tests 27 | run: | 28 | CI=true npm run test 29 | 30 | lint: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v1 35 | - name: Setup Node 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: '16.x' 39 | - name: Install Dependencies 40 | run: | 41 | npm i 42 | - name: Run Lint 43 | run: | 44 | npm run lint 45 | - name: Run Format Check 46 | run: | 47 | npm run checkFormat 48 | 49 | publish: 50 | needs: [test, lint] 51 | if: github.event_name == 'push' && contains(github.event.head_commit.message, 'Bump version') 52 | runs-on: ubuntu-latest 53 | 54 | steps: 55 | - uses: actions/checkout@v1 56 | - name: Setup Node 57 | uses: actions/setup-node@v1 58 | with: 59 | node-version: '16.x' 60 | registry-url: 'https://registry.npmjs.org' 61 | - name: Install Dependencies 62 | run: | 63 | npm i 64 | - name: Run Build 65 | run: | 66 | npm run build 67 | - name: Publish to npm 68 | run: npm publish 69 | env: 70 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/test-lint-and-build.yml: -------------------------------------------------------------------------------- 1 | name: Test, Lint, and Build 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Setup Node 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: '16.x' 15 | - name: Install Dependencies 16 | run: | 17 | npm i 18 | - name: Run Tests 19 | run: | 20 | CI=true npm run test 21 | 22 | lint: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v1 27 | - name: Setup Node 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: '16.x' 31 | - name: Install Dependencies 32 | run: | 33 | npm i 34 | - name: Run Lint 35 | run: | 36 | npm run lint 37 | - name: Run Format Check 38 | run: | 39 | npm run checkFormat 40 | 41 | build: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v1 46 | - name: Setup Node 47 | uses: actions/setup-node@v1 48 | with: 49 | node-version: '16.x' 50 | - name: Install Dependencies 51 | run: | 52 | npm i 53 | - name: Run Build 54 | run: | 55 | npm run build 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | .vscode 4 | node_modules 5 | lib 6 | es 7 | dist 8 | build 9 | type-declarations 10 | dist-docs 11 | coverage 12 | storybook-static 13 | .DS_Store -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | "hooks": { 2 | "pre-commit": "lint-staged" 3 | } -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx,js,jsx}": ["prettier --write", "git add"] 3 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all image files 2 | *.svg 3 | *.jpg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "jsxBracketSameLine": false, 4 | "printWidth": 100, 5 | "trailingComma": "es5", 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /.storybook/customTheme.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | base: "dark", 5 | brandTitle: "Council Data Project", 6 | brandUrl: "https://councildataproject.github.io/", 7 | // brandImage: "https://placehold.it/350x150", 8 | }); 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | stories: ["../src/**/*.stories.@(tsx|mdx)"], 5 | addons: [ 6 | "@storybook/addon-a11y", 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | import customTheme from "./customTheme"; 3 | 4 | addons.setConfig({ 5 | isFullscreen: false, 6 | showNav: true, 7 | showPanel: true, 8 | panelPosition: "bottom", 9 | sidebarAnimations: true, 10 | enableShortcuts: true, 11 | isToolshown: true, 12 | theme: customTheme, 13 | selectedPanel: undefined, 14 | initialActive: "sidebar", 15 | showRoots: true, 16 | }); 17 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { themes } from "@storybook/theming"; 3 | import { StaticRouter } from 'react-router-dom'; 4 | import "@councildataproject/cdp-design/dist/images.css"; 5 | import "@councildataproject/cdp-design/dist/colors.css"; 6 | import "@mozilla-protocol/core/protocol/css/protocol.css"; 7 | import "@mozilla-protocol/core/protocol/css/protocol-components.css"; 8 | import "semantic-ui-css/semantic.min.css"; 9 | 10 | export const decorators = [ 11 | (story) =>
{story()}
12 | ]; 13 | 14 | export const parameters = { 15 | options: { 16 | storySort: { 17 | order: ["Getting Started", "Library"], 18 | }, 19 | isToolshown: true, 20 | }, 21 | docs: { 22 | theme: themes.light, 23 | }, 24 | backgrounds: { 25 | default: "light", 26 | values: [ 27 | { 28 | name: "light", 29 | value: "#f9f9f9", 30 | }, 31 | { 32 | name: "grey", 33 | value: "#929396", 34 | }, 35 | { 36 | name: "dark", 37 | value: "#333", 38 | }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Council Data Project 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | testMatch: ["**/__tests__/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"], 4 | transform: { 5 | "^.+\\.(ts|tsx)$": "ts-jest", 6 | }, 7 | snapshotSerializers: ["enzyme-to-json/serializer"], 8 | setupFilesAfterEnv: ["/src/setupEnzyme.ts"], 9 | moduleNameMapper: { 10 | "\\.(scss|sass|css)$": "identity-obj-proxy", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /localize.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_LANGUAGE = "en"; 2 | const SUPPORTED_LANGUAGES = require(`./lib/constants/ProjectConstants`).SUPPORTED_LANGUAGES.filter((language) => { return language !== DEFAULT_LANGUAGE }); 3 | 4 | const defaultLanguageStrings = require(`./lib/assets/strings/${DEFAULT_LANGUAGE}`).default 5 | 6 | function checkLanguages() { 7 | console.log(`***\n\nBeginning localization-checking script for languages: ${SUPPORTED_LANGUAGES}. \n\nTake a look at any SUSPECT KEYS and make sure they are actually translated.\n\n***`); 8 | 9 | SUPPORTED_LANGUAGES.forEach((languageCode) => { 10 | console.log(` ${languageCode.toUpperCase()} starting...\n`); 11 | const currentLang = require(`./lib/assets/strings/${languageCode}`).default; 12 | let keys = Object.keys(currentLang); 13 | keys.forEach((key) => { 14 | if(currentLang[key] === defaultLanguageStrings[key]) { 15 | // english and this language have the same value for the same key, this is suspect 16 | console.log(` SUSPECT KEY: "${key}"\n`); 17 | } 18 | }) 19 | console.log(` ${languageCode.toUpperCase()} complete\n`); 20 | }); 21 | 22 | console.log(`***\n\nCompleted localization-checking script.\n\n***`); 23 | } 24 | 25 | try { 26 | checkLanguages(); 27 | process.exit(0); 28 | } catch (error) { 29 | console.error(error); 30 | process.exit(1); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/LanguageConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, FC, useContext } from "react"; 2 | 3 | export interface LanguageConfig { 4 | language: string; 5 | setLanguage: React.Dispatch>; 6 | } 7 | 8 | const LanguageConfigContext = createContext({ 9 | language: "en", 10 | setLanguage: () => undefined, 11 | }); 12 | 13 | interface LanguageConfigProviderProps { 14 | languageConfig: LanguageConfig; 15 | children: ReactNode; 16 | } 17 | 18 | export const LanguageConfigProvider: FC = ({ 19 | languageConfig, 20 | children, 21 | }: LanguageConfigProviderProps) => { 22 | return ( 23 | 24 | {children} 25 | 26 | ); 27 | }; 28 | 29 | export const useLanguageConfigContext = () => { 30 | const context = useContext(LanguageConfigContext); 31 | 32 | if (context === undefined) { 33 | throw new Error("useLanguageConfigContext must be used within a LanguageConfigProvider"); 34 | } 35 | 36 | return context; 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export { AppConfigProvider, useAppConfigContext } from "./AppConfigContext"; 2 | export { LanguageConfigProvider, useLanguageConfigContext } from "./LanguageConfigContext"; 3 | export { default as App } from "./App"; 4 | -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/faa871d7e69fb8e95b2f97b3ed74f56f9f609dc5/src/assets/images/dave-hoefler-reduced-1.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/faa871d7e69fb8e95b2f97b3ed74f56f9f609dc5/src/assets/images/dave-hoefler-reduced-2.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/faa871d7e69fb8e95b2f97b3ed74f56f9f609dc5/src/assets/images/dave-hoefler-reduced-3.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/faa871d7e69fb8e95b2f97b3ed74f56f9f609dc5/src/assets/images/dave-hoefler-reduced-4.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/faa871d7e69fb8e95b2f97b3ed74f56f9f609dc5/src/assets/images/dave-hoefler-reduced-5.jpg -------------------------------------------------------------------------------- /src/assets/images/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | -------------------------------------------------------------------------------- /src/assets/strings/index.ts: -------------------------------------------------------------------------------- 1 | export { default as de } from "./de"; 2 | export { default as en } from "./en"; 3 | export { default as es } from "./es"; 4 | -------------------------------------------------------------------------------- /src/components/Cards/Base/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from "./Base"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/LegislationCard/LegislationCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import { MATTER_STATUS_DECISION } from "../../../models/constants"; 5 | 6 | import LegislationCard, { LegislationCardProps } from "./LegislationCard"; 7 | 8 | import { basicPerson } from "../../../stories/model-mocks/person"; 9 | 10 | export default { 11 | component: LegislationCard, 12 | title: "Library/Cards/Legislation", 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | const exampleLegislation = { 18 | name: "Council Budget Action SPD-4-A-1", 19 | excerpt: 20 | "Add $175,000 GF (ongoing) in 2020 to SPD to contract with an Indigenous led organization that can assist the City with its efforts to end the Missing and Murdered Indigenous Women and Girls Crisis, and impose a proviso", 21 | status: MATTER_STATUS_DECISION.ADOPTED, 22 | date: "January 22nd, 2020", 23 | tags: ["barbara bailey boulevard", "ADU", "single-family", "cottage"], 24 | sponsors: Array.from({ length: 5 }).map((_, i) => { 25 | return { 26 | ...basicPerson, 27 | id: `${basicPerson.id}-${i}`, 28 | name: `${basicPerson.name} ${i}`, 29 | }; 30 | }), 31 | }; 32 | 33 | export const adopted = Template.bind({}); 34 | adopted.args = exampleLegislation; 35 | 36 | export const rejected = Template.bind({}); 37 | rejected.args = { 38 | ...exampleLegislation, 39 | status: MATTER_STATUS_DECISION.REJECTED, 40 | }; 41 | 42 | export const inProgress = Template.bind({}); 43 | inProgress.args = { 44 | ...exampleLegislation, 45 | status: MATTER_STATUS_DECISION.IN_PROGRESS, 46 | }; 47 | 48 | export const withSearchQuery = Template.bind({}); 49 | withSearchQuery.args = { 50 | ...exampleLegislation, 51 | excerpt: 52 | "... contract with an Indigenous led organization that can assist the City with its efforts ...", 53 | query: "indigenous", 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/Cards/LegislationCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LegislationCard } from "./LegislationCard"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/MeetingCard/MeetingCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import MeetingCard, { MeetingCardProps } from "./MeetingCard"; 5 | 6 | import { eventWithRealImages } from "../../../stories/model-mocks/event"; 7 | 8 | export default { 9 | component: MeetingCard, 10 | title: "Library/Cards/Event", 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const meeting = Template.bind({}); 16 | meeting.args = { 17 | event: eventWithRealImages, 18 | tags: ["bike", "adu", "accessories", "rental"], 19 | }; 20 | 21 | export const meetingSearchResult = Template.bind({}); 22 | meetingSearchResult.args = { 23 | event: eventWithRealImages, 24 | tags: ["bike", "adu", "accessories", "rental"], 25 | excerpt: 26 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce interdum, lorem eget vestibulum tincidunt, augue eros gravida lectus, ut efficitur neque nisi eu metus.", 27 | gram: "ipsum", 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Cards/MeetingCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MeetingCard } from "./MeetingCard"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/PersonCard/PersonCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import PersonCard, { PersonCardProps } from "./PersonCard"; 5 | import { realPerson } from "../../../stories/model-mocks/person"; 6 | import { realSeat, realSeatNoImage } from "../../../stories/model-mocks/seat"; 7 | 8 | export default { 9 | component: PersonCard, 10 | title: "Library/Cards/Person", 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const withSeatPicture = Template.bind({}); 16 | withSeatPicture.args = { 17 | person: realPerson, 18 | seat: realSeat, 19 | }; 20 | 21 | export const withoutSeatPicture = Template.bind({}); 22 | withoutSeatPicture.args = { 23 | person: realPerson, 24 | seat: realSeatNoImage, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Cards/PersonCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PersonCard } from "./PersonCard"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/constants.ts: -------------------------------------------------------------------------------- 1 | export const CARD_DESC_MAX_LENGTH = 140; 2 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/EventVideo.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import EventVideo, { EventVideoProps, EventVideoRef } from "./EventVideo"; 5 | 6 | export default { 7 | component: EventVideo, 8 | title: "Library/Details/Event Video", 9 | } as Meta; 10 | 11 | const Template: Story = (args) => ; 12 | 13 | export const eventVideo = Template.bind({}); 14 | eventVideo.args = { 15 | uri: "https://video.seattle.gov/media/council/council_113020_2022091V.mp4", 16 | componentRef: createRef(), 17 | sessionIndex: 0, 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/ShareVideo/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ShareVideo } from "./ShareVideo"; 2 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/ShareVideo/state.ts: -------------------------------------------------------------------------------- 1 | import { timePointToSeconds, secondsToTimePointStr } from "./utils"; 2 | 3 | export interface TimePointState { 4 | /** The time point str */ 5 | value: string; 6 | /** Is the input html (to enter a timepoint) disabled? */ 7 | isDisabled: boolean; 8 | /** Does the share link URL contain a timepoint query parameter? */ 9 | isActive: boolean; 10 | /** Is the the modal open? */ 11 | isOpen: boolean; 12 | } 13 | 14 | export enum TimePointActionType { 15 | UPDATE_VALUE, 16 | VALIDATE_VALUE, 17 | OPEN, 18 | CLOSE, 19 | } 20 | 21 | export type TimePointAction = 22 | | { type: TimePointActionType.UPDATE_VALUE; payload: string } 23 | // The payload is for `isActive` 24 | | { type: TimePointActionType.VALIDATE_VALUE; payload?: boolean } 25 | | { type: TimePointActionType.OPEN; payload: number } 26 | | { type: TimePointActionType.CLOSE }; 27 | 28 | export const initialTimePoint = { 29 | value: "", 30 | isDisabled: false, 31 | isActive: false, 32 | isOpen: false, 33 | }; 34 | 35 | export const timePointReducer = (state: TimePointState, action: TimePointAction) => { 36 | switch (action.type) { 37 | case TimePointActionType.UPDATE_VALUE: { 38 | return { ...state, value: action.payload }; 39 | } 40 | case TimePointActionType.VALIDATE_VALUE: { 41 | const newValue = secondsToTimePointStr(timePointToSeconds(state.value)); 42 | if (action.payload !== undefined) { 43 | return { 44 | ...state, 45 | value: newValue, 46 | isDisabled: !action.payload, 47 | isActive: action.payload, 48 | }; 49 | } else { 50 | return { ...state, value: newValue }; 51 | } 52 | } 53 | case TimePointActionType.OPEN: { 54 | const newValue = secondsToTimePointStr(action.payload); 55 | return { ...state, isOpen: true, value: newValue, isDisabled: false, isActive: false }; 56 | } 57 | case TimePointActionType.CLOSE: { 58 | return { ...state, isOpen: false }; 59 | } 60 | default: 61 | return state; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/ShareVideo/utils.ts: -------------------------------------------------------------------------------- 1 | import padNumWithZero from "../../../../utils/padNumWithZero"; 2 | 3 | export const timePointToSeconds = (timePointInputStr: string) => { 4 | const nums = timePointInputStr.trim().split(":").reverse(); 5 | let totalSeconds = 0; 6 | nums.forEach((n, i) => { 7 | const num = parseFloat(n); 8 | if (isNaN(num) || n.includes("-")) { 9 | // num is not a number, or is negative 10 | totalSeconds += NaN; 11 | } else { 12 | totalSeconds += num * Math.pow(60, i); 13 | } 14 | }); 15 | if (totalSeconds) { 16 | totalSeconds = Math.floor(totalSeconds); 17 | } 18 | return totalSeconds; 19 | }; 20 | 21 | export const secondsToTimePointStr = (totalSeconds: number) => { 22 | if (isNaN(totalSeconds)) { 23 | return "NaN"; 24 | } 25 | 26 | const totalSecondsInt = Math.floor(totalSeconds); 27 | 28 | const hours = Math.floor(totalSecondsInt / 3600); 29 | const minutes = Math.floor((totalSecondsInt - hours * 3600) / 60); 30 | const seconds = totalSecondsInt - hours * 3600 - minutes * 60; 31 | 32 | const secondsStr = padNumWithZero(seconds); 33 | 34 | return hours > 0 35 | ? `${hours}:${padNumWithZero(minutes)}:${secondsStr}` 36 | : `${minutes}:${secondsStr}`; 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/constants.ts: -------------------------------------------------------------------------------- 1 | export enum VideoMediaType { 2 | mp4 = "mp4", 3 | webm = "webm", 4 | youtube = "youtube", 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventVideo } from "./EventVideo"; 2 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/utils.ts: -------------------------------------------------------------------------------- 1 | import { VideoMediaType } from "./constants"; 2 | 3 | export function getMediaTypeFromUri(uri: string) { 4 | let type: string | undefined; 5 | if (uri.match(/youtu?.be/)) { 6 | type = VideoMediaType.youtube; 7 | } else { 8 | // uri must have media type webm or mp4 9 | const matches = uri.match(new RegExp(`(${VideoMediaType.webm}|${VideoMediaType.mp4})$`)); 10 | if (matches) { 11 | type = matches[0]; 12 | } 13 | } 14 | return type; 15 | } 16 | 17 | interface Source { 18 | src: string; 19 | type?: string; 20 | } 21 | 22 | export function getSource(uri: string) { 23 | const mediaType = getMediaTypeFromUri(uri); 24 | const source: Source = { src: uri }; 25 | if (mediaType) { 26 | source.type = `video/${mediaType}`; 27 | } 28 | return source; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationHistory.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Story, Meta } from "@storybook/react"; 4 | 5 | import { LegislationHistoryProps, LegislationHistory } from "./LegislationHistory"; 6 | import { 7 | basicFailEventMinutesItem, 8 | basicPassEventMinutesItem, 9 | nonVotingEventMinutesItem, 10 | } from "../../../stories/model-mocks/eventMinutesItem"; 11 | 12 | export default { 13 | component: LegislationHistory, 14 | title: "Library/Details/Legislation/History", 15 | } as Meta; 16 | 17 | const Template: Story = (args) => ; 18 | 19 | export const history = Template.bind({}); 20 | history.args = { 21 | eventMinutesItems: [ 22 | basicFailEventMinutesItem, 23 | nonVotingEventMinutesItem, 24 | basicPassEventMinutesItem, 25 | basicFailEventMinutesItem, 26 | nonVotingEventMinutesItem, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationHistory.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import EventMinutesItem from "../../../models/EventMinutesItem"; 4 | import H2 from "../../Shared/H2"; 5 | import Details from "../../Shared/Details"; 6 | import { LegislativeHistoryNode } from "./LegislativeHistoryNode"; 7 | import { strings } from "../../../assets/LocalizedStrings"; 8 | 9 | import styled from "@emotion/styled"; 10 | 11 | const SpacerContainer = styled.div({ marginTop: 16 }); 12 | const ColumnMakingContainer = styled.div({ display: "flex", flexDirection: "column" }); 13 | 14 | export interface LegislationHistoryProps { 15 | /** the timeline of a matter as represented by an ordered series of EventMinutesItem */ 16 | eventMinutesItems: EventMinutesItem[]; 17 | } 18 | 19 | const LegislationHistory: FC = ({ 20 | eventMinutesItems, 21 | }: LegislationHistoryProps) => { 22 | return ( 23 | 24 |
29 | {strings.history} 30 | 31 | } 32 | hiddenContent={ 33 | 34 | {eventMinutesItems.map((eventMinutesItem, index) => { 35 | return ( 36 | 41 | ); 42 | })} 43 | 44 | } 45 | /> 46 | 47 | ); 48 | }; 49 | 50 | export { LegislationHistory }; 51 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationIntroduction.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Story, Meta } from "@storybook/react"; 4 | 5 | import { 6 | basicIndexMatterGram, 7 | rentBasicMatterGram, 8 | } from "../../../stories/model-mocks/indexedMatterGram"; 9 | import { 10 | adoptedMatterStatus, 11 | rejectedMatterStatus, 12 | inProgressMatterStatus, 13 | longTitleMatterStatus, 14 | } from "../../../stories/model-mocks/matterStatus"; 15 | 16 | import LegislationIntroduction, { LegislationIntroductionProps } from "./LegislationIntroduction"; 17 | 18 | export default { 19 | component: LegislationIntroduction, 20 | title: "Library/Details/Legislation/Introduction", 21 | } as Meta; 22 | 23 | const approvalProps: LegislationIntroductionProps = { 24 | matterStatus: adoptedMatterStatus, 25 | indexedMatterGrams: [basicIndexMatterGram, rentBasicMatterGram], 26 | }; 27 | 28 | const rejectedProps: LegislationIntroductionProps = { 29 | matterStatus: rejectedMatterStatus, 30 | indexedMatterGrams: [basicIndexMatterGram, rentBasicMatterGram], 31 | }; 32 | 33 | const inProgressProps: LegislationIntroductionProps = { 34 | matterStatus: inProgressMatterStatus, 35 | indexedMatterGrams: [basicIndexMatterGram, rentBasicMatterGram], 36 | }; 37 | 38 | const longTitleProps: LegislationIntroductionProps = { 39 | matterStatus: longTitleMatterStatus, 40 | indexedMatterGrams: [basicIndexMatterGram, rentBasicMatterGram], 41 | }; 42 | 43 | const Template: Story = (args) => ( 44 | 45 | ); 46 | 47 | export const approvalStory = Template.bind({}); 48 | approvalStory.args = approvalProps; 49 | 50 | export const rejectedStory = Template.bind({}); 51 | rejectedStory.args = rejectedProps; 52 | 53 | export const inProgressStory = Template.bind({}); 54 | inProgressStory.args = inProgressProps; 55 | 56 | export const longTitleStory = Template.bind({}); 57 | longTitleStory.args = longTitleProps; 58 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationLatestVote.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Story, Meta } from "@storybook/react"; 4 | 5 | import { generatePopulatedVoteList } from "../../../stories/model-mocks/vote"; 6 | import { LegislationLatestVoteProps, LegislationLatestVote } from "./LegislationLatestVote"; 7 | import { VOTE_DECISION } from "../../../models/constants"; 8 | 9 | export default { 10 | component: LegislationLatestVote, 11 | title: "Library/Details/Legislation/LatestVote", 12 | } as Meta; 13 | 14 | const Template: Story = (args) => ; 15 | 16 | export const adopted = Template.bind({}); 17 | adopted.args = { 18 | votes: generatePopulatedVoteList(VOTE_DECISION.APPROVE), 19 | }; 20 | 21 | export const rejected = Template.bind({}); 22 | rejected.args = { 23 | votes: generatePopulatedVoteList(VOTE_DECISION.REJECT), 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationLatestVote.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import Vote from "../../../models/Vote"; 4 | import H2 from "../../Shared/H2"; 5 | import Details from "../../Shared/Details"; 6 | import { strings } from "../../../assets/LocalizedStrings"; 7 | import { VoteDistributionGraphic } from "./VoteDistributionGraphic"; 8 | export interface LegislationLatestVoteProps { 9 | /** votes on the matter */ 10 | votes: Vote[]; 11 | } 12 | 13 | const LegislationLatestVote: FC = ({ 14 | votes, 15 | }: LegislationLatestVoteProps) => { 16 | return ( 17 |
18 |
23 | {strings.latest_vote} 24 | 25 | } 26 | hiddenContent={} 27 | /> 28 |
29 | ); 30 | }; 31 | 32 | export { LegislationLatestVote }; 33 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/LegislationOverview.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Story, Meta } from "@storybook/react"; 4 | 5 | import LegislationOverview, { LegislationOverviewProps } from "./LegislationOverview"; 6 | import { basicEvent } from "../../../stories/model-mocks/event"; 7 | import { 8 | adoptedMatterStatus, 9 | rejectedMatterStatus, 10 | inProgressMatterStatus, 11 | } from "../../../stories/model-mocks/matterStatus"; 12 | 13 | import { basicPerson } from "../../../stories/model-mocks/person"; 14 | 15 | const basicDocument = { 16 | name: "Legislation Summary", 17 | url: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", 18 | }; 19 | 20 | export default { 21 | component: LegislationOverview, 22 | title: "Library/Details/Legislation/Overview", 23 | } as Meta; 24 | 25 | const Template: Story = (args) => ; 26 | 27 | export const adopted = Template.bind({}); 28 | adopted.args = { 29 | matterStatus: adoptedMatterStatus, 30 | event: basicEvent, 31 | sponsors: [ 32 | basicPerson, 33 | { ...basicPerson, id: `${basicPerson.id}-2`, name: `${basicPerson.name} 2` }, 34 | ], 35 | document: basicDocument, 36 | }; 37 | 38 | export const rejected = Template.bind({}); 39 | rejected.args = { 40 | matterStatus: rejectedMatterStatus, 41 | event: basicEvent, 42 | sponsors: [basicPerson], 43 | document: basicDocument, 44 | }; 45 | 46 | export const inProgress = Template.bind({}); 47 | inProgress.args = { 48 | matterStatus: inProgressMatterStatus, 49 | event: basicEvent, 50 | sponsors: [basicPerson], 51 | document: basicDocument, 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/Details/Legislation/VoteDistributionGraphic.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from "react"; 2 | 3 | import Vote from "../../../models/Vote"; 4 | import VoteBar from "./VoteBar"; 5 | import { getVoteDistribution } from "../../Shared/util/voteDistribution"; 6 | import { strings } from "../../../assets/LocalizedStrings"; 7 | 8 | const VOTE_BAR_HEIGHT = 16; 9 | export interface VoteDistributionGraphicProps { 10 | /** votes on the matter */ 11 | votes: Vote[]; 12 | } 13 | 14 | const VoteDistributionGraphic: FC = ({ 15 | votes, 16 | }: VoteDistributionGraphicProps) => { 17 | const { inFavor, against, abstained } = getVoteDistribution(votes); 18 | const votesForLabel: string = strings.number_approved.replace("{number}", `${inFavor.length}`); 19 | const votesAgainstLabel: string = strings.number_rejected.replace( 20 | "{number}", 21 | `${against.length}` 22 | ); 23 | const votesAbstainedLabel: string = strings.number_non_voting.replace( 24 | "{number}", 25 | `${abstained.length}` 26 | ); 27 | 28 | const voteBars: ReactNode[] = [ 29 | , 37 | , 45 | , 53 | ]; 54 | 55 | return
{voteBars}
; 56 | }; 57 | 58 | export { VoteDistributionGraphic }; 59 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/DocumentsList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | import styled from "@emotion/styled"; 3 | 4 | import { Document } from "./types"; 5 | import MinusIcon from "../../Shared/MinusIcon"; 6 | import PlusIcon from "../../Shared/PlusIcon"; 7 | 8 | import { strings } from "../../../assets/LocalizedStrings"; 9 | import colors from "../../../styles/colors"; 10 | 11 | const Summary = styled.summary({ 12 | color: colors.dark_blue, 13 | display: "flex", 14 | alignItems: "center", 15 | "&::before, &::marker": { 16 | // remove mozilla protocol +/- icon 17 | // remove triangle marker 18 | content: "none", 19 | }, 20 | "& svg": { 21 | marginLeft: 4, 22 | // limit the width and height of +/- icon 23 | width: "1rem", 24 | height: "1rem", 25 | }, 26 | }); 27 | 28 | interface DocumentsListProps { 29 | documents?: Document[]; 30 | } 31 | 32 | const DocumentsList: FC = ({ documents }: DocumentsListProps) => { 33 | const [isExpanded, setIsExpanded] = useState(false); 34 | const onToggleIsExpanded = () => setIsExpanded((prev) => !prev); 35 | 36 | if (!documents || documents.length === 0) { 37 | return null; 38 | } 39 | return ( 40 |
41 | 42 | {strings.see_documents} 43 | {isExpanded ? : } 44 | 45 | 56 |
57 | ); 58 | }; 59 | 60 | export default DocumentsList; 61 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/MinutesItemsList.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import MinutesItemsList, { MinutesItemsListProps } from "./MinutesItemsList"; 5 | 6 | export default { 7 | component: MinutesItemsList, 8 | title: "Library/Details/Minutes Items List", 9 | } as Meta; 10 | 11 | const Template: Story = (args) => ; 12 | 13 | export const minutesItemsList = Template.bind({}); 14 | minutesItemsList.args = { 15 | minutesItems: [ 16 | { 17 | name: "Approval of the minutes", 18 | }, 19 | { 20 | name: "Council briefing minutes (2019)", 21 | documents: [ 22 | { 23 | url: "http://google.com", 24 | label: "Meeting transcript", 25 | }, 26 | { 27 | url: "http://google.com", 28 | label: "2nd Meeting transcript", 29 | }, 30 | ], 31 | }, 32 | { 33 | name: "President's report", 34 | }, 35 | { 36 | name: "Preview of today's city council actions, council and regional committees", 37 | }, 38 | { 39 | name: "Inf 1579", 40 | description: "Inf 1579 description", 41 | documents: [ 42 | { 43 | url: "http://google.com", 44 | label: "A document", 45 | }, 46 | ], 47 | }, 48 | { 49 | name: "Executive session on Pending, Potential, or Actual Litigation", 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/MinutesItemsList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import styled from "@emotion/styled"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import DocumentsList from "./DocumentsList"; 6 | import ChevronDownIcon from "../../Shared/ChevronDownIcon"; 7 | 8 | import { Item } from "./types"; 9 | import { strings } from "../../../assets/LocalizedStrings"; 10 | 11 | const ListItem = styled.li({ 12 | "& > div:first-of-type": { 13 | // bold the minutes item's name 14 | fontWeight: 600, 15 | }, 16 | "& > a": { 17 | display: "flex", 18 | alignItems: "center", 19 | }, 20 | "& > a > svg": { 21 | width: "1rem", 22 | height: "1rem", 23 | transform: "rotate(-90deg)", 24 | }, 25 | }); 26 | 27 | export interface MinutesItemsListProps { 28 | /** 29 | * List of minutes items headlines, each of which may have a list of associated documents 30 | */ 31 | minutesItems: Item[]; 32 | } 33 | 34 | const MinutesItemsList: FC = ({ minutesItems }: MinutesItemsListProps) => { 35 | return ( 36 |
    37 | {minutesItems.map((elem) => { 38 | return ( 39 | 40 |
    {elem.name}
    41 | {elem.matter_ref && ( 42 | 43 | {strings.go_to_matter_details} 44 | 45 | )} 46 | {elem.description &&
    {elem.description}
    } 47 | 48 |
    49 | ); 50 | })} 51 |
52 | ); 53 | }; 54 | 55 | export default MinutesItemsList; 56 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MinutesItemsList } from "./MinutesItemsList"; 2 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/types.ts: -------------------------------------------------------------------------------- 1 | import MinutesItem from "../../../models/MinutesItem"; 2 | 3 | export interface Document { 4 | /*Document item label */ 5 | label: string; 6 | /*Document item url */ 7 | url: string; 8 | } 9 | 10 | export interface Item extends Pick { 11 | /*Array of attachments for a given minutes item*/ 12 | documents?: Document[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptFull/TranscriptFull.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from "react"; 2 | 3 | import { action } from "@storybook/addon-actions"; 4 | import { Story, Meta } from "@storybook/react"; 5 | 6 | import TranscriptFull, { TranscriptFullProps } from "./TranscriptFull"; 7 | import { TranscriptItemRef } from "../TranscriptItem/TranscriptItem"; 8 | 9 | import { mockSentences } from "../../../stories/model-mocks/sentence"; 10 | 11 | export default { 12 | component: TranscriptFull, 13 | title: "Library/Details/Transcript Full", 14 | } as Meta; 15 | 16 | const Template: Story = (args) => ; 17 | 18 | const exampleSentences = mockSentences(2, 10); 19 | 20 | export const transcriptFull = Template.bind({}); 21 | transcriptFull.args = { 22 | sentences: exampleSentences, 23 | transcriptItemsRefs: exampleSentences.map(() => createRef()), 24 | jumpToVideoClip: (_, startTime) => action(`jump to video clip at ${startTime}`), 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptFull/TranscriptFull.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, RefObject } from "react"; 2 | 3 | import styled from "@emotion/styled"; 4 | 5 | import TranscriptItem, { TranscriptItemRef } from "../TranscriptItem/TranscriptItem"; 6 | 7 | import { ECSentence } from "../../../containers/EventContainer/types"; 8 | import secondsToHHMMSS from "../../../utils/secondsToHHMMSS"; 9 | 10 | const TranscripItems = styled.div({ 11 | maxHeight: "100vh", 12 | overflowY: "auto", 13 | }); 14 | 15 | export interface TranscriptFullProps { 16 | /**The sentences of the transcript */ 17 | sentences: ECSentence[]; 18 | /**List of transcript item React references */ 19 | transcriptItemsRefs: RefObject[]; 20 | /**Callback to play video clip */ 21 | jumpToVideoClip(sessionIndex: number, startTime: number): void; 22 | } 23 | 24 | /**Full view of transcript */ 25 | const TranscriptFull: FC = ({ 26 | sentences, 27 | transcriptItemsRefs, 28 | jumpToVideoClip, 29 | }: TranscriptFullProps) => { 30 | /**Creates a function that handles jumping to video clip at startTime */ 31 | const handleJumpToVideoClip = (sessionIndex: number, startTime: number) => () => 32 | jumpToVideoClip(sessionIndex, startTime); 33 | return ( 34 | 35 | {sentences.map((sentence, i) => ( 36 | 48 | ))} 49 | 50 | ); 51 | }; 52 | 53 | export default TranscriptFull; 54 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptFull/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TranscriptFull } from "./TranscriptFull"; 2 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptItem/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as TranscriptItem } from "./TranscriptItem"; 2 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptSearch/TranscriptSearch.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import React from "react"; 3 | 4 | import { action } from "@storybook/addon-actions"; 5 | import { Story, Meta } from "@storybook/react"; 6 | 7 | import { initialFetchDataState } from "../../../containers/FetchDataContainer/useFetchData"; 8 | 9 | import TranscriptSearch, { TranscriptSearchProps } from "./TranscriptSearch"; 10 | 11 | import { mockSentences } from "../../../stories/model-mocks/sentence"; 12 | 13 | export default { 14 | component: TranscriptSearch, 15 | title: "Library/Details/Transcript Search", 16 | decorators: [ 17 | (Story) => ( 18 |
19 | 20 |
21 | ), 22 | ], 23 | } as Meta; 24 | 25 | const Template: Story = (args) => ; 26 | 27 | export const Default = Template.bind({}); 28 | Default.args = { 29 | searchQuery: "sentence", 30 | sentences: { 31 | ...initialFetchDataState, 32 | data: mockSentences(1, 2), 33 | }, 34 | jumpToVideoClip: (sessionIndex, startTime) => action("jump to video clip"), 35 | jumpToTranscript: (sentenceIndex) => action("jump to transcript"), 36 | }; 37 | 38 | export const manySentences = Template.bind({}); 39 | manySentences.args = { 40 | searchQuery: "sentence", 41 | sentences: { 42 | ...initialFetchDataState, 43 | data: mockSentences(2, 100), 44 | }, 45 | jumpToVideoClip: (sessionIndex, startTime) => action("jump to video clip"), 46 | jumpToTranscript: (sentenceIndex) => action("jump to transcript"), 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptSearch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TranscriptSearch } from "./TranscriptSearch"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/EventsFilter/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventsFilter } from "./EventsFilter"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/FilterPopup/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FilterPopup } from "./FilterPopup"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/FiltersContainer/FiltersContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | import { screenWidths } from "../../../styles/mediaBreakpoints"; 4 | 5 | const FiltersContainer = styled.div({ 6 | display: "flex", 7 | flexDirection: "column", 8 | gap: 4, 9 | [`@media (min-width:${screenWidths.tablet})`]: { 10 | flexDirection: "row", 11 | flexWrap: "wrap", 12 | "& > div:last-of-type:not(:first-of-type), & > button:last-of-type": { 13 | marginLeft: "auto", 14 | }, 15 | }, 16 | }); 17 | 18 | export default FiltersContainer; 19 | -------------------------------------------------------------------------------- /src/components/Filters/FiltersContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FiltersContainer } from "./FiltersContainer"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/SelectDateRange.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { action } from "@storybook/addon-actions"; 4 | import { Story, Meta } from "@storybook/react"; 5 | 6 | import SelectDateRange, { SelectDateRangeProps } from "./SelectDateRange"; 7 | 8 | export default { 9 | component: SelectDateRange, 10 | title: "Library/Filters/Select Date Range", 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const selectDateRange = Template.bind({}); 16 | selectDateRange.args = { 17 | state: { 18 | start: "", 19 | end: "", 20 | }, 21 | update: action("update-date-range-state"), 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/SelectDateRange.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { shallow, ShallowWrapper } from "enzyme"; 4 | 5 | import SelectDateRange, { SelectDateRangeProps } from "./SelectDateRange"; 6 | 7 | describe("SelectDateRange", () => { 8 | let selectDateRange: ShallowWrapper; 9 | const updateMock = jest.fn(); 10 | 11 | beforeEach(() => { 12 | selectDateRange = shallow( 13 | 14 | ); 15 | }); 16 | 17 | afterEach(() => { 18 | jest.resetAllMocks(); 19 | }); 20 | 21 | test("Renders 2 Form Fields", () => { 22 | expect(selectDateRange.find(".mzp-c-field-control")).toHaveLength(2); 23 | }); 24 | 25 | test("Calls update date range state callback with correct args for start date field", () => { 26 | selectDateRange 27 | .find(".mzp-c-field-control[name='start']") 28 | .simulate("change", { currentTarget: { name: "start", value: "01/01/2020" } }); 29 | expect(updateMock).toHaveBeenCalledWith("start", "01/01/2020"); 30 | }); 31 | 32 | test("Calls update date range state callback with correct args for end date field", () => { 33 | selectDateRange 34 | .find(".mzp-c-field-control[name='end']") 35 | .simulate("change", { currentTarget: { name: "end", value: "01/01/2020" } }); 36 | expect(updateMock).toHaveBeenCalledWith("end", "01/01/2020"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/SelectDateRange.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FunctionComponent } from "react"; 2 | import { FilterState } from "../reducer"; 3 | import Form from "../Shared/Form"; 4 | 5 | export interface SelectDateRangeProps { 6 | /**The date range state. */ 7 | state: FilterState; 8 | /**Callback to update the date range state. */ 9 | update(keyName: string, dataValue: string): void; 10 | } 11 | 12 | /**Two input fields to select start and end dates. */ 13 | const SelectDateRange: FunctionComponent = ({ 14 | state, 15 | update, 16 | }: SelectDateRangeProps) => { 17 | const onChange = (e: ChangeEvent) => { 18 | const dateInputName = e.currentTarget.name; 19 | const dateInputValue = e.currentTarget.value; 20 | update(dateInputName, dateInputValue); 21 | }; 22 | 23 | return ( 24 |
25 |
26 | 29 | 37 |
38 |
39 | 42 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default SelectDateRange; 56 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/getDateText.test.ts: -------------------------------------------------------------------------------- 1 | import getDateTextFunctionCreator from "./getDateText"; 2 | 3 | describe("getDateText", () => { 4 | const defaultText = "Date"; 5 | const getDateText = getDateTextFunctionCreator("en-US", "America/Los_Angeles"); 6 | 7 | test("Returns defaultText", () => { 8 | const textRep = getDateText({ start: "", end: "" }, defaultText); 9 | expect(textRep).toEqual(defaultText); 10 | }); 11 | 12 | test("Returns start date", () => { 13 | const textRep = getDateText({ start: "2020/01/01", end: "" }, defaultText); 14 | expect(textRep).toEqual("Jan 1, 2020 -"); 15 | }); 16 | 17 | test("Returns end date", () => { 18 | const textRep = getDateText({ start: "", end: "2020/01/01" }, defaultText); 19 | expect(textRep).toEqual("- Jan 1, 2020"); 20 | }); 21 | 22 | test("Returns start and end date with same year and month", () => { 23 | const textRep = getDateText({ start: "2020/01/01", end: "2020/01/02" }, defaultText); 24 | expect(textRep).toEqual("Jan 1 - Jan 2, 2020"); 25 | }); 26 | 27 | test("Returns start and end date with same year only", () => { 28 | const textRep = getDateText({ start: "2020/01/01", end: "2020/02/01" }, defaultText); 29 | expect(textRep).toEqual("Jan 1 - Feb 1, 2020"); 30 | }); 31 | 32 | test("Returns start and end date with different year", () => { 33 | const textRep = getDateText({ start: "2020/01/01", end: "2021/01/01" }, defaultText); 34 | expect(textRep).toEqual("Jan 1, 2020 - Jan 1, 2021"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/getDateText.ts: -------------------------------------------------------------------------------- 1 | import { FilterState } from "../reducer"; 2 | 3 | import getTimeZoneDate from "../../../utils/getTimeZoneName"; 4 | 5 | const getDateText = 6 | (language: string, timeZone: string) => 7 | (dateRange: FilterState, defaultText: string): string => { 8 | const timeZoneStartDate = getTimeZoneDate(new Date(dateRange.start), timeZone); 9 | const timeZoneEndDate = getTimeZoneDate(new Date(dateRange.end), timeZone); 10 | const startString = timeZoneStartDate?.toLocaleDateString(language, { 11 | timeZone, 12 | month: "short", 13 | day: "numeric", 14 | year: 15 | timeZoneStartDate.getUTCFullYear() === timeZoneEndDate?.getUTCFullYear() 16 | ? undefined 17 | : "numeric", 18 | }); 19 | const endString = timeZoneEndDate?.toLocaleDateString(language, { 20 | timeZone, 21 | month: "short", 22 | day: "numeric", 23 | year: "numeric", 24 | }); 25 | let textRep; 26 | if (timeZoneStartDate && timeZoneEndDate) { 27 | textRep = `${startString} - ${endString}`; 28 | } else if (timeZoneStartDate) { 29 | textRep = `${startString} -`; 30 | } else if (timeZoneEndDate) { 31 | textRep = `- ${endString}`; 32 | } else { 33 | textRep = defaultText; 34 | } 35 | return textRep; 36 | }; 37 | 38 | export default getDateText; 39 | -------------------------------------------------------------------------------- /src/components/Filters/SelectDateRange/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getDateText } from "./getDateText"; 2 | export { default as SelectDateRange } from "./SelectDateRange"; 3 | -------------------------------------------------------------------------------- /src/components/Filters/SelectSorting/SelectSorting.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { action } from "@storybook/addon-actions"; 4 | import { Story, Meta } from "@storybook/react"; 5 | 6 | import { ORDER_DIRECTION } from "../../../networking/constants"; 7 | 8 | import SelectSorting, { SelectSortingProps } from "./SelectSorting"; 9 | 10 | export default { 11 | component: SelectSorting, 12 | title: "Library/Filters/Select Sorting", 13 | } as Meta; 14 | 15 | const Template: Story = (args) => ; 16 | 17 | export const selectSorting = Template.bind({}); 18 | selectSorting.args = { 19 | state: { 20 | by: "value", 21 | order: ORDER_DIRECTION.desc, 22 | label: "Most relevant", 23 | }, 24 | update: action("update-sorting-state"), 25 | sortOptions: [ 26 | { by: "value", order: ORDER_DIRECTION.desc, label: "Most relevant" }, 27 | { by: "date", order: ORDER_DIRECTION.desc, label: "Newest first" }, 28 | { by: "date", order: ORDER_DIRECTION.asc, label: "Oldest first" }, 29 | ], 30 | onPopupClose: action("on-popup-close"), 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Filters/SelectSorting/getSortingText.test.ts: -------------------------------------------------------------------------------- 1 | import { ORDER_DIRECTION } from "../../../networking/constants"; 2 | 3 | import getSortingText from "./getSortingText"; 4 | 5 | describe("getSortingText", () => { 6 | const defaultText = "Sort By"; 7 | test("Returns defaultText", () => { 8 | const textRep = getSortingText({ by: "", order: "", label: "" }, defaultText); 9 | expect(textRep).toEqual(defaultText); 10 | }); 11 | 12 | test("Returns correct label given sort state", () => { 13 | const sortState = { by: "value", order: ORDER_DIRECTION.desc, label: "Most relevant" }; 14 | const textRep = getSortingText(sortState, defaultText); 15 | expect(textRep).toEqual(sortState.label); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/Filters/SelectSorting/getSortingText.ts: -------------------------------------------------------------------------------- 1 | import { FilterState } from "../reducer"; 2 | import { ORDER_DIRECTION } from "../../../networking/constants"; 3 | 4 | export interface SortOption { 5 | /**The field to sort by */ 6 | by: string; 7 | /**The order to sort by */ 8 | order: ORDER_DIRECTION; 9 | /**The label of the sort option */ 10 | label: string; 11 | } 12 | 13 | /** 14 | * Generate the text representation of a sort object. 15 | * @param {Object} sort The sort object. 16 | * @param {string} sort.by 17 | * @param {string} sort.order 18 | * @param {string} sort.label 19 | * @param {string} defaultText The default text representation, 20 | * when no sort options are selected. 21 | * @return {string} The text representation. 22 | */ 23 | const getSortingText = (sort: FilterState, defaultText: string): string => { 24 | let textRep = defaultText; 25 | if (sort.by && sort.order && sort.label) { 26 | textRep = sort.label; 27 | } 28 | return textRep; 29 | }; 30 | 31 | export default getSortingText; 32 | -------------------------------------------------------------------------------- /src/components/Filters/SelectSorting/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SelectSorting } from "./SelectSorting"; 2 | export { default as getSortingText } from "./getSortingText"; 3 | -------------------------------------------------------------------------------- /src/components/Filters/SelectTextFilterOptions/getCheckboxText.test.ts: -------------------------------------------------------------------------------- 1 | import getCheckboxText from "./getCheckboxText"; 2 | 3 | describe("getCheckboxText", () => { 4 | const defaultText = "default"; 5 | 6 | test("Returns defaultText", () => { 7 | const textRep = getCheckboxText({ a: false, b: false }, defaultText); 8 | expect(textRep).toEqual(defaultText); 9 | }); 10 | 11 | test("Returns number of selected options and defaultText", () => { 12 | const textRep = getCheckboxText({ a: true, b: false, c: true }, defaultText); 13 | expect(textRep).toEqual(`${defaultText} : 2`); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/Filters/SelectTextFilterOptions/getCheckboxText.ts: -------------------------------------------------------------------------------- 1 | import { FilterState } from "../reducer"; 2 | 3 | /** 4 | * Generate the text representation of a list of checkboxes, by appending the number of selected checkboxes 5 | * to the defaultText. 6 | * @param {Object} checkboxes The object representation of the list of checkboxes, 7 | * where the keys are the different options, and each value is a boolean(whether the option is selected). 8 | * @param {string} defaultText The default text representation, when no checkboxes are selected. 9 | * @returns {string} The text representation. 10 | */ 11 | const getCheckboxText = (checkboxes: FilterState, defaultText: string): string => { 12 | const numberOfSelectedCheckbox = Object.values(checkboxes).filter( 13 | (dataValue) => dataValue 14 | ).length; 15 | const textRep = numberOfSelectedCheckbox 16 | ? `${defaultText} : ${numberOfSelectedCheckbox}` 17 | : defaultText; 18 | return textRep; 19 | }; 20 | 21 | export default getCheckboxText; 22 | -------------------------------------------------------------------------------- /src/components/Filters/SelectTextFilterOptions/getSelectedOptions.test.ts: -------------------------------------------------------------------------------- 1 | import getSelectedOptions from "./getSelectedOptions"; 2 | 3 | describe("getSelectedOptions", () => { 4 | test("Returns no selections with default state", () => { 5 | const selectedOptions = getSelectedOptions({}); 6 | expect(selectedOptions).toHaveLength(0); 7 | }); 8 | 9 | test("Returns correct selections", () => { 10 | const selectedOptions = getSelectedOptions({ a: true, b: false }); 11 | expect(selectedOptions).toHaveLength(1); 12 | expect(selectedOptions).toContainEqual("a"); 13 | }); 14 | 15 | test("Return no selections with all false state", () => { 16 | const selectedOptions = getSelectedOptions({ a: false, b: false }); 17 | expect(selectedOptions).toHaveLength(0); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/Filters/SelectTextFilterOptions/getSelectedOptions.ts: -------------------------------------------------------------------------------- 1 | import { FilterState } from "../reducer"; 2 | 3 | /** 4 | * @param {Object} checkboxes The object representation of a list of checkboxes, 5 | * where the keys are the different options, and each value is a boolean(whether the option is selected). 6 | * @return {string[]} The list of selected options. 7 | */ 8 | const getSelectedOptions = (checkboxes: FilterState): string[] => { 9 | return Object.keys(checkboxes).filter((k) => checkboxes[k] === true); 10 | }; 11 | 12 | export default getSelectedOptions; 13 | -------------------------------------------------------------------------------- /src/components/Filters/SelectTextFilterOptions/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getCheckboxText } from "./getCheckboxText"; 2 | export { default as getSelectedOptions } from "./getSelectedOptions"; 3 | export { default as SelectTextFilterOptions } from "./SelectTextFilterOptions"; 4 | -------------------------------------------------------------------------------- /src/components/Filters/Shared/Form.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export default styled.form({ 4 | marginBottom: 0, 5 | "& .mzp-c-field-set": { 6 | padding: 0, 7 | }, 8 | "& .mzp-c-choices": { 9 | paddingBottom: 0, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/Filters/actions.ts: -------------------------------------------------------------------------------- 1 | export const FILTER_CLEAR = "FILTER_CLEAR"; 2 | export const FILTER_UPDATE = "FILTER_UPDATE"; 3 | -------------------------------------------------------------------------------- /src/components/Filters/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { FILTER_CLEAR, FILTER_UPDATE } from "./actions"; 2 | import createFilterReducer from "./reducer"; 3 | 4 | describe("filterReducer", () => { 5 | const filterReducer = createFilterReducer(); 6 | 7 | test("Return current state", () => { 8 | const currentState = { a: true }; 9 | const action = { type: "unknown", payload: {} }; 10 | expect(filterReducer(currentState, action)).toEqual(currentState); 11 | }); 12 | 13 | test("Updates state's keyName with correct dataValue", () => { 14 | const currentState = { a: true }; 15 | const nextState = { a: false }; 16 | const action = { type: FILTER_UPDATE, payload: { keyName: "a", dataValue: false } }; 17 | expect(filterReducer(currentState, action)).toEqual(nextState); 18 | }); 19 | 20 | test("Resets state's dataValues with default dataValue", () => { 21 | const currentState = { a: true, b: false }; 22 | const nextState = { a: false, b: false }; 23 | const action = { type: FILTER_CLEAR, payload: { dataValue: false } }; 24 | expect(filterReducer(currentState, action)).toEqual(nextState); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/Filters/reducer.ts: -------------------------------------------------------------------------------- 1 | import { FILTER_CLEAR, FILTER_UPDATE } from "./actions"; 2 | 3 | /**The filter's state type. */ 4 | export interface FilterState { 5 | [key: string]: T; 6 | } 7 | /**The action type to update filter's state */ 8 | export interface FilterAction { 9 | type: string; 10 | payload: { 11 | keyName?: string; 12 | dataValue?: T; 13 | }; 14 | } 15 | 16 | /**Create a filterReducer to manage filter's state.*/ 17 | const createFilterReducer = 18 | () => 19 | (state: FilterState, action: FilterAction): FilterState => { 20 | switch (action.type) { 21 | case FILTER_UPDATE: 22 | return { ...state, [action.payload.keyName as string]: action.payload.dataValue as T }; 23 | case FILTER_CLEAR: 24 | const newState = { ...state }; 25 | Object.keys(newState).forEach( 26 | (keyName) => (newState[keyName] = action.payload.dataValue as T) 27 | ); 28 | return newState; 29 | default: 30 | return state; 31 | } 32 | }; 33 | 34 | export default createFilterReducer; 35 | -------------------------------------------------------------------------------- /src/components/Layout/Footer/Footer.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import Footer, { FooterProps } from "./Footer"; 5 | 6 | export default { 7 | component: Footer, 8 | title: "Library/Layout/Footer", 9 | } as Meta; 10 | 11 | const Template: Story = (args) =>