├── .eslintignore ├── src ├── assets │ ├── images │ │ ├── index.d.ts │ │ ├── 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 │ └── strings │ │ └── index.ts ├── components │ ├── Cards │ │ ├── constants.ts │ │ ├── Base │ │ │ └── index.ts │ │ ├── PersonCard │ │ │ ├── index.ts │ │ │ └── PersonCard.stories.tsx │ │ ├── MeetingCard │ │ │ ├── index.ts │ │ │ └── MeetingCard.stories.tsx │ │ └── LegislationCard │ │ │ ├── index.ts │ │ │ └── LegislationCard.stories.tsx │ ├── Layout │ │ ├── Footer │ │ │ ├── index.ts │ │ │ └── Footer.stories.tsx │ │ ├── Header │ │ │ ├── index.ts │ │ │ └── Header.stories.tsx │ │ ├── TabbedContainer │ │ │ ├── index.ts │ │ │ ├── TabbedContainer.stories.tsx │ │ │ ├── Tab.tsx │ │ │ └── TabbedContainer.tsx │ │ ├── LocalizationWidget │ │ │ ├── index.tsx │ │ │ ├── LocalizationWidget.stories.tsx │ │ │ └── LocalizationWidget.tsx │ │ └── HomeSearchBar │ │ │ ├── index.tsx │ │ │ └── HomeSearchBar.stories.tsx │ ├── Tables │ │ ├── EmptyRow │ │ │ ├── index.ts │ │ │ └── EmptyRow.tsx │ │ ├── VotingTable │ │ │ ├── index.ts │ │ │ └── VotingTable.stories.tsx │ │ ├── ReactiveTable │ │ │ ├── index.ts │ │ │ └── ReactiveTable.tsx │ │ ├── VotingTableRow │ │ │ ├── index.ts │ │ │ └── VotingTableRow.stories.tsx │ │ ├── ReactiveTableRow │ │ │ └── index.ts │ │ ├── MeetingVotesTable │ │ │ ├── index.ts │ │ │ └── MeetingVotesTable.tsx │ │ ├── ReactiveTableHeader │ │ │ ├── index.ts │ │ │ └── ReactiveTableHeader.tsx │ │ └── MeetingVotesTableRow │ │ │ └── index.ts │ ├── Shared │ │ ├── index.ts │ │ ├── Ul.tsx │ │ ├── Types │ │ │ ├── Matter.tsx │ │ │ ├── IndividualMeetingVote.tsx │ │ │ └── MeetingVote.tsx │ │ ├── Dot.tsx │ │ ├── PageContainer.tsx │ │ ├── FetchCardsStatus.tsx │ │ ├── H2.tsx │ │ ├── MinusIcon.tsx │ │ ├── ChevronDownIcon.tsx │ │ ├── AbsoluteBox.tsx │ │ ├── PlusIcon.tsx │ │ ├── DefaultAvatar.tsx │ │ ├── AdoptedIcon.tsx │ │ ├── ShareIcon.tsx │ │ ├── RejectedIcon.tsx │ │ ├── InProgressIcon.tsx │ │ ├── ShowMoreCards.tsx │ │ ├── DocumentTextIcon.tsx │ │ ├── SearchPageTitle.tsx │ │ ├── PlayIcon.tsx │ │ ├── AbstainIcon.tsx │ │ ├── ExpandIcon.tsx │ │ ├── CollapseIcon.tsx │ │ ├── CopyIcon.tsx │ │ ├── SearchBar.tsx │ │ ├── Details.tsx │ │ ├── util │ │ │ └── voteDistribution.ts │ │ ├── ResponsiveTab.tsx │ │ └── PlaceHolder.tsx │ ├── Details │ │ ├── EventVideo │ │ │ ├── index.ts │ │ │ ├── ShareVideo │ │ │ │ ├── index.ts │ │ │ │ ├── utils.ts │ │ │ │ └── state.ts │ │ │ ├── constants.ts │ │ │ ├── EventVideo.stories.tsx │ │ │ └── utils.ts │ │ ├── TranscriptFull │ │ │ ├── index.ts │ │ │ ├── TranscriptFull.stories.tsx │ │ │ └── TranscriptFull.tsx │ │ ├── TranscriptItem │ │ │ └── index.tsx │ │ ├── MinutesItemsList │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── MinutesItemsList.stories.tsx │ │ │ ├── MinutesItemsList.tsx │ │ │ └── DocumentsList.tsx │ │ ├── TranscriptSearch │ │ │ ├── index.ts │ │ │ └── TranscriptSearch.stories.tsx │ │ └── Legislation │ │ │ ├── LegislationLatestVote.stories.tsx │ │ │ ├── LegislationHistory.stories.tsx │ │ │ ├── LegislationLatestVote.tsx │ │ │ ├── LegislationOverview.stories.tsx │ │ │ ├── LegislationHistory.tsx │ │ │ ├── LegislationIntroduction.stories.tsx │ │ │ └── VoteDistributionGraphic.tsx │ └── Filters │ │ ├── FilterPopup │ │ └── index.ts │ │ ├── EventsFilter │ │ └── index.ts │ │ ├── FiltersContainer │ │ ├── index.ts │ │ └── FiltersContainer.tsx │ │ ├── actions.ts │ │ ├── SelectDateRange │ │ ├── index.ts │ │ ├── SelectDateRange.stories.tsx │ │ ├── getDateText.ts │ │ ├── SelectDateRange.test.tsx │ │ ├── getDateText.test.ts │ │ └── SelectDateRange.tsx │ │ ├── SelectSorting │ │ ├── index.ts │ │ ├── getSortingText.test.ts │ │ ├── getSortingText.ts │ │ └── SelectSorting.stories.tsx │ │ ├── SelectTextFilterOptions │ │ ├── index.ts │ │ ├── getSelectedOptions.ts │ │ ├── getCheckboxText.test.ts │ │ ├── getSelectedOptions.test.ts │ │ └── getCheckboxText.ts │ │ ├── Shared │ │ └── Form.tsx │ │ ├── reducer.ts │ │ └── reducer.test.ts ├── pages │ ├── HomePage │ │ ├── index.ts │ │ └── HomePage.tsx │ ├── ErrorPage │ │ ├── index.ts │ │ └── ErrorPage.tsx │ ├── EventPage │ │ ├── index.ts │ │ └── utils.ts │ ├── EventsPage │ │ ├── index.ts │ │ └── EventsPage.tsx │ ├── MatterPage │ │ └── index.ts │ ├── PeoplePage │ │ └── index.ts │ ├── PersonPage │ │ └── index.ts │ ├── SearchPage │ │ ├── index.ts │ │ ├── types.ts │ │ └── SearchPage.tsx │ └── SearchEventsPage │ │ ├── index.ts │ │ ├── types.ts │ │ └── SearchEventsPage.tsx ├── containers │ ├── CardsContainer │ │ ├── index.ts │ │ ├── types.ts │ │ └── CardsContainer.tsx │ ├── EventContainer │ │ ├── index.ts │ │ └── types.ts │ ├── EventsContainer │ │ ├── index.ts │ │ └── types.ts │ ├── MatterContainer │ │ ├── index.ts │ │ └── MatterContainer.tsx │ ├── PeopleContainer │ │ ├── index.ts │ │ └── PeopleContainer.tsx │ ├── PersonContainer │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── types.ts │ │ └── PersonContainer.stories.tsx │ ├── SearchContainer │ │ ├── index.ts │ │ ├── types.ts │ │ └── SearchResultContainer.tsx │ ├── FetchDataContainer │ │ ├── index.ts │ │ ├── FetchDataContainer.tsx │ │ └── LazyFetchDataContainer.tsx │ └── SearchEventsContainer │ │ ├── index.ts │ │ └── types.ts ├── setupEnzyme.ts ├── utils │ ├── padNumWithZero.ts │ ├── isSubstring.ts │ ├── createError.ts │ ├── firestoreTimestampToDate.ts │ ├── ordinalSuffix.ts │ ├── secondsToHHMMSS.ts │ ├── test │ │ └── getTimeZoneDate.test.ts │ ├── getTimeZoneName.ts │ └── cleanText.ts ├── styles │ ├── mediaBreakpoints.ts │ ├── colors.ts │ └── fonts.ts ├── app │ ├── index.ts │ └── LanguageConfigContext.tsx ├── stories │ ├── model-mocks │ │ ├── imageUrl.ts │ │ ├── body.ts │ │ ├── file.ts │ │ ├── event.ts │ │ ├── sentence.ts │ │ ├── person.ts │ │ ├── indexedMatterGram.ts │ │ ├── seat.ts │ │ ├── matterSponsor.ts │ │ ├── eventMinutesItem.ts │ │ └── matterStatus.ts │ └── assets │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── code-brackets.svg │ │ ├── comments.svg │ │ ├── repo.svg │ │ └── plugin.svg ├── models │ ├── Model.ts │ ├── Matter.ts │ ├── File.ts │ ├── constants.ts │ ├── MatterFile.ts │ ├── Body.ts │ ├── MinutesItem.ts │ ├── IndexedEventGram.ts │ ├── EventMinutesItemFile.ts │ ├── IndexedMatterGram.ts │ ├── MatterSponsor.ts │ ├── Seat.ts │ ├── Session.ts │ ├── Transcript.ts │ ├── TranscriptJson.ts │ ├── Person.ts │ ├── EventMinutesItem.ts │ ├── MatterStatus.ts │ ├── Role.ts │ └── Event.ts ├── constants │ ├── StyleConstants.ts │ └── ProjectConstants.ts ├── hooks │ └── useDocumentTitle.ts ├── networking │ ├── constants.ts │ ├── FileService.ts │ ├── MatterService.ts │ ├── SeatService.ts │ ├── TranscriptJsonService.ts │ ├── NetworkResponse.ts │ ├── PersonService.ts │ ├── BodyService.ts │ ├── SessionService.ts │ ├── TranscriptService.ts │ ├── EventMinutesItemService.ts │ ├── EventMinutesItemFileService.ts │ ├── MatterStatusService.ts │ └── IndexedEventGramService.ts └── index-react.tsx ├── .huskyrc ├── .prettierignore ├── .lintstagedrc ├── tsconfig.json ├── .prettierrc ├── .gitignore ├── .storybook ├── preview-head.html ├── main.js ├── customTheme.js ├── manager.js └── preview.js ├── index.html ├── .editorconfig ├── jest.config.js ├── tsconfig.base.json ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── build-docs.yml │ ├── test-lint-and-build.yml │ └── build-main.yml └── PULL_REQUEST_TEMPLATE.md ├── vite.app.config.js ├── vite.lib.config.js └── localize.js /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | jest.config.js -------------------------------------------------------------------------------- /src/assets/images/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.jpg"; 2 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | "hooks": { 2 | "pre-commit": "lint-staged" 3 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all image files 2 | *.svg 3 | *.jpg -------------------------------------------------------------------------------- /src/components/Cards/constants.ts: -------------------------------------------------------------------------------- 1 | export const CARD_DESC_MAX_LENGTH = 140; 2 | -------------------------------------------------------------------------------- /src/components/Cards/Base/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from "./Base"; 2 | -------------------------------------------------------------------------------- /src/pages/HomePage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HomePage } from "./HomePage"; 2 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx,js,jsx}": ["prettier --write", "git add"] 3 | } -------------------------------------------------------------------------------- /src/components/Layout/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Footer } from "./Footer"; 2 | -------------------------------------------------------------------------------- /src/components/Layout/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from "./Header"; 2 | -------------------------------------------------------------------------------- /src/pages/ErrorPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ErrorPage } from "./ErrorPage"; 2 | -------------------------------------------------------------------------------- /src/pages/EventPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventPage } from "./EventPage"; 2 | -------------------------------------------------------------------------------- /src/pages/EventsPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventsPage } from "./EventsPage"; 2 | -------------------------------------------------------------------------------- /src/pages/MatterPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MatterPage } from "./MatterPage"; 2 | -------------------------------------------------------------------------------- /src/pages/PeoplePage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PeoplePage } from "./PeoplePage"; 2 | -------------------------------------------------------------------------------- /src/pages/PersonPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PersonPage } from "./PersonPage"; 2 | -------------------------------------------------------------------------------- /src/pages/SearchPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SearchPage } from "./SearchPage"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/EmptyRow/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EmptyRow } from "./EmptyRow"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/PersonCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PersonCard } from "./PersonCard"; 2 | -------------------------------------------------------------------------------- /src/components/Shared/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DecisionResult } from "./DecisionResult"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/MeetingCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MeetingCard } from "./MeetingCard"; 2 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventVideo } from "./EventVideo"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/FilterPopup/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FilterPopup } from "./FilterPopup"; 2 | -------------------------------------------------------------------------------- /src/components/Layout/TabbedContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { TabbedContainer } from "./TabbedContainer"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/VotingTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default as VotingTable } from "./VotingTable"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/EventsFilter/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventsFilter } from "./EventsFilter"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/ReactiveTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ReactiveTable } from "./ReactiveTable"; 2 | -------------------------------------------------------------------------------- /src/containers/CardsContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CardsContainer } from "./CardsContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/EventContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventContainer } from "./EventContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/EventsContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as EventsContainer } from "./EventsContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/MatterContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MatterContainer } from "./MatterContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/PeopleContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PeopleContainer } from "./PeopleContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/PersonContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PersonContainer } from "./PersonContainer"; 2 | -------------------------------------------------------------------------------- /src/containers/SearchContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SearchContainer } from "./SearchContainer"; 2 | -------------------------------------------------------------------------------- /src/pages/SearchEventsPage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SearchEventsPage } from "./SearchEventsPage"; 2 | -------------------------------------------------------------------------------- /src/components/Cards/LegislationCard/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LegislationCard } from "./LegislationCard"; 2 | -------------------------------------------------------------------------------- /src/components/Details/EventVideo/ShareVideo/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ShareVideo } from "./ShareVideo"; 2 | -------------------------------------------------------------------------------- /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/Tables/VotingTableRow/index.ts: -------------------------------------------------------------------------------- 1 | export { default as VotingTableRow } from "./VotingTableRow"; 2 | -------------------------------------------------------------------------------- /src/components/Details/MinutesItemsList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MinutesItemsList } from "./MinutesItemsList"; 2 | -------------------------------------------------------------------------------- /src/components/Details/TranscriptSearch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TranscriptSearch } from "./TranscriptSearch"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/FiltersContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FiltersContainer } from "./FiltersContainer"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/ReactiveTableRow/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ReactiveTableRow } from "./ReactiveTableRow"; 2 | -------------------------------------------------------------------------------- /src/containers/FetchDataContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FetchDataContainer } from "./FetchDataContainer"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/MeetingVotesTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MeetingVotesTable } from "./MeetingVotesTable"; 2 | -------------------------------------------------------------------------------- /src/components/Layout/LocalizationWidget/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as LocalizationWidget } from "./LocalizationWidget"; 2 | -------------------------------------------------------------------------------- /src/components/Tables/ReactiveTableHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ReactiveTableHeader } from "./ReactiveTableHeader"; 2 | -------------------------------------------------------------------------------- /src/containers/SearchEventsContainer/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SearchEventsContainer } from "./SearchEventsContainer"; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base", 3 | "include": [ 4 | "src/**/*" 5 | ], 6 | "exclude": [] 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Filters/actions.ts: -------------------------------------------------------------------------------- 1 | export const FILTER_CLEAR = "FILTER_CLEAR"; 2 | export const FILTER_UPDATE = "FILTER_UPDATE"; 3 | -------------------------------------------------------------------------------- /src/components/Tables/MeetingVotesTableRow/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MeetingVotesTableRow } from "./MeetingVotesTableRow"; 2 | -------------------------------------------------------------------------------- /src/containers/EventsContainer/types.ts: -------------------------------------------------------------------------------- 1 | import Body from "../../models/Body"; 2 | 3 | export interface EventsData { 4 | bodies: Body[]; 5 | } 6 | -------------------------------------------------------------------------------- /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/Details/EventVideo/constants.ts: -------------------------------------------------------------------------------- 1 | export enum VideoMediaType { 2 | mp4 = "mp4", 3 | webm = "webm", 4 | youtube = "youtube", 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/PersonContainer/constants.ts: -------------------------------------------------------------------------------- 1 | export const FETCH_VOTES_BATCH_SIZE = 25; 2 | 3 | export const FETCH_MATTERS_SPONSORED_BATCH_SIZE = 25; 4 | -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/HEAD/src/assets/images/dave-hoefler-reduced-1.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/HEAD/src/assets/images/dave-hoefler-reduced-2.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/HEAD/src/assets/images/dave-hoefler-reduced-3.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/HEAD/src/assets/images/dave-hoefler-reduced-4.jpg -------------------------------------------------------------------------------- /src/assets/images/dave-hoefler-reduced-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CouncilDataProject/cdp-frontend/HEAD/src/assets/images/dave-hoefler-reduced-5.jpg -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "jsxBracketSameLine": false, 4 | "printWidth": 100, 5 | "trailingComma": "es5", 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /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/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SelectSorting } from "./SelectSorting"; 2 | export { default as getSortingText } from "./getSortingText"; 3 | -------------------------------------------------------------------------------- /src/setupEnzyme.ts: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import ReactSixteenAdapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new ReactSixteenAdapter() }); 5 | -------------------------------------------------------------------------------- /src/utils/padNumWithZero.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pad the number with 0 if it is < 10. 3 | */ 4 | export default function pad(num: number) { 5 | return `${num}`.padStart(2, "0"); 6 | } 7 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /src/components/Layout/HomeSearchBar/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as HomeSearchBar } from "./HomeSearchBar"; 2 | export { searchTypeOptions, getSearchTypeText } from "./HomeSearchBar"; 3 | -------------------------------------------------------------------------------- /src/utils/isSubstring.ts: -------------------------------------------------------------------------------- 1 | const isSubstring = (string: string, substring: string): boolean => 2 | string.toLowerCase().indexOf(substring.toLowerCase()) !== -1; 3 | 4 | export default isSubstring; 5 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/styles/mediaBreakpoints.ts: -------------------------------------------------------------------------------- 1 | // px values of different screen widths 2 | export const screenWidths = { 3 | smallMobile: "320px", 4 | largeMobile: "414px", 5 | tablet: "768px", 6 | desktop: "1224px", 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Shared/Ul.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export default styled.ul<{ gap: number }>((props) => ({ 4 | marginLeft: "1.25rem", 5 | "& > li": { 6 | marginTop: props.gap, 7 | }, 8 | })); 9 | -------------------------------------------------------------------------------- /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/pages/SearchPage/types.ts: -------------------------------------------------------------------------------- 1 | export enum SEARCH_TYPE { 2 | EVENT = "events", 3 | LEGISLATION = "legislations", 4 | } 5 | 6 | export interface SearchState { 7 | query: string; 8 | searchTypes: Record; 9 | } 10 | -------------------------------------------------------------------------------- /src/stories/model-mocks/imageUrl.ts: -------------------------------------------------------------------------------- 1 | function mockImageUrl(width: number, height: number, name: string): string { 2 | return `https://via.placeholder.com/${width}x${height}?text=${name.split(" ").join("+")}`; 3 | } 4 | 5 | export { mockImageUrl }; 6 | -------------------------------------------------------------------------------- /src/pages/SearchEventsPage/types.ts: -------------------------------------------------------------------------------- 1 | import { FilterState } from "../../components/Filters/reducer"; 2 | 3 | export interface SearchEventsState { 4 | query: string; 5 | committees: FilterState; 6 | dateRange: FilterState; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/Model.ts: -------------------------------------------------------------------------------- 1 | import { ResponseData } from "../networking/NetworkResponse"; 2 | 3 | /* eslint-disable @typescript-eslint/no-empty-interface */ 4 | export interface Model {} 5 | export interface ModelConstructor { 6 | new (jsonData: ResponseData): Model; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/colors.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | lightgrey: "#DDDDDD", 3 | grey: "#595959", 4 | black: "#111111", 5 | white: "#FFFFFF", 6 | basicGrey: "#ececec", // rgb (236, 236, 236) 7 | dark_blue: "#00458b", 8 | }; 9 | 10 | export default colors; 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/Shared/Types/Matter.tsx: -------------------------------------------------------------------------------- 1 | export type Matter = { 2 | /** the name of the matter, usually a legislation name */ 3 | name: string; 4 | /** the description of the matter being voted upon */ 5 | description: string; 6 | /** the id of the matter */ 7 | id: string; 8 | }; 9 | -------------------------------------------------------------------------------- /src/containers/SearchEventsContainer/types.ts: -------------------------------------------------------------------------------- 1 | import Body from "../../models/Body"; 2 | 3 | import { SearchEventsState } from "../../pages/SearchEventsPage/types"; 4 | 5 | export interface SearchEventsContainerData { 6 | searchEventsState: SearchEventsState; 7 | bodies: Body[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/constants/StyleConstants.ts: -------------------------------------------------------------------------------- 1 | import colors from "../styles/colors"; 2 | 3 | export const TAG_CONNECTOR = " • "; 4 | export const STYLES = { 5 | COLORS: { 6 | ODD_CELL: colors.white, 7 | EVEN_CELL: colors.lightgrey, 8 | ACTIVE_UNDERLINE: colors.dark_blue, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Shared/Dot.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | const DOT_SIZE = 20; 4 | const DOT_MARGIN = 6; 5 | 6 | const Dot = styled.div({ 7 | width: DOT_SIZE, 8 | height: DOT_SIZE, 9 | borderRadius: "50%", 10 | zIndex: 1, 11 | }); 12 | 13 | export { Dot, DOT_MARGIN, DOT_SIZE }; 14 | -------------------------------------------------------------------------------- /src/pages/EventPage/utils.ts: -------------------------------------------------------------------------------- 1 | import videojs from "video.js"; 2 | 3 | import esJson from "video.js/dist/lang/es.json"; 4 | import deJson from "video.js/dist/lang/de.json"; 5 | 6 | export const initVideoJsLanguages = () => { 7 | videojs.addLanguage("es", esJson); 8 | videojs.addLanguage("de", deJson); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Shared/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | /**A flex container for pages that display cards - legislations, events */ 4 | const PageContainer = styled.div({ 5 | display: "flex", 6 | flexDirection: "column", 7 | gap: 32, 8 | }); 9 | 10 | export default PageContainer; 11 | -------------------------------------------------------------------------------- /src/containers/CardsContainer/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface Card { 4 | //**The link pathname of the card */ 5 | link: string; 6 | /**The jsx element of the card */ 7 | jsx: ReactNode; 8 | /**The search query used to find the card */ 9 | searchQuery?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Shared/FetchCardsStatus.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | import { fontSizes } from "../../styles/fonts"; 4 | 5 | /**An element to display the status of fetching cards. */ 6 | const FetchCardsStatus = styled.p({ 7 | fontSize: fontSizes.font_size_6, 8 | }); 9 | 10 | export default FetchCardsStatus; 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Council Data Project 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/Shared/Types/IndividualMeetingVote.tsx: -------------------------------------------------------------------------------- 1 | import { VOTE_DECISION } from "../../../models/constants"; 2 | 3 | export type IndividualMeetingVote = { 4 | /** the voter's name */ 5 | name: string; 6 | /** the voter's id */ 7 | personId: string; 8 | /** the persons vote */ 9 | decision: VOTE_DECISION; 10 | /** vote id */ 11 | id: string; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Shared/H2.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | import colors from "../../styles/colors"; 4 | 5 | export default styled.h2<{ hasBorderBottom?: boolean; isInline?: boolean }>((props) => ({ 6 | display: props.isInline ? "inline" : "block", 7 | paddingBottom: props.hasBorderBottom ? 8 : 0, 8 | borderBottom: props.hasBorderBottom ? `1px solid ${colors.grey}` : 0, 9 | })); 10 | -------------------------------------------------------------------------------- /src/utils/createError.ts: -------------------------------------------------------------------------------- 1 | /** Create Error from error of unknown type. 2 | * This is specifically used to convert caught errors in try catch blocks. 3 | */ 4 | export function createError(error: unknown): Error { 5 | if (error instanceof Error) { 6 | return error; 7 | } else if (typeof error === "string") { 8 | return new Error(error); 9 | } else { 10 | return new Error(String(error)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/stories/model-mocks/body.ts: -------------------------------------------------------------------------------- 1 | import Body from "../../models/Body"; 2 | 3 | const basicBody: Body = { 4 | id: "basic-id", 5 | name: "Illustrious Test Board of Experimental Confirmation", 6 | description: "A board for testing.", 7 | start_datetime: new Date("1/1/2019"), 8 | end_datetime: new Date("1/1/2020"), 9 | is_active: true, 10 | external_source_id: "test-basicBody-external-id", 11 | }; 12 | 13 | export { basicBody }; 14 | -------------------------------------------------------------------------------- /src/components/Layout/HomeSearchBar/HomeSearchBar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import HomeSearchBar from "./HomeSearchBar"; 5 | 6 | export default { 7 | component: HomeSearchBar, 8 | title: "Library/Layout/Home Search Bar", 9 | } as Meta; 10 | 11 | const Template: Story = (args) => ; 12 | 13 | export const homeSearchBar = Template.bind({}); 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/stories/model-mocks/file.ts: -------------------------------------------------------------------------------- 1 | import File from "../../models/File"; 2 | 3 | function mockImageFile(width: number, height: number, name: string): File { 4 | return { 5 | uri: `https://via.placeholder.com/${width}x${height}?text=${name.split(" ").join("+")}`, 6 | id: `test-picture-${name}`, 7 | name: `${name}.jpg`, 8 | description: "a populated test pic", 9 | media_type: "jpeg", 10 | }; 11 | } 12 | 13 | export { mockImageFile }; 14 | -------------------------------------------------------------------------------- /src/utils/firestoreTimestampToDate.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "firebase/firestore"; 2 | 3 | /* 4 | Timestamp-typed objects stored in Firestore are not directly 5 | convertable to Javascript Date objects. Therefore you must 6 | access the `seconds` property in the timestamp and convert it to a 7 | unix timestamp. 8 | */ 9 | 10 | export default function (timestamp: Timestamp): Date { 11 | return new Date(timestamp.seconds * 1000); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Shared/MinusIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MinusIcon = () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default MinusIcon; 18 | -------------------------------------------------------------------------------- /src/containers/SearchContainer/types.ts: -------------------------------------------------------------------------------- 1 | import { RenderableEvent } from "../../networking/EventSearchService"; 2 | 3 | import { SearchState } from "../../pages/SearchPage/types"; 4 | 5 | export interface SearchContainerData { 6 | searchState: SearchState; 7 | } 8 | 9 | export interface SearchData { 10 | event: { 11 | isRequested: boolean; 12 | total: number; 13 | events: RenderableEvent[]; 14 | }; 15 | //TODO: add legislation result 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Layout/LocalizationWidget/LocalizationWidget.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import LocalizationWidget from "./LocalizationWidget"; 5 | 6 | export default { 7 | component: LocalizationWidget, 8 | title: "Library/Layout/Localization Widget", 9 | } as Meta; 10 | 11 | const Template: Story = (args) => ; 12 | 13 | export const localizationWidget = Template.bind({}); 14 | -------------------------------------------------------------------------------- /src/components/Shared/ChevronDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ChevronDownIcon = () => { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default ChevronDownIcon; 18 | -------------------------------------------------------------------------------- /src/styles/fonts.ts: -------------------------------------------------------------------------------- 1 | // px values based on 16px base font size 2 | export const fontSizes = { 3 | font_size_1: "0.625rem", // 10px 4 | font_size_2: "0.75rem", // 12px 5 | font_size_3: "0.8125rem", // 13px 6 | font_size_4: "0.875rem", // 14px 7 | font_size_5: "1rem", // 16px 8 | font_size_6: "1.125rem", // 18px 9 | font_size_7: "1.25rem", // 20px 10 | font_size_8: "1.5rem", // 24px 11 | font_size_9: "1.625rem", // 26px 12 | font_size_10: "1.75rem", // 28px 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Story, Meta } from "@storybook/react"; 3 | 4 | import Header, { HeaderProps } from "./Header"; 5 | 6 | export default { 7 | component: Header, 8 | title: "Library/Layout/Header", 9 | } as Meta; 10 | 11 | const Template: Story = (args) =>
; 12 | 13 | export const header = Template.bind({}); 14 | header.args = { 15 | municipalityName: "Test deployment", 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/Shared/Types/MeetingVote.tsx: -------------------------------------------------------------------------------- 1 | import { Matter } from "./Matter"; 2 | import { IndividualMeetingVote } from "./IndividualMeetingVote"; 3 | import { EVENT_MINUTES_ITEM_DECISION } from "../../../models/constants"; 4 | 5 | export type MeetingVote = { 6 | /** the matter being voted upon */ 7 | matter: Matter; 8 | /** the voting body decision */ 9 | council_decision: EVENT_MINUTES_ITEM_DECISION; 10 | /** an array of MeetingVotes */ 11 | votes: IndividualMeetingVote[]; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Shared/AbsoluteBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | /** 4 | * A utility component to create space in the media section of a Mozilla Protocol card. 5 | * `mzp-c-card-media-wrapper` creates space with `padding`, but doesn't have any height` and `width`. 6 | * This utility component provides real `height` and `width` needed by `PlaceholderWrapper`. 7 | */ 8 | export default styled.div({ 9 | position: "absolute", 10 | height: "100%", 11 | width: "100%", 12 | }); 13 | -------------------------------------------------------------------------------- /src/hooks/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | function useDocumentTitle(title?: string) { 4 | useEffect(() => { 5 | if (title == null || title === "") { 6 | // If there's nothing meaningful to put in the title, do nothing. 7 | return; 8 | } 9 | 10 | const originalTitle = document.title; 11 | document.title = title; 12 | 13 | return () => { 14 | document.title = originalTitle; 15 | }; 16 | }, [title]); 17 | } 18 | 19 | export default useDocumentTitle; 20 | -------------------------------------------------------------------------------- /src/networking/constants.ts: -------------------------------------------------------------------------------- 1 | export enum WHERE_OPERATOR { 2 | lt = "<", 3 | lteq = "<=", 4 | eq = "==", 5 | gt = ">", 6 | gteq = ">=", 7 | not_eq = "!=", 8 | array_contains = "array-contains", 9 | array_contains_any = "array-contains-any", 10 | in = "in", 11 | not_in = "not-in", 12 | } 13 | 14 | export enum ORDER_DIRECTION { 15 | asc = "asc", 16 | desc = "desc", 17 | } 18 | 19 | // https://firebase.google.com/docs/firestore/query-data/queries#query_limitations 20 | export const OR_QUERY_LIMIT_NUM = 10; 21 | -------------------------------------------------------------------------------- /src/stories/model-mocks/event.ts: -------------------------------------------------------------------------------- 1 | import Event from "../../models/Event"; 2 | 3 | import { basicBody } from "./body"; 4 | 5 | const basicEvent: Event = { 6 | id: "event-id", 7 | external_source_id: "external-id", 8 | event_datetime: new Date(), 9 | body_ref: "body-ref", 10 | }; 11 | 12 | const eventWithRealImages: Event = { 13 | ...basicEvent, 14 | body: basicBody, 15 | static_thumbnail_ref: "e22f322a21e7", 16 | hover_thumbnail_ref: "da4673e5f412", 17 | }; 18 | 19 | export { basicEvent, eventWithRealImages }; 20 | -------------------------------------------------------------------------------- /src/components/Shared/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PlusIcon = () => { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default PlusIcon; 23 | -------------------------------------------------------------------------------- /src/components/Shared/DefaultAvatar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DefaultAvatar = () => ( 4 | 11 | 17 | 18 | ); 19 | 20 | export default DefaultAvatar; 21 | -------------------------------------------------------------------------------- /src/utils/ordinalSuffix.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param i any real number 4 | * @returns the string suffix appropriate for that number to make it an ordinal (e.g. for 1, return 'st' to make "1st") 5 | */ 6 | function ordinalSuffix(i: number) { 7 | const j = i % 10; 8 | const k = i % 100; 9 | if (j == 1 && k != 11) { 10 | return `${i}st`; 11 | } 12 | if (j == 2 && k != 12) { 13 | return `${i}nd`; 14 | } 15 | if (j == 3 && k != 13) { 16 | return `${i}rd`; 17 | } 18 | return `${i}th`; 19 | } 20 | 21 | export default ordinalSuffix; 22 | -------------------------------------------------------------------------------- /src/components/Shared/AdoptedIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AdoptedIcon = () => { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default AdoptedIcon; 23 | -------------------------------------------------------------------------------- /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/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/Shared/ShareIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ShareIcon = () => { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | }; 22 | 23 | export default ShareIcon; 24 | -------------------------------------------------------------------------------- /src/components/Shared/RejectedIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RejectedIcon = () => { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default RejectedIcon; 23 | -------------------------------------------------------------------------------- /src/components/Shared/InProgressIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const InProgressIcon = () => { 4 | return ( 5 | 12 | 18 | 19 | ); 20 | }; 21 | 22 | export default InProgressIcon; 23 | -------------------------------------------------------------------------------- /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/Shared/ShowMoreCards.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | import { screenWidths } from "../../styles/mediaBreakpoints"; 4 | 5 | /**A container of the show more cards button */ 6 | const ShowMoreCards = styled.div<{ isVisible: boolean }>((props) => ({ 7 | visibility: props.isVisible ? "visible" : "hidden", 8 | "& > button": { 9 | width: "100%", 10 | }, 11 | "& .ui.loader": { 12 | marginLeft: 16, 13 | }, 14 | [`@media (min-width:${screenWidths.tablet})`]: { 15 | "& > button": { 16 | width: "auto", 17 | }, 18 | }, 19 | })); 20 | 21 | export default ShowMoreCards; 22 | -------------------------------------------------------------------------------- /src/utils/secondsToHHMMSS.ts: -------------------------------------------------------------------------------- 1 | import padNumWithZero from "./padNumWithZero"; 2 | 3 | /** 4 | * 5 | * @param sec_num The time duration in seconds. 6 | * @returns The number of seconds in hh:mm:ss format. 7 | */ 8 | function secondsToHHMMSS(sec_num: number) { 9 | const sec_int = Math.floor(sec_num); 10 | const hours = Math.floor(sec_int / 3600); 11 | const minutes = Math.floor((sec_int - hours * 3600) / 60); 12 | const seconds = sec_int - hours * 3600 - minutes * 60; 13 | 14 | return `${padNumWithZero(hours)}:${padNumWithZero(minutes)}:${padNumWithZero(seconds)}`; 15 | } 16 | 17 | export default secondsToHHMMSS; 18 | -------------------------------------------------------------------------------- /src/models/Matter.ts: -------------------------------------------------------------------------------- 1 | import { ResponseData } from "../networking/NetworkResponse"; 2 | import { Model } from "./Model"; 3 | export default class Matter implements Model { 4 | id: string; 5 | external_source_id?: string; 6 | matter_type: string; 7 | name: string; 8 | title: string; 9 | 10 | constructor(jsonData: ResponseData) { 11 | this.id = jsonData["id"]; 12 | this.matter_type = jsonData["matter_type"]; 13 | this.name = jsonData["name"]; 14 | this.title = jsonData["title"]; 15 | 16 | if (jsonData["external_source_id"]) { 17 | this.external_source_id = jsonData["external_source_id"]; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "declarationDir": "type-declarations", 6 | "downlevelIteration": true, 7 | "esModuleInterop": true, 8 | "jsx": "react", 9 | "lib": ["DOM", "ES2020", "ES6", "DOM.Iterable", "ScriptHost"], 10 | "moduleResolution": "Node", 11 | "preserveConstEnums": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "ES6", 15 | "resolveJsonModule": true 16 | }, 17 | "include": [ 18 | "src/**/*.ts" 19 | ], 20 | "exclude": [ 21 | "src/**/test/**", 22 | "src/**/stories/**" 23 | ] 24 | } -------------------------------------------------------------------------------- /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/networking/FileService.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseConfig } from "../app/AppConfigContext"; 2 | 3 | import ModelService from "./ModelService"; 4 | import { COLLECTION_NAME } from "./PopulationOptions"; 5 | 6 | import File from "../models/File"; 7 | 8 | export default class FileService extends ModelService { 9 | constructor(firebaseConfig: FirebaseConfig) { 10 | super(COLLECTION_NAME.File, firebaseConfig); 11 | } 12 | 13 | async getFileById(id: string): Promise { 14 | const networkResponse = this.networkService.getDocument(id, COLLECTION_NAME.File); 15 | return this.createModel(networkResponse, File, `getFileById(${id})`) as Promise; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Shared/DocumentTextIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DocumentTextIcon = () => { 4 | return ( 5 | 14 | 20 | 21 | ); 22 | }; 23 | 24 | export default DocumentTextIcon; 25 | -------------------------------------------------------------------------------- /src/constants/ProjectConstants.ts: -------------------------------------------------------------------------------- 1 | import exampleCover1 from "../assets/images/dave-hoefler-reduced-1.jpg"; 2 | import exampleCover2 from "../assets/images/dave-hoefler-reduced-2.jpg"; 3 | import exampleCover3 from "../assets/images/dave-hoefler-reduced-3.jpg"; 4 | import exampleCover4 from "../assets/images/dave-hoefler-reduced-4.jpg"; 5 | import exampleCover5 from "../assets/images/dave-hoefler-reduced-5.jpg"; 6 | 7 | export const SUPPORTED_LANGUAGES = ["en", "de", "es"]; 8 | 9 | export const FETCH_CARDS_BATCH_SIZE = 10; 10 | 11 | export const EXAMPLE_COVER_VIEWS = [ 12 | exampleCover1, 13 | exampleCover2, 14 | exampleCover3, 15 | exampleCover4, 16 | exampleCover5, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/containers/PersonContainer/types.ts: -------------------------------------------------------------------------------- 1 | import Person from "../../models/Person"; 2 | import Role from "../../models/Role"; 3 | 4 | import { FetchDataState } from "../FetchDataContainer/useFetchData"; 5 | 6 | export interface PersonPageData { 7 | /** The person */ 8 | person: Person; 9 | /** roles held by the person with only body populated */ 10 | roles: Role[]; 11 | /** Councilmember roles with only seat populated */ 12 | councilMemberRoles: Role[]; 13 | /** Picture src of the person */ 14 | personPictureSrc: FetchDataState; 15 | /** Picture of the seat for the person's recent councilmember role */ 16 | seatPictureSrc: FetchDataState; 17 | } 18 | -------------------------------------------------------------------------------- /src/models/File.ts: -------------------------------------------------------------------------------- 1 | import { ResponseData } from "../networking/NetworkResponse"; 2 | import { Model } from "./Model"; 3 | 4 | class File implements Model { 5 | id: string; 6 | uri: string; 7 | name: string; 8 | description?: string; 9 | media_type?: string; 10 | 11 | constructor(jsonData: ResponseData) { 12 | this.id = jsonData["id"]; 13 | 14 | this.uri = jsonData["uri"]; 15 | 16 | this.name = jsonData["name"]; 17 | 18 | if (jsonData["description"]) { 19 | this.description = jsonData["description"]; 20 | } 21 | 22 | if (jsonData["media_type"]) { 23 | this.media_type = jsonData["media_type"]; 24 | } 25 | } 26 | } 27 | 28 | export default File; 29 | -------------------------------------------------------------------------------- /src/pages/ErrorPage/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | interface ErrorPageProps { 5 | error?: any; 6 | } 7 | 8 | const ErrorPage: FC = ({ error }: ErrorPageProps) => { 9 | const history = useHistory(); 10 | const errorText = error ? error.toString() : ""; 11 | 12 | return ( 13 |
14 |

Sorry, we can’t find that page.

15 |

{errorText}

16 | 19 |
20 | ); 21 | }; 22 | 23 | export default ErrorPage; 24 | -------------------------------------------------------------------------------- /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/index-react.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App, AppConfigProvider } from "./app"; 4 | 5 | ReactDOM.render( 6 | 7 | 22 | 23 | 24 | , 25 | document.getElementById("root") 26 | ); 27 | -------------------------------------------------------------------------------- /src/containers/FetchDataContainer/FetchDataContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactNode } from "react"; 2 | 3 | import { Loader } from "semantic-ui-react"; 4 | import ErrorPage from "../../pages/ErrorPage/ErrorPage"; 5 | 6 | interface FetchDataContainerProps { 7 | isLoading: boolean; 8 | children?: ReactNode; 9 | error?: any; 10 | } 11 | 12 | const FetchDataContainer: FC = ({ 13 | isLoading, 14 | children, 15 | error, 16 | }: FetchDataContainerProps) => { 17 | if (isLoading) { 18 | return ; 19 | } 20 | if (error) { 21 | return ; 22 | } 23 | return <>{children}; 24 | }; 25 | 26 | export default FetchDataContainer; 27 | -------------------------------------------------------------------------------- /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/networking/MatterService.ts: -------------------------------------------------------------------------------- 1 | import ModelService from "./ModelService"; 2 | import { COLLECTION_NAME } from "./PopulationOptions"; 3 | import Matter from "../models/Matter"; 4 | import { FirebaseConfig } from "../app/AppConfigContext"; 5 | 6 | export default class MatterService extends ModelService { 7 | constructor(firebaseConfig: FirebaseConfig) { 8 | super(COLLECTION_NAME.Matter, firebaseConfig); 9 | } 10 | 11 | async getMatterById(matterId: string): Promise { 12 | const networkResponse = this.networkService.getDocument(matterId, COLLECTION_NAME.Matter); 13 | return this.createModel( 14 | networkResponse, 15 | Matter, 16 | `getMatterById(${matterId})` 17 | ) as Promise; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/networking/SeatService.ts: -------------------------------------------------------------------------------- 1 | import { orderBy } from "firebase/firestore"; 2 | import ModelService from "./ModelService"; 3 | import Seat from "../models/Seat"; 4 | import { COLLECTION_NAME } from "./PopulationOptions"; 5 | import { FirebaseConfig } from "../app/AppConfigContext"; 6 | 7 | export default class SeatService extends ModelService { 8 | constructor(firebaseConfig: FirebaseConfig) { 9 | super(COLLECTION_NAME.Seat, firebaseConfig); 10 | } 11 | 12 | async getAllSeats(): Promise { 13 | const networkResponse = this.networkService.getDocuments(COLLECTION_NAME.Seat, [ 14 | orderBy("name"), 15 | ]); 16 | 17 | return this.createModels(networkResponse, Seat, `getAllSeats`) as Promise; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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) =>