├── public
├── BetaIcon.jpg
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── BetaKnesset.jpg
├── manifest.json
└── index.html
├── src
├── static
│ └── images
│ │ ├── Or.jpg
│ │ ├── Omer.jpg
│ │ ├── Yoav.jpg
│ │ ├── Shani.jpg
│ │ └── BetaKnesset.jpg
├── setupTests.js
├── pages
│ ├── FrameApp.js
│ ├── Main
│ │ ├── index.js
│ │ ├── Search
│ │ │ ├── index.js
│ │ │ ├── Histogram.js
│ │ │ └── Bubble
│ │ │ │ └── index.js
│ │ ├── index.css
│ │ └── PersonProfile
│ │ │ ├── PersonQuotes.js
│ │ │ ├── index.css
│ │ │ ├── PersonBillsStats
│ │ │ └── index.js
│ │ │ ├── PersonBills
│ │ │ └── index.js
│ │ │ └── index.js
│ ├── Calendar
│ │ ├── index.css
│ │ └── index.js
│ ├── Quotes.js
│ └── Document
│ │ └── index.js
├── event-utils.js
├── reportWebVitals.js
├── index.js
├── config.json
├── defaultTopics.json
├── theme.js
├── index.css
├── components
│ ├── Explainer.js
│ ├── ShareButtons.js
│ ├── Dialog.js
│ ├── DocumentLink.js
│ ├── ChatLoader
│ │ ├── index.css
│ │ └── index.js
│ ├── QuoteFooter.js
│ ├── QuotesLoader
│ │ ├── quotes.json
│ │ └── index.js
│ ├── NavigationBar
│ │ ├── Disclaimer.js
│ │ ├── About.js
│ │ ├── ContactUs.js
│ │ └── index.js
│ ├── ScrollableView
│ │ └── index.js
│ ├── Chat
│ │ ├── index.js
│ │ └── index.css
│ ├── PersonSearch.js
│ ├── WordCloud
│ │ └── index.js
│ └── QuotesSearch.js
├── particles.config.json
├── logo.svg
├── App.js
└── utils.js
├── README.md
├── package.json
└── .gitignore
/public/BetaIcon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/public/BetaIcon.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/BetaKnesset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/public/BetaKnesset.jpg
--------------------------------------------------------------------------------
/src/static/images/Or.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/src/static/images/Or.jpg
--------------------------------------------------------------------------------
/src/static/images/Omer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/src/static/images/Omer.jpg
--------------------------------------------------------------------------------
/src/static/images/Yoav.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/src/static/images/Yoav.jpg
--------------------------------------------------------------------------------
/src/static/images/Shani.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/src/static/images/Shani.jpg
--------------------------------------------------------------------------------
/src/static/images/BetaKnesset.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SgtTepper/BetaKnessetWeb/HEAD/src/static/images/BetaKnesset.jpg
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 |
--------------------------------------------------------------------------------
/src/pages/FrameApp.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { ScrollPage } from "../components/ScrollableView";
4 |
5 | const FrameApp = React.memo(function ({ url, title }) {
6 | return (
7 |
8 |
13 |
14 | );
15 | });
16 | export default FrameApp;
17 |
--------------------------------------------------------------------------------
/src/event-utils.js:
--------------------------------------------------------------------------------
1 | let eventGuid = 0;
2 | let todayStr = new Date().toISOString().replace(/T.*$/, ""); // YYYY-MM-DD of today
3 |
4 | export const INITIAL_EVENTS = [
5 | {
6 | id: createEventId(),
7 | title: "All-day event",
8 | start: todayStr,
9 | },
10 | {
11 | id: createEventId(),
12 | title: "Timed event",
13 | start: todayStr + "T12:00:00",
14 | },
15 | ];
16 |
17 | export function createEventId() {
18 | return String(eventGuid++);
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/Main/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Search from "./Search";
3 | import PersonProfile from "./PersonProfile";
4 |
5 | import "./index.css";
6 | import { ScrollIntoView } from "rrc";
7 | import { useLocation } from "react-router-dom";
8 |
9 | export default function Main() {
10 | const location = useLocation();
11 | return (
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import("web-vitals").then(
4 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
5 | getCLS(onPerfEntry);
6 | getFID(onPerfEntry);
7 | getFCP(onPerfEntry);
8 | getLCP(onPerfEntry);
9 | getTTFB(onPerfEntry);
10 | }
11 | );
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitUrl": "https://github.com/SgtTepper/BetaKnessetWeb",
3 | "server": "https://betaknesset.azurewebsites.net/api",
4 | "server_debug": "http://localhost:7071/api",
5 |
6 | "defaultBubblesCount": 25,
7 | "defaultBubblesCountMobile": 17,
8 | "wordcloudCount": 60,
9 | "wordcloudCountMobile": 25,
10 | "apps": [
11 | {
12 | "name": "votes",
13 | "title": "הצבעות",
14 | "url": "https://beta-knesset-votes-simulator.vercel.app/"
15 | }
16 | ],
17 | "showOverloadedScreen": false
18 | }
19 |
--------------------------------------------------------------------------------
/src/defaultTopics.json:
--------------------------------------------------------------------------------
1 | [
2 | "לגליזציה",
3 | "סמים",
4 | "איראן",
5 | "קורונה",
6 | "כלכלה",
7 | "חינוך",
8 | "צבא",
9 | "ביטחון",
10 | "שחיתות",
11 | "דת",
12 | "חרדים",
13 | "ערבים",
14 | "השמאל",
15 | "הימין",
16 | "חילונים",
17 | "ארצות הברית",
18 | "טראמפ",
19 | "עסקים קטנים",
20 | "רפואה",
21 | "חיסונים",
22 | "סוריה",
23 | "לבנון",
24 | "בורקס",
25 | "אירופה",
26 | "פריפריה",
27 | "תרבות",
28 | "ספורט",
29 | "להט\"ב",
30 | "שוק",
31 | "התקציב",
32 | "מיסוי",
33 | "אלימות",
34 | "נשים",
35 | "זכויות הפרט",
36 | "משטרה",
37 | "פנסיה",
38 | "עולים"
39 | ]
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import { unstable_createMuiStrictModeTheme as createMuiTheme } from "@material-ui/core";
2 |
3 | const theme = createMuiTheme({
4 | // rtl fixes
5 | direction: "rtl",
6 | overrides: {
7 | MuiInputAdornment: {
8 | positionStart: {
9 | marginRight: 0,
10 | },
11 | positionEnd: {
12 | marginLeft: 0,
13 | },
14 | },
15 | MuiListItem: {
16 | root: {
17 | textAlign: "right",
18 | },
19 | },
20 | },
21 |
22 | palette: {
23 | primary: {
24 | main: "#0d47a1",
25 | },
26 | secondary: {
27 | main: "#90756d",
28 | },
29 | pronounced: {
30 | main: "white",
31 | },
32 | },
33 | typography: {
34 | fontFamily: ['"Secular One"', "sans-serif"],
35 | },
36 | });
37 | export default theme;
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Beta Knesset - בטא מחוקקים
2 |
3 | [](https://www.betaknesset.com/)
4 |
5 | [BetaKnesset](https://www.betaknesset.com) is a tool to inspect and explore the Israeli parliament.
6 |
7 | ## Resources
8 | * [Knesset Web Project](https://github.com/SgtTepper/BetaKnessetWeb)
9 | * [Knesset Data Project](https://github.com/SgtTepper/BetaKnessetData)
10 | * Due to security concerns, we are keeping our back-end code to ourselves 😎
11 |
12 | ## Credits
13 |
14 | * Vote simulator [App](https://github.com/OrLichter/BetaKnessetWeb) and [Repository](https://github.com/OrLichter/BetaKnessetWeb)
15 | * [Knesset Data](https://main.knesset.gov.il/Activity/Info/pages/databases.aspx)
16 | * [Hebrew NLP](https://hebrew-nlp.co.il/)
17 | * [Chat Component](https://codepen.io/Varo/pen/gbZzgr)
18 |
19 |
20 | ## Contributing
21 |
22 | Contributions to the project are welcome! if you have a neat idea feel free to [contact us](mailto:betaknesset@gmail.com)
23 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | scroll-behavior: smooth;
3 | overflow: hidden;
4 | height: -webkit-fill-available;
5 | height: -moz-available;
6 | height: stretch;
7 | }
8 |
9 | body {
10 | direction: rtl;
11 | margin: 0;
12 | overflow: hidden;
13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
14 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
15 | "Helvetica Neue", sans-serif;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | height: 100vh;
19 | height: -webkit-fill-available;
20 |
21 | display: flex;
22 | flex-direction: column;
23 | align-items: stretch;
24 | }
25 |
26 | code {
27 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
28 | monospace;
29 | }
30 |
31 | a {
32 | text-decoration: none;
33 | color: rgb(42, 68, 101);
34 | }
35 | a:hover {
36 | text-decoration: none;
37 | color: rgb(56, 105, 150);
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Explainer.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Typography from "@material-ui/core/Typography";
3 | import Dialog from "./Dialog";
4 | import IconButton from "@material-ui/core/IconButton";
5 | import InfoRoundedIcon from "@material-ui/icons/InfoRounded";
6 |
7 | export default function Explainer({ children, style }) {
8 | const [open, setOpen] = useState(false);
9 | return (
10 | <>
11 | setOpen(true)}
13 | style={{ position: "absolute", top: 0, left: 0, ...style }}
14 | >
15 |
16 |
17 |
18 |
19 | מה אני רואה פה?
20 |
21 | {children}
22 |
23 | >
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/pages/Main/Search/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Bubble from "./Bubble";
3 | import Histogram from "./Histogram";
4 | import { ScrollPage } from "../../../components/ScrollableView";
5 | import { makeStyles } from "@material-ui/core/styles";
6 | import QuotesSearch from "../../../components/QuotesSearch";
7 |
8 | const useStyles = makeStyles({
9 | root: {
10 | maxWidth: "850px",
11 | height: "85%",
12 | display: "flex",
13 | flexDirection: "column",
14 | position: "relative",
15 | placeContent: "flex-start",
16 | },
17 | });
18 |
19 | const Search = React.memo(function Search() {
20 | const classes = useStyles();
21 |
22 | return (
23 |
24 |
30 |
31 |
32 | );
33 | });
34 |
35 | export default Search;
36 |
--------------------------------------------------------------------------------
/src/pages/Calendar/index.css:
--------------------------------------------------------------------------------
1 | .calendar h2 {
2 | margin: 0;
3 | font-size: 16px;
4 | }
5 |
6 | .calendar ul {
7 | margin: 0;
8 | padding: 0 0 0 1.5em;
9 | }
10 |
11 | .calendar li {
12 | margin: 1.5em 0;
13 | padding: 0;
14 | }
15 |
16 | .calendar b {
17 | /* used for event dates/times */
18 | margin-right: 3px;
19 | }
20 |
21 | .demo-app {
22 | width: 100%;
23 | height: 80vh;
24 | display: flex;
25 | font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
26 | font-size: 14px;
27 | }
28 |
29 | .demo-app-sidebar {
30 | width: 300px;
31 | line-height: 1.5;
32 | background: #eaf9ff;
33 | border-right: 1px solid #d3e2e8;
34 | }
35 |
36 | .demo-app-sidebar-section {
37 | padding: 2em;
38 | }
39 |
40 | .demo-app-main {
41 | flex-grow: 1;
42 | padding: 0.5em 2em;
43 | }
44 |
45 | .fc {
46 | /* the calendar root */
47 | height: 85%;
48 | max-width: 1100px;
49 | margin: 0 auto;
50 | }
51 |
52 | .fc-toolbar-title {
53 | font-family: "Secular One", sans-serif;
54 | color: #555;
55 | font-weight: normal;
56 | }
57 |
58 | .fc-timegrid-event-harness {
59 | overflow: hidden;
60 | }
61 |
62 | .fc-daygrid-event-harness {
63 | overflow: hidden;
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/ShareButtons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | TelegramIcon,
4 | TwitterIcon,
5 | WhatsappIcon,
6 | TelegramShareButton,
7 | TwitterShareButton,
8 | WhatsappShareButton,
9 | } from "react-share";
10 |
11 | export default function ShareButtons({ Text, Speaker, FilePath }) {
12 | const size = 18;
13 | return (
14 |
15 |
19 |
20 |
21 | 500 ? "..." : ""
25 | }"`}
26 | >
27 |
28 |
29 |
34 |
35 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Dialog.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles } from "@material-ui/core/styles";
3 | import Button from "@material-ui/core/Button";
4 | import Dialog from "@material-ui/core/Dialog";
5 | import MuiDialogContent from "@material-ui/core/DialogContent";
6 | import DialogActions from "@material-ui/core/DialogActions";
7 |
8 | const DialogContent = withStyles((theme) => ({
9 | root: {
10 | padding: theme.spacing(2),
11 | fontFamily: "'Varela Round', sans-serif",
12 | },
13 | }))(MuiDialogContent);
14 |
15 | export default function SiteDialog({ children, open, setOpen, closeText }) {
16 | const handleClose = () => {
17 | setOpen(false);
18 | };
19 |
20 | return (
21 |
26 | {children}
27 |
28 |
34 | {closeText}
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/DocumentLink.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "../utils";
3 | import { Link } from "react-router-dom";
4 |
5 | export default function DocumentLink({
6 | children,
7 | SessionType,
8 | DocumentID,
9 | Index,
10 | PersonID,
11 | }) {
12 | const query = useQuery();
13 |
14 | const link = getDocumentLink({
15 | SessionType,
16 | DocumentID,
17 | Index,
18 | PersonID,
19 | query,
20 | });
21 | if (link === null) return children ||
;
22 |
23 | return {children};
24 | }
25 |
26 | export function getDocumentLink({
27 | SessionType,
28 | DocumentID,
29 | Index,
30 | PersonID,
31 | query,
32 | }) {
33 | const urlParams = new URLSearchParams();
34 |
35 | if (PersonID) urlParams.set("personID", PersonID);
36 | if (query?.length) urlParams.set("q", query);
37 | if (Index !== undefined) urlParams.set("index", Index);
38 |
39 | const urlExtension = `${DocumentID}?${urlParams.toString()}`;
40 |
41 | switch (SessionType) {
42 | case "Committee":
43 | return `/document/committee/${urlExtension}`;
44 | case "Plenum":
45 | return `/document/plenum/${urlExtension}`;
46 | default:
47 | return null;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/Main/index.css:
--------------------------------------------------------------------------------
1 | #top,
2 | #quotes,
3 | #calendar {
4 | place-content: flex-start;
5 | flex-direction: column;
6 | place-items: center;
7 | }
8 |
9 | .rv-treemap__leaf__content {
10 | align-self: stretch;
11 | justify-self: stretch;
12 | display: flex;
13 | justify-items: stretch;
14 | align-items: stretch;
15 | width: 100%;
16 | padding: 0 !important;
17 | padding-bottom: 7.5% !important;
18 | }
19 |
20 | .share-buttons > * {
21 | text-align: center;
22 | margin: 0 0.15em;
23 | }
24 |
25 | @keyframes pulse {
26 | 0% {
27 | transform: translate(-10%, 8%);
28 | }
29 | 100% {
30 | transform: translate(14%, -8%);
31 | }
32 | 100% {
33 | transform: translate(8%, 15%);
34 | }
35 | }
36 |
37 | .suggestion {
38 | display: flex;
39 | place-items: center;
40 | align-items: center;
41 | width: 100%;
42 | padding: 0 5%;
43 | font-weight: bold;
44 | cursor: pointer;
45 | font-size: 100%;
46 | transition: font-size 0.2s;
47 | }
48 |
49 | .suggestion:hover {
50 | font-size: 110%;
51 | }
52 |
53 | .person-bubble {
54 | background-color: rgba(158, 82, 31, 0.15);
55 | transition: all 0.3s;
56 | }
57 | .person-bubble:hover {
58 | background-color: rgba(255, 255, 255, 0);
59 | }
60 |
61 | .person-bubble .person-name {
62 | text-align: center;
63 | width: 100%;
64 | background-color: black;
65 | opacity: 0.7;
66 | padding: 0.5em 1em;
67 | transition: all 0.3s;
68 | }
69 | .person-bubble:hover .person-name {
70 | background-color: #001177;
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/ChatLoader/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | body {
3 | background: #E9E9E9;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | }
8 | */
9 |
10 | .wrapper {
11 | width: 100%;
12 | }
13 |
14 | .chat-container {
15 | margin: 30px;
16 | padding: 30px;
17 | background: #fff;
18 | display: flex;
19 | flex-direction: column;
20 | max-height: 60vh;
21 | overflow: hidden;
22 | }
23 |
24 | .wrapper-cell {
25 | display: flex;
26 | margin-bottom: 30px;
27 | }
28 |
29 | @-webkit-keyframes placeHolderShimmer {
30 | 0% {
31 | background-position: 468px 0;
32 | }
33 | 100% {
34 | background-position: -468px 0;
35 | }
36 | }
37 |
38 | @keyframes placeHolderShimmer {
39 | 0% {
40 | background-position: 468px 0;
41 | }
42 | 100% {
43 | background-position: -468px 0;
44 | }
45 | }
46 | .animated-background,
47 | .text-line,
48 | .image {
49 | -webkit-animation-duration: 1.25s;
50 | animation-duration: 1.25s;
51 | -webkit-animation-fill-mode: forwards;
52 | animation-fill-mode: forwards;
53 | -webkit-animation-iteration-count: infinite;
54 | animation-iteration-count: infinite;
55 | -webkit-animation-name: placeHolderShimmer;
56 | animation-name: placeHolderShimmer;
57 | -webkit-animation-timing-function: linear;
58 | animation-timing-function: linear;
59 | background: #e6e6e6;
60 | background: linear-gradient(to left, #e6e6e6 8%, #e0e0e0 18%, #e6e6e6 33%);
61 | background-size: 800px 104px;
62 | height: 96px;
63 | position: relative;
64 | }
65 |
66 | .image {
67 | height: 60px;
68 | width: 60px;
69 | }
70 |
71 | .text {
72 | flex-grow: 1;
73 | margin-right: 20px;
74 | }
75 |
76 | .text-line {
77 | height: 10px;
78 | width: 100%;
79 | margin: 4px 0;
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/ChatLoader/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 |
4 | export default function ChatLoader({ show }) {
5 | if (!show) return null;
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | const ChatItem = function () {
21 | return (
22 |
23 |
24 |
25 |
29 | {" "}
30 |
31 |
35 | {" "}
36 |
37 | {Math.random() > 0.25 && (
38 |
42 | {" "}
43 |
44 | )}
45 | {Math.random() > 0.5 && (
46 |
50 | {" "}
51 |
52 | )}
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/particles.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "particles": {
3 | "number": {
4 | "value": 20,
5 | "density": {
6 | "enable": true,
7 | "value_area": 1000
8 | }
9 | },
10 | "color": {
11 | "value": "#5294c8"
12 | },
13 | "shape": {
14 | "type": ["circle"],
15 | "stroke": {
16 | "width": 0,
17 | "color": "#000000"
18 | },
19 | "polygon": {
20 | "nb_sides": 5
21 | }
22 | },
23 | "opacity": {
24 | "value": 0.4,
25 | "anim": {
26 | "enable": false
27 | }
28 | },
29 | "size": {
30 | "value": 5,
31 | "random": true,
32 | "anim": {
33 | "enable": false
34 | }
35 | },
36 | "line_linked": {
37 | "enable": true,
38 | "distance": 200,
39 | "color": "#72a4e8",
40 | "opacity": 0.2,
41 | "width": 1
42 | },
43 | "move": {
44 | "enable": true,
45 | "speed": 1,
46 | "random": true,
47 | "straight": false,
48 | "out_mode": "bounce",
49 | "bounce": false,
50 | "attract": {
51 | "enable": false
52 | }
53 | }
54 | },
55 | "interactivity": {
56 | "detect_on": "canvas",
57 | "onresize": {
58 | "enable": false,
59 | "density_auto": true,
60 | "density_area": 40000000
61 | }
62 | },
63 | "modes": {
64 | "bubble": {
65 | "distance": 200,
66 | "size": 4,
67 | "duration": 2,
68 | "opacity": 0.8,
69 | "speed": 1
70 | }
71 | },
72 | "retina_detect": true
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/QuoteFooter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ShareButtons from "./ShareButtons";
3 | import { toNiceDate, useQuery } from "../utils";
4 | import DocumentLink, { getDocumentLink } from "./DocumentLink";
5 |
6 | export default function QuoteFooter(props) {
7 | const { StartDate, TopicName, Text, Speaker, isInProtocol } = props;
8 | const query = useQuery();
9 | return (
10 | <>
11 | {isInProtocol ? (
12 |
13 | ) : (
14 |
15 |
22 | לחצו לפרוטוקול המלא -
23 | {TopicName}
24 |
28 | ❯❯❯
29 |
30 |
31 |
32 | )}
33 |
34 |
44 |
45 | {toNiceDate(new Date(StartDate), true)}
46 |
47 |
48 | >
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/QuotesLoader/quotes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "בנימין נתניהו",
4 | "quote": "זה ציטוט מדויק"
5 | },
6 | {
7 | "name": "ניצן הורוביץ",
8 | "quote": "הפסקה קלה, לשתות איזה תה, לא יזיק. ככה, כמה דקות"
9 | },
10 | {
11 | "name": "לאה פדידה",
12 | "quote": "זה היה מצחיק אם זה לא היה עצוב כל כך."
13 | },
14 | {
15 | "name": "יואל חסון",
16 | "quote": "בשנייה וחצי מוכיחים שמה שאתה אומר זה שטויות."
17 | },
18 | {
19 | "name": "אופיר כץ",
20 | "quote": "אלי, תעשה רגע בגוגל \"דמוקרטיה\". אתה במחשב – תעשה בגוגל \"דמוקרטיה\"."
21 | },
22 | {
23 | "name": "עיסאווי פריג׳",
24 | "quote": "תנו לנו להגיע לאנשים בלי עריכה, באותנטיות, בחופשיות"
25 | },
26 | {
27 | "name": "יוליה מלינובסקי קונין",
28 | "quote": "התעסקתם בקקה ולא התעסקתם במהות."
29 | },
30 | {
31 | "name": "אביגדור ליברמן",
32 | "quote": "אני בעד לתת לכל ליצן פה במה, זה מאוד מתאים."
33 | },
34 | {
35 | "name": "משה גפני",
36 | "quote": "היום ועדת הכספים זה המקום הכי מעניין במזרח התיכון. שום דבר אחר לא."
37 | },
38 | {
39 | "name": "יעל כהן-פארן",
40 | "quote": "מטרתנו לפתח כמה שיותר שקיפות."
41 | },
42 | {
43 | "name": "אופיר כץ",
44 | "quote": "לא, זה לא לפרוטוקול."
45 | },
46 | {
47 | "name": "יואב קיש",
48 | "quote": "לא לפרוטוקול. אבל את התרגילים של האופוזיציה, אני מכבד."
49 | },
50 | {
51 | "name": "קרן ברק",
52 | "quote": "אני מציעה רק שאת האפליקציה - תשדרו לציבור אם יש אפליקציה."
53 | },
54 | {
55 | "name": "עידן רול",
56 | "quote": "אנחנו רוצים תשובות."
57 | },
58 | {
59 | "name": "נפתלי בנט",
60 | "quote": "אבל אז כל היום אני אצטרך להכחיש דברים. אין לי כוח לזה."
61 | },
62 | {
63 | "name": "משה גפני",
64 | "quote": "טוב שלא הסרטת מה שאמרתי קודם."
65 | },
66 | {
67 | "name": "אמיר אוחנה",
68 | "quote": "ברוך הבא לכנסת."
69 | }
70 | ]
71 |
--------------------------------------------------------------------------------
/src/components/NavigationBar/Disclaimer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Typography from "@material-ui/core/Typography";
3 | import Dialog from "../Dialog";
4 |
5 | export default function DisclaimerDialog(props) {
6 | return (
7 |
8 |
9 | גילוי נאות
10 |
11 |
12 | לתשומת לב הגולשים
13 |
14 |
15 | האתר מתבסס על{" "}
16 |
21 | מאגרי המידע הפתוחים של הכנסת
22 |
23 | , וזאת בהתאם לתקנות המתירות פיתוח יישומים ומערכות על בסיס מידע
24 | זה.
25 |
26 |
27 | באתר זה מוצגים פריטים שהינם תוצאה של אלגוריתמים לעיבוד מידע אשר
28 | אינם חפים מטעויות. יתכן למשל שהתוכנה שלנו תבצע טעות בזיהוי
29 | דובר/ת, בפירוש הטקסט, או במדידת ערכים הקשורים בטקסט.
30 |
31 |
32 | למען הסר ספק, המידע האמין והמדויק ביותר הינו זה שמקורו במאגרי
33 | המידע של הכנסת ובמסמכי הפרוטוקולים שמפורסמים באתר הכנסת והם מקור
34 | המידע המכריע.
35 |
36 |
37 | הנהלת האתר אינה נושאת באחריות על טעויות שייתכנו. השימוש באתר
38 | ושיתוף המידע שבאתר הוא באחריות המשתמש בלבד.
39 |
40 |
41 | התמונות באתר לקוחות{" "}
42 |
47 | מאתר הכנסת
48 |
49 | .
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fullcalendar/bootstrap": "^6.1.4",
7 | "@fullcalendar/core": "^6.1.4",
8 | "@fullcalendar/daygrid": "^6.1.4",
9 | "@fullcalendar/interaction": "^6.1.4",
10 | "@fullcalendar/list": "^6.1.4",
11 | "@fullcalendar/react": "^6.1.4",
12 | "@fullcalendar/timegrid": "^6.1.4",
13 | "@material-ui/core": "^4.11.3",
14 | "@material-ui/icons": "^4.11.2",
15 | "@material-ui/lab": "^4.0.0-alpha.57",
16 | "@testing-library/jest-dom": "^5.11.4",
17 | "@testing-library/react": "^14.0.0",
18 | "@testing-library/user-event": "^14.4.3",
19 | "axios": "^1.3.4",
20 | "axios-retry": "^3.1.9",
21 | "chart.js": "^2.9.4",
22 | "chartjs-plugin-datalabels": "^0.7.0",
23 | "color-hash": "^2.0.2",
24 | "react": "^18.2.0",
25 | "react-chartjs-2": "^2.11.1",
26 | "react-device-detect": "^2.2.3",
27 | "react-dom": "^18.2.0",
28 | "react-google-charts": "^4.0.0",
29 | "react-highlight-words": "^0.20.0",
30 | "react-infinite-scroll-component": "^6.0.0",
31 | "react-lazyload": "^3.2.0",
32 | "react-particles": "latest",
33 | "react-responsive": "^9.0.2",
34 | "react-router-dom": "^5.2.0",
35 | "react-scripts": "5.0.1",
36 | "react-share": "^4.3.1",
37 | "react-text-transition": "^3.0.2",
38 | "react-vis": "^1.11.7",
39 | "react-wordcloud": "^1.2.7",
40 | "rrc": "^0.10.1",
41 | "uuid": "^9.0.0",
42 | "web-vitals": "^3.3.0"
43 | },
44 | "scripts": {
45 | "start": "react-scripts start",
46 | "build": "react-scripts build",
47 | "test": "react-scripts test",
48 | "eject": "react-scripts eject"
49 | },
50 | "eslintConfig": {
51 | "extends": [
52 | "react-app",
53 | "react-app/jest"
54 | ]
55 | },
56 | "browserslist": {
57 | "production": [
58 | ">0.2%",
59 | "not dead",
60 | "not op_mini all"
61 | ],
62 | "development": [
63 | "last 1 chrome version",
64 | "last 1 firefox version",
65 | "last 1 safari version"
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/NavigationBar/About.js:
--------------------------------------------------------------------------------
1 | import Dialog from "../Dialog";
2 | import { Typography } from "@material-ui/core";
3 |
4 | export default function AboutDialog(props) {
5 | return (
6 |
7 |
8 | אודות
9 |
10 |
11 | בטא מחוקקים הוא פרויקט קהילתי שהתחיל מהשאלה הפשוטה - מה עושים
12 | חברי הכנסת?
13 |
14 |
15 | כשגילינו ש
16 |
17 |
22 | המידע של הכנסת זמין כבר מ-2016
23 |
24 | {" "}
25 | ופתוח לכל, הבנו שיש פה הזדמנות לגשת להכל אחרת.
26 |
27 |
28 |
29 | במקום לקבל פרפרזות מהתקשורת, אפשר לראות את{" "}
30 | ההקשר האמיתי .
31 |
32 |
33 | במקום להשען על מידע ממקורות לא ברורים, ניתן לבסס הבנה על סמך{" "}
34 | נתונים עובדתיים
35 |
36 |
37 | במקום שרוב חברי הכנסת יהיו דמויות עלומות ולא מוכרות, אפשר
38 | לקרוא, לחקור ולהתעמק במה שבאמת חשוב להם .
39 |
40 |
41 |
42 | כלל הציטוטים המופיעים באתר זה חולצו בעזרת אלגוריתם שבנינו שעובר
43 | על פרוטוקולי המליאה והועדות של הכנסת החל משנת 2015.
44 |
45 |
46 | המידע בפרויקט זה מתעדכן באופן שוטף יחד עם מאגרי המידע של הכנסת
47 | מדי יום ועדכניות המידע תלויה בזמן עדכון הארכיונים של הכנסת.
48 |
49 |
50 | נציין כי אנו פועלים ללא מטרת רווח והפרויקט בלעדית למען הקהל
51 | הרחב, תודה מיוחדת{" "}
52 |
57 | לסדנא לידע ציבורי
58 | {" "}
59 | שמאחסנת עבורנו חלק מהמידע ונושאת בעלויות תחזוקת האתר.
60 |
61 |
62 | אנחנו יותר מנשמח לקבל פידבקים ואנו מזמינים אתכם ליצור עמנו קשר.
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/ScrollableView/index.js:
--------------------------------------------------------------------------------
1 | import { makeStyles } from "@material-ui/core/styles";
2 | import clsx from "clsx";
3 | import { useBigScreen, useWindowSize } from "../../utils";
4 |
5 | const useStyles = makeStyles((theme) => ({
6 | scrollView: {
7 | scrollSnapType: "y mandatory",
8 | WebkitOverflowScrolling: "touch",
9 | overflowY: "auto",
10 | overflowX: "hidden",
11 | position: "relative",
12 | height: "100%",
13 | scrollBehavior: "smooth",
14 | [theme.breakpoints.down("sm")]: {
15 | scrollBehavior: "auto",
16 | },
17 |
18 | ".smallScreen &": {
19 | scrollSnapType: "none",
20 | },
21 | },
22 | smallScreen: {},
23 |
24 | scrollPage: {
25 | display: "flex",
26 | flexDirection: "column",
27 | placeContent: "stretch",
28 | position: "relative",
29 | scrollSnapAlign: "start",
30 | overflowY: "hidden",
31 | },
32 |
33 | wrapper: {
34 | zIndex: 2,
35 | display: "flex",
36 | placeContent: "center",
37 | position: "relative",
38 | width: "100%",
39 | overflowY: "hidden",
40 | },
41 |
42 | limitScreen: {
43 | height: "100%",
44 | boxSizing: "border-box",
45 | overflowX: "hidden",
46 | },
47 | }));
48 |
49 | export default function ScrollableView(props) {
50 | const { children } = props;
51 | const classes = useStyles();
52 | return (
53 |
54 | {children}
55 |
56 | );
57 | }
58 |
59 | export function ScrollPage({
60 | children,
61 | limit,
62 | parentStyle,
63 | style,
64 | className,
65 | ...props
66 | }) {
67 | const classes = useStyles();
68 |
69 | // XXX hack for 100% screen scrollable size (flex 100% doesnt work well for all browsers)
70 | const windowSize = useWindowSize();
71 | const isBigScreen = useBigScreen();
72 | const minHeight = limit
73 | ? windowSize.height - (isBigScreen ? 55 : 48)
74 | : "initial";
75 |
76 | return (
77 |
81 |
90 | {children}
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/src/pages/Main/PersonProfile/PersonQuotes.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Typography from "@material-ui/core/Typography";
3 |
4 | import Loader from "../../../components/ChatLoader";
5 | import Chat from "../../../components/Chat";
6 | import config from "../../../config";
7 | import { useQuery, useCancellableFetch } from "../../../utils";
8 | import { WhiteQuotesSearch } from "../../../components/QuotesSearch";
9 |
10 | const PersonQuotes = React.memo(function ({ personID }) {
11 | const [loading, setLoading] = useState(false);
12 |
13 | return (
14 | <>
15 |
16 |
20 |
21 |
22 |
27 | >
28 | );
29 | });
30 | export default PersonQuotes;
31 |
32 | const QuoteView = React.memo(function ({ personID, loading, setLoading }) {
33 | const [data, setData] = useState([]);
34 | const query = useQuery();
35 | const serverFetch = useCancellableFetch();
36 |
37 | useEffect(() => {
38 | (async () => {
39 | if (!personID) return;
40 | setLoading(true);
41 | setData([]);
42 | try {
43 | const res = await serverFetch(
44 | `${config.server}/PersonQuotes?keyword=${query}&PersonID=${personID}`
45 | );
46 | setData(res);
47 | } catch (e) {
48 | // TODO handle errors
49 | console.error(e);
50 | setData([]);
51 | } finally {
52 | setLoading(false);
53 | }
54 | })();
55 | }, [query, personID, setLoading, serverFetch]);
56 |
57 | if (!personID || loading) return null;
58 |
59 | if (!data.length)
60 | return (
61 |
70 | לא נמצאו ציטוטים שלי{query?.length ? ` על ${query}` : ""}
71 |
72 | );
73 |
74 | return (
75 | ({
77 | highlight: query,
78 | ...d,
79 | }))}
80 | />
81 | );
82 | });
83 |
--------------------------------------------------------------------------------
/src/components/Chat/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Highlighter from "react-highlight-words";
3 | import clsx from "clsx";
4 | import "./index.css";
5 | import { ScrollIntoView } from "rrc";
6 | import { imageOrDefault, useIndex } from "../../utils";
7 | import QuoteFooter from "../QuoteFooter";
8 |
9 | const ColorHash = require("color-hash").default;
10 | const colorHash = new ColorHash({ lightness: 0.4 });
11 |
12 | export default function Chat({ items }) {
13 | const index = useIndex();
14 | return (
15 |
16 |
17 | {items.map((i) => (
18 |
19 | ))}
20 |
21 |
22 | );
23 | }
24 |
25 | function ChatItem(props) {
26 | const {
27 | Index,
28 | Text,
29 | imgPath,
30 | Speaker,
31 | isSpeaker,
32 | isContinuation,
33 | highlight,
34 | isInProtocol,
35 | } = props;
36 |
37 | return (
38 |
46 |
57 |
58 | {Speaker && (
59 |
65 | {Speaker}
66 |
67 | )}
68 |
69 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # Logs
26 | logs
27 | *.log
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | lerna-debug.log*
32 |
33 | # Diagnostic reports (https://nodejs.org/api/report.html)
34 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
35 |
36 | # Runtime data
37 | pids
38 | *.pid
39 | *.seed
40 | *.pid.lock
41 |
42 | # Directory for instrumented libs generated by jscoverage/JSCover
43 | lib-cov
44 |
45 | # Coverage directory used by tools like istanbul
46 | coverage
47 | *.lcov
48 |
49 | # nyc test coverage
50 | .nyc_output
51 |
52 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
53 | .grunt
54 |
55 | # Bower dependency directory (https://bower.io/)
56 | bower_components
57 |
58 | # node-waf configuration
59 | .lock-wscript
60 |
61 | # Compiled binary addons (https://nodejs.org/api/addons.html)
62 | build/Release
63 |
64 | # Dependency directories
65 | node_modules/
66 | jspm_packages/
67 |
68 | # Snowpack dependency directory (https://snowpack.dev/)
69 | web_modules/
70 |
71 | # TypeScript cache
72 | *.tsbuildinfo
73 |
74 | # Optional npm cache directory
75 | .npm
76 |
77 | # Optional eslint cache
78 | .eslintcache
79 |
80 | # Microbundle cache
81 | .rpt2_cache/
82 | .rts2_cache_cjs/
83 | .rts2_cache_es/
84 | .rts2_cache_umd/
85 |
86 | # Optional REPL history
87 | .node_repl_history
88 |
89 | # Output of 'npm pack'
90 | *.tgz
91 |
92 | # Yarn Integrity file
93 | .yarn-integrity
94 |
95 | # dotenv environment variables file
96 | .env
97 | .env.test
98 |
99 | # parcel-bundler cache (https://parceljs.org/)
100 | .cache
101 | .parcel-cache
102 |
103 | # Next.js build output
104 | .next
105 | out
106 |
107 | # Nuxt.js build / generate output
108 | .nuxt
109 | dist
110 |
111 | # Gatsby files
112 | .cache/
113 | # Comment in the public line in if your project uses Gatsby and not Next.js
114 | # https://nextjs.org/blog/next-9-1#public-directory-support
115 | # public
116 |
117 | # vuepress build output
118 | .vuepress/dist
119 |
120 | # Serverless directories
121 | .serverless/
122 |
123 | # FuseBox cache
124 | .fusebox/
125 |
126 | # DynamoDB Local files
127 | .dynamodb/
128 |
129 | # TernJS port file
130 | .tern-port
131 |
132 | # Stores VSCode versions used for testing VSCode extensions
133 | .vscode-test
134 |
135 | # yarn v2
136 | .yarn/cache
137 | .yarn/unplugged
138 | .yarn/build-state.yml
139 | .yarn/install-state.gz
140 | .pnp.*
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
21 |
22 |
31 |
32 | בטא מחוקקים
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 | You need to enable JavaScript to run this app.
61 |
62 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/pages/Main/PersonProfile/index.css:
--------------------------------------------------------------------------------
1 | .person-grid-container {
2 | display: grid;
3 | grid-template-columns: 0.01fr 0.8fr 1.1fr 1.1fr;
4 | grid-template-rows: 0.01fr 1fr 1fr 1fr 0.01fr;
5 | gap: 1% 1%;
6 | grid-template-areas:
7 | "gap-top gap-top gap-top gap-top"
8 | "gap-side info mid-content side-content-top"
9 | "gap-side mini-content mid-content side-content"
10 | "gap-side mini-content mid-content side-content"
11 | "gap-bottom gap-bottom mid-content side-content";
12 | }
13 |
14 | .info {
15 | grid-area: info;
16 | }
17 |
18 | .mini-content {
19 | grid-area: mini-content;
20 | }
21 |
22 | .mid-content {
23 | grid-area: mid-content;
24 | }
25 |
26 | .side-content {
27 | grid-area: side-content;
28 | }
29 |
30 | .side-content-top {
31 | grid-area: side-content-top;
32 | }
33 |
34 | @keyframes float {
35 | 0% {
36 | box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6);
37 | transform: translatey(0px);
38 | }
39 | 50% {
40 | box-shadow: 0 25px 15px 0px rgba(0, 0, 0, 0.2);
41 | transform: translatey(-5px);
42 | }
43 | 100% {
44 | box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6);
45 | transform: translatey(0px);
46 | }
47 | }
48 |
49 | .info {
50 | width: 100%;
51 | height: 100%;
52 | display: flex;
53 | flex-direction: column;
54 | justify-content: flex-start;
55 | align-items: center;
56 | }
57 |
58 | .person-avatar {
59 | width: 150px;
60 | height: 150px;
61 | box-sizing: border-box;
62 | border: 5px white solid;
63 | border-radius: 50%;
64 | margin-bottom: 1em;
65 | overflow: hidden;
66 | box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6);
67 | transform: translatey(0px);
68 | animation: float 6s ease-in-out infinite;
69 | }
70 |
71 | .side-content-top {
72 | position: relative;
73 | width: 100%;
74 | height: 100%;
75 | display: flex;
76 | flex-direction: column;
77 | place-content: center;
78 | }
79 |
80 | .mini-content {
81 | position: relative;
82 | place-items: center;
83 | width: 100%;
84 | height: 100%;
85 | display: flex;
86 | place-content: stretch center;
87 | }
88 |
89 | .content-wrapper {
90 | direction: ltr;
91 | overflow: auto;
92 | height: 100%;
93 | }
94 |
95 | .content-wrapper .content-rtl {
96 | direction: rtl;
97 | width: "100%";
98 | display: flex;
99 | flex-direction: row;
100 | flex-wrap: wrap;
101 | justify-content: center;
102 | align-items: center;
103 | }
104 |
105 | .content-wrapper::-webkit-scrollbar {
106 | width: 12px;
107 | height: 16px;
108 | }
109 | .content-wrapper::-webkit-scrollbar-thumb {
110 | background: linear-gradient(52deg, #ebf0ff 38%, #ffffff 64%);
111 | border-radius: 8px;
112 | }
113 | .content-wrapper::-webkit-scrollbar-thumb:hover {
114 | background: linear-gradient(52deg, #c4cbff 38%, #dbe0ff 64%);
115 | }
116 |
117 | .person-quotes-search {
118 | padding: 0 50px 0 0.5em;
119 | width: 100%;
120 | }
121 |
122 | @media screen and (max-width: 600px), screen and (max-height: 400px) {
123 | .person-grid-container {
124 | display: block;
125 | overflow-x: hidden;
126 | overflow-y: auto;
127 | padding: 0.5em;
128 | }
129 | .info,
130 | .side-content-top,
131 | .mini-content {
132 | height: auto;
133 | }
134 |
135 | .person-quotes-search {
136 | padding: 1em 0.5em 0;
137 | }
138 |
139 | .mid-content .chat {
140 | background-color: #ffffff11;
141 | max-height: 70vh;
142 | margin: 0 0.5em 2em;
143 | overflow-y: auto;
144 | border-radius: 0.5em;
145 | border-top-left-radius: 0;
146 | border-top-right-radius: 0;
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/components/PersonSearch.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useLayoutEffect } from "react";
2 | import TextField from "@material-ui/core/TextField";
3 | import InputAdornment from "@material-ui/core/InputAdornment";
4 | import PersonIcon from "@material-ui/icons/Person";
5 | import ClearIcon from "@material-ui/icons/Clear";
6 | import IconButton from "@material-ui/core/IconButton";
7 | import Autocomplete from "@material-ui/lab/Autocomplete";
8 | import { usePersonID, useNavigate, getFullName } from "../utils";
9 |
10 | const PersonSearch = React.memo(function ({ persons, style, variant }) {
11 | const navigate = useNavigate();
12 | const personID = usePersonID();
13 | const [inputValue, setInputValue] = useState("");
14 |
15 | useLayoutEffect(() => {
16 | if (personID) setInputValue(getFullName(persons[personID]));
17 | }, [persons, personID]);
18 |
19 | return (
20 |
21 |
27 |
getFullName(persons[id])}
30 | getOptionSelected={(id) => parseInt(id) === personID}
31 | value={personID || 0}
32 | onChange={(event, newValue) => {
33 | navigate({ personID: newValue });
34 | }}
35 | inputValue={inputValue}
36 | onInputChange={(event, newInputValue) => {
37 | setInputValue(newInputValue);
38 | }}
39 | style={{ flexGrow: 1 }}
40 | renderInput={(params) => (
41 |
50 |
57 |
58 | ),
59 | endAdornment: (
60 |
61 |
63 | navigate({ personID: null })
64 | }
65 | disabled={personID == null}
66 | >
67 |
74 |
75 |
76 | ),
77 | }}
78 | />
79 | )}
80 | forcePopupIcon={false}
81 | disableClearable={true}
82 | noOptionsText={"אין תוצאות"}
83 | />
84 |
85 |
86 | );
87 | });
88 |
89 | export default PersonSearch;
90 |
--------------------------------------------------------------------------------
/src/components/WordCloud/index.js:
--------------------------------------------------------------------------------
1 | import React, { lazy, Suspense, useState, useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 |
4 | import Explainer from "../Explainer";
5 | import config from "../../config";
6 | import { useBigScreen, useCancellableFetch, usePersonID } from "../../utils";
7 |
8 | const ReactWordcloud = lazy(() => import("react-wordcloud"));
9 |
10 | const options = {
11 | colors: ["#40b2e6", "#53b4e0", "#4f91ab", "#8aaebd", "#bad3de", "#d8e3e8"],
12 | rotations: 3,
13 | rotationAngles: [-5, 5],
14 | fontSizes: [20, 75],
15 | fontFamily: "'Secular One', sans-serif",
16 | fontWeight: "600",
17 | padding: 3,
18 | enableOptimizations: true,
19 | enableTooltip: false,
20 | deterministic: true,
21 | };
22 | const minSize = [200, 300];
23 |
24 | export default React.memo(function WordCloud() {
25 | const personID = usePersonID();
26 | return ;
27 | });
28 |
29 | const CachedWordCloud = React.memo(({ personID }) => {
30 | const isBigScreen = useBigScreen();
31 | const [loading, setLoading] = useState(false);
32 | const [data, setData] = useState(null);
33 | const serverFetch = useCancellableFetch();
34 |
35 | useEffect(() => {
36 | if (!personID) return;
37 | setLoading(true);
38 | // FIXME this is a hack to allow the page to finish scrolling before loading the word cloud.
39 | //. the moment it loads, the smooth scroll effect stops - which causes the page to stop in the middle
40 | setTimeout(
41 | async () => {
42 | const res = await serverFetch(
43 | `${config.server}/WordCloud?personId=${personID}`
44 | );
45 | if (res.length) {
46 | setData(JSON.parse(res[0].WordCloud));
47 | } else {
48 | setData(null);
49 | }
50 | setLoading(false);
51 | },
52 | isBigScreen ? 200 : 0
53 | );
54 | }, [personID, serverFetch, isBigScreen]);
55 |
56 | if (loading) {
57 | return ;
58 | }
59 | if (!data) return null;
60 |
61 | options.transitionDuration = isBigScreen ? 400 : 0;
62 |
63 | return (
64 | }>
65 | {data && (
66 | <>
67 |
68 |
78 | >
79 | )}
80 |
81 | );
82 | });
83 |
84 | function WordCloudExplainer(props) {
85 | return (
86 |
87 |
88 | ענן מילים זה מבוסס על אוסף כל הציטוטים של הח"כ כפי שזיהינו אותם
89 | במערכת שלנו.
90 |
91 |
92 | בעזרת מערכת בינה מלאכותית לעיבוד שפה טבעית{" "}
93 |
94 |
99 | https://hebrew-nlp.co.il
100 |
101 |
102 |
103 |
104 | ביצענו "נורמליזציה" לאוצר המילים של הח"כ, כך מילים בעלות משמעות
105 | זהה התקבלו בכתיב אחיד, למשל "בטחון", "הבטחון", "לבטחון",
106 | "בבטחון" כולן עברו נורמליזציה למילה "בטחון".
107 |
108 |
109 | כמו כן מענן מילים זה מחקנו מונחים רבים כגון מילות קישור או מילים
110 | חסרות הקשר.
111 |
112 |
113 | לאחר עיבוד זה ספרנו את שכיחות השימוש במונחים, ככל שהח"כ משתמש\ת
114 | יותר במילה - כך המילה תופיע גדולה יותר בענן המילים.
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/QuotesLoader/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useMemo } from "react";
2 | import TextTransition, { presets } from "react-text-transition";
3 | import CircularProgress from "@material-ui/core/CircularProgress";
4 |
5 | import quotes from "./quotes.json";
6 | import { useBigScreen, shuffleArray } from "../../utils";
7 | import { makeStyles } from "@material-ui/core";
8 |
9 | // initial shuffle to avoid seeding first render
10 | shuffleArray(quotes);
11 |
12 | const useStyles = makeStyles({
13 | root: {
14 | display: "flex",
15 | flexDirection: "column",
16 | justifyItems: "center",
17 | alignItems: "center",
18 | justifyContent: "center",
19 | position: "absolute",
20 | width: "100%",
21 | height: "100vh",
22 | top: 0,
23 | right: 0,
24 | background: "white",
25 | zIndex: 5,
26 | transition: "opacity .2s ease-in-out",
27 | pointerEvents: "none",
28 | },
29 | quote: {
30 | fontStyle: "italic",
31 | minWidth: "240px",
32 | "$bigLoader &": {
33 | marginTop: "-25px",
34 | marginRight: "2.5em",
35 | },
36 | "$smallLoader &": {
37 | width: "100%",
38 | },
39 | },
40 | text: {
41 | color: "#332",
42 | fontSize: "24px",
43 | fontWeight: "bold",
44 | "&::first-letter": {
45 | fontSize: "200%",
46 | lineHeight: "60px",
47 | },
48 | "$smallLoader &": {
49 | fontSize: "20px",
50 | },
51 | },
52 | name: {
53 | color: "#555",
54 | fontSize: "18px",
55 | fontFamily: "Helvetica, Verdana",
56 | paddingRight: "20px",
57 | "$smallLoader &": {
58 | fontSize: "15px",
59 | paddingRight: 0,
60 | },
61 | },
62 |
63 | smallLoader: {
64 | display: "flex",
65 | flexDirection: "column",
66 | alignContent: "center",
67 | justifyContent: "flex-start",
68 | alignItems: "stretch",
69 | justifyItems: "center",
70 | width: "100%",
71 | height: "30%",
72 | // XXX hacky, but the transition library sucks, so whaterver - OP
73 | "& .text-transition_inner > div": {
74 | margin: "0 7.5%",
75 | width: "85%",
76 | },
77 | },
78 | bigLoader: {
79 | display: "inline-flex",
80 | flexDirection: "row",
81 | placeContent: "center",
82 | placeItems: "center",
83 | width: "100%",
84 | paddingLeft: "25%",
85 | paddingBottom: "15%",
86 | },
87 | });
88 |
89 | const Loader = React.memo(function ({ show }) {
90 | const classes = useStyles();
91 | const isBigScreen = useBigScreen();
92 | const shuffled = useMemo(() => [...quotes], []);
93 | const [index, setIndex] = useState(0);
94 | const intervalId = useRef(null);
95 |
96 | useEffect(() => {
97 | if (show) {
98 | shuffleArray(shuffled);
99 | intervalId.current = setInterval(
100 | () => setIndex((index) => index + 1),
101 | 4000
102 | );
103 | } else {
104 | clearInterval(intervalId.current);
105 | }
106 | }, [show, shuffled]);
107 |
108 | const currentQuote = shuffled[index % shuffled.length];
109 |
110 | const contents = isBigScreen ? (
111 | <>
112 |
113 | }
115 | springConfig={presets.wobbly}
116 | />
117 | >
118 | ) : (
119 | <>
120 |
121 |
122 |
123 |
124 | }
126 | springConfig={presets.wobbly}
127 | />
128 |
129 |
130 | >
131 | );
132 |
133 | return (
134 |
135 |
140 | {contents}
141 |
142 |
143 | );
144 | });
145 |
146 | const Quote = React.memo(function ({ name, quote }) {
147 | const classes = useStyles();
148 | return (
149 |
150 |
{quote}
151 |
{name}
152 |
153 | );
154 | });
155 |
156 | export default Loader;
157 |
--------------------------------------------------------------------------------
/src/pages/Main/Search/Histogram.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Bar } from "react-chartjs-2";
3 | import { useQuery, useCancellableFetch } from "../../../utils";
4 |
5 | import config from "../../../config";
6 |
7 | const CachedHistogramView = React.memo(function HistogramView({ query }) {
8 | const [loading, setLoading] = useState(false);
9 | const [data, setData] = useState([]);
10 | const serverFetch = useCancellableFetch();
11 |
12 | useEffect(() => {
13 | (async () => {
14 | setLoading(true);
15 | setData([]);
16 | try {
17 | const res = await serverFetch(
18 | `${config.server}/KeywordTrend?keyword=${query}`
19 | );
20 | setData(res);
21 | } catch (e) {
22 | console.error(e);
23 | } finally {
24 | setLoading(false);
25 | }
26 | })();
27 | }, [setLoading, query, serverFetch]);
28 |
29 | if (loading) {
30 | return null;
31 | }
32 |
33 | if (!data.length || data.every((d) => !d.counter)) return null;
34 |
35 | const dataFn = (canvas) => {
36 | const ctx = canvas.getContext("2d");
37 | const gradient = ctx.createLinearGradient(0, 0, 0, 140);
38 | gradient.addColorStop(0, "rgba(0,100,255,.6)");
39 | gradient.addColorStop(1, "rgba(0,100,255,.2)");
40 |
41 | return {
42 | labels: data.map(
43 | (d) =>
44 | `${d.month}.${(parseInt(d.year) % 100)
45 | .toString()
46 | .padStart(2, "0")}`
47 | ),
48 | datasets: [
49 | {
50 | label: "trend",
51 | type: "line",
52 | borderColor: "rgb(54, 162, 235)",
53 | borderWidth: 2,
54 | pointRadius: 2,
55 | pointRadiusHover: 10,
56 | fill: true,
57 | data: data.map((d) => d.counter),
58 | backgroundColor: gradient,
59 | },
60 | {
61 | label: "bar",
62 | type: "bar",
63 | backgroundColor: "transparent",
64 | data: data.map((d) => d.counter),
65 | },
66 | ],
67 | };
68 | };
69 |
70 | const options = {
71 | responsive: true,
72 | maintainAspectRatio: false,
73 |
74 | onClick: function (evt, element) {
75 | if (!element.length) return;
76 | const { month, year, counter } = data[element[0]._index];
77 | if (!counter) return;
78 |
79 | // TODO
80 | console.log(month, year);
81 | },
82 | layout: {
83 | padding: {
84 | top: 5,
85 | },
86 | },
87 | datasets: {
88 | barPercentage: 0.95,
89 | },
90 | scales: {
91 | xAxes: [
92 | {
93 | gridLines: {
94 | display: false,
95 | },
96 | ticks: {
97 | fontColor: "#444", // this here
98 | },
99 | },
100 | ],
101 | yAxes: [
102 | {
103 | display: false,
104 | gridLines: {
105 | display: false,
106 | },
107 | },
108 | ],
109 | },
110 | legend: {
111 | display: false,
112 | },
113 | tooltips: {
114 | rtl: true,
115 | custom: function (tooltip) {
116 | if (!tooltip) return;
117 | // disable displaying the color box;
118 | tooltip.displayColors = false;
119 | },
120 | callbacks: {
121 | label: function (tooltipItem, data) {
122 | return `${tooltipItem.yLabel} תוצאות`;
123 | },
124 | },
125 | },
126 |
127 | plugins: {
128 | // important - disabling chartjs-plugin-datalabels stupid global override
129 | datalabels: {
130 | formatter: () => "",
131 | },
132 | },
133 | };
134 |
135 | return (
136 |
145 |
146 |
147 | );
148 | });
149 |
150 | export default React.memo(function HistogramView() {
151 | const query = useQuery();
152 |
153 | return ;
154 | });
155 |
--------------------------------------------------------------------------------
/src/components/NavigationBar/ContactUs.js:
--------------------------------------------------------------------------------
1 | import Dialog from "../Dialog";
2 | import { Typography } from "@material-ui/core";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Avatar from "@material-ui/core/Avatar";
5 | import IconButton from "@material-ui/core/IconButton";
6 |
7 | import YoavImage from "../../static/images/Yoav.jpg";
8 | import OmerImage from "../../static/images/Omer.jpg";
9 | import OrImage from "../../static/images/Or.jpg";
10 | import ShaniImage from "../../static/images/Shani.jpg";
11 |
12 | const useStyles = makeStyles((theme) => ({
13 | root: {
14 | display: "flex",
15 | "& > *": {
16 | margin: theme.spacing(1),
17 | },
18 | placeContent: "center",
19 | },
20 | medium: {
21 | width: theme.spacing(7),
22 | height: theme.spacing(7),
23 | },
24 | small: {
25 | width: theme.spacing(5),
26 | height: theme.spacing(5),
27 | },
28 | }));
29 |
30 | export default function ContactUsDialog(props) {
31 | const classes = useStyles();
32 |
33 | return (
34 |
35 |
36 | צרו קשר
37 |
38 |
39 | פיתוח המערכת
40 |
41 |
42 |
51 |
58 |
59 |
60 | אם יש לכם רעיון מגניב ,
61 |
62 |
63 | אם מצאתם טעות מחרידה ,
64 |
65 |
66 |
67 | אם אתם רוצים להוסיף אפליקציה
68 |
69 |
70 | כמו{" "}
71 |
72 |
77 | סימולטור ההצבעות
78 |
79 | {" "}
80 | שיצרו
81 |
82 |
83 |
84 |
93 |
102 |
103 |
104 | אתם מוזמנים לפנות אלינו בכתובת
105 |
106 |
107 |
108 | betaknesset@gmail.com
109 |
110 |
111 |
112 | );
113 | }
114 |
115 | function NamedAvatar({ classStyle, imageUrl, linkedinUrl, EngName, HebName }) {
116 | return (
117 |
134 | );
135 | }
136 |
--------------------------------------------------------------------------------
/src/pages/Main/PersonProfile/PersonBillsStats/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Doughnut } from "react-chartjs-2";
3 | import "chartjs-plugin-datalabels";
4 | import Explainer from "../../../../components/Explainer";
5 | import config from "../../../../config";
6 | import { useCancellableFetch } from "../../../../utils";
7 |
8 | export default React.memo(function PersonBillsStats({
9 | personID,
10 | filter,
11 | setFilter,
12 | }) {
13 | const [data, setData] = useState([]);
14 | const serverFetch = useCancellableFetch();
15 |
16 | useEffect(() => {
17 | setFilter(null);
18 | if (!personID) return;
19 | (async () => {
20 | const res = await serverFetch(
21 | `${config.server}/PersonBillsStats?personId=${personID}`
22 | );
23 | setData(res);
24 | })();
25 | }, [personID, setFilter, serverFetch]);
26 |
27 | const total = data?.map((d) => d.Counter).reduce((a, b) => a + b, 0);
28 | if (total === 0) return null;
29 |
30 | const doughnutData = {
31 | labels: data.map((d) => d.Desc),
32 | datasets: [
33 | {
34 | data: data.map((d) => d.Counter),
35 | backgroundColor: data.map((x) =>
36 | getStatusColor(x.Desc, filter)
37 | ),
38 | hoverBackgroundColor: data.map((x) =>
39 | getStatusColorHover(x.Desc)
40 | ),
41 | borderColor: data.map((x) =>
42 | filter === x.Desc ? "white" : getStatusColor(x.Desc, filter)
43 | ),
44 | hoverBorderColor: data.map((x) =>
45 | filter === x.Desc
46 | ? "white"
47 | : getStatusColorHover(x.Desc, filter)
48 | ),
49 | borderWidth: data.map((x) => (filter === x.Desc ? 5 : 0)),
50 | hoverBorderWidth: 5,
51 | },
52 | ],
53 | };
54 |
55 | const options = {
56 | legend: {
57 | display: false,
58 | position: "right",
59 | },
60 | plugins: {
61 | // Change options for ALL labels of THIS CHART
62 | datalabels: {
63 | color: "white",
64 | textAlign: "center",
65 | formatter: function (value, context) {
66 | if (value / total < 0.05) return "";
67 | return context.chart.data.labels[context.dataIndex];
68 | },
69 | },
70 | },
71 | onClick: function (evt, element) {
72 | if (element[0] && data[element[0]._index].Desc) {
73 | if (filter === data[element[0]._index].Desc) setFilter(null);
74 | else setFilter(data[element[0]._index].Desc);
75 | } else setFilter(null);
76 | },
77 | };
78 |
79 | return (
80 | <>
81 |
82 |
97 |
{total}
98 |
הצעות חוק
99 |
100 |
101 | >
102 | );
103 | });
104 |
105 | function getStatusColor(status, filter) {
106 | if (status === filter) return getStatusColorHover(status);
107 | switch (status) {
108 | case "בתהליך":
109 | return "#FF9900";
110 | case "אושרה":
111 | return "#109618";
112 | case "נעצרה":
113 | return "#DC3912";
114 | default:
115 | return "#3366CC";
116 | }
117 | }
118 | function getStatusColorHover(status) {
119 | switch (status) {
120 | case "בתהליך":
121 | return "#FFaa22";
122 | case "אושרה":
123 | return "#30a638";
124 | case "נעצרה":
125 | return "#fC5932";
126 | default:
127 | return "#5386eC";
128 | }
129 | }
130 |
131 | function BillExplainer(props) {
132 | return (
133 |
134 | ח"כים יכולים גם ליזום הצעות חוק וגם להצטרף להצעות חוק.
135 |
136 | ברשימה לפניכם אנו מציגים עבור כל ח"כ את רשימת החוקים אותם יזם
137 | באופן ישיר בתור "יוזם ראשון".
138 |
139 |
140 | פרטים נוספים על אודות שלבי החקיקה והליך החקיקה{" "}
141 |
142 |
147 | תוכלו למצוא בקישור זה
148 |
149 |
150 |
151 |
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/src/pages/Quotes.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Typography from "@material-ui/core/Typography";
3 |
4 | import { ScrollPage } from "../components/ScrollableView";
5 | import Loader from "../components/ChatLoader";
6 | import Chat from "../components/Chat";
7 | import { WhiteQuotesSearch } from "../components/QuotesSearch";
8 | import config from "../config";
9 | import { useBigScreen, useCancellableFetch, useQuery } from "../utils";
10 | import defaultTopics from "../defaultTopics";
11 |
12 | const Quotes = React.memo(function () {
13 | const [loading, setLoading] = useState(false);
14 | const randomTopic =
15 | defaultTopics[Math.floor(Math.random() * defaultTopics.length)];
16 | const isBigScreen = useBigScreen();
17 | const prefix = isBigScreen ? "כל נושא שהוא, " : "";
18 | const serverFetch = useCancellableFetch();
19 | const [data, setData] = useState({});
20 | const query = useQuery();
21 |
22 | const cleanQuery = query.replace("״", '"').replace("׳", "'");
23 |
24 | useEffect(() => {
25 | (async () => {
26 | setData({});
27 | if (!cleanQuery.length) return;
28 | setLoading(true);
29 | try {
30 | const res = await serverFetch(
31 | `${config.server}/Quotes?keyword=${cleanQuery}`
32 | );
33 | setData(res);
34 | } catch (e) {
35 | // TODO handle errors - (ex: when multi term search is empty)
36 | console.error(e);
37 | setData({});
38 | } finally {
39 | setLoading(false);
40 | }
41 | })();
42 | }, [cleanQuery, serverFetch]);
43 |
44 | return (
45 |
50 |
58 |
67 |
74 |
75 |
78 | {data.count > 0 && (
79 |
82 | {data.count === 1
83 | ? "תוצאה אחת"
84 | : `${numberWithCommas(
85 | data.count
86 | )} תוצאות`}
87 | {data.count > data.quotes.length &&
88 | ` (מציג ${data.quotes.length} אחרונות)`}
89 |
90 | )}
91 |
92 |
93 |
101 |
102 |
103 | {!loading && query.length && data.count === 0 ? (
104 | <>
105 |
114 | לא נמצאו ציטוטים בנושא {query}
115 |
116 | >
117 | ) : null}
118 | {!loading && data.count > 0 && (
119 |
120 | )}
121 |
122 |
123 |
124 |
125 |
126 | );
127 | });
128 | export default Quotes;
129 |
130 | const QuoteView = React.memo(function ({ query, quotes }) {
131 | return (
132 | ({
134 | highlight: query,
135 | ...d,
136 | }))}
137 | />
138 | );
139 | });
140 |
141 | function numberWithCommas(x) {
142 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
143 | }
144 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy, useEffect } from "react";
2 | import clsx from "clsx";
3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
4 | import { CircularProgress, ThemeProvider } from "@material-ui/core";
5 | import Particles from "react-particles";
6 | import { makeStyles } from "@material-ui/core/styles";
7 | import Typography from "@material-ui/core/Typography";
8 | import Modal from "@material-ui/core/Modal";
9 | import Fade from "@material-ui/core/Fade";
10 | import Backdrop from "@material-ui/core/Backdrop";
11 | import Container from "@material-ui/core/Container";
12 |
13 | import config from "./config";
14 | import theme from "./theme";
15 | import { useBigScreen } from "./utils";
16 | import particlesConfig from "./particles.config.json";
17 | import NavigationBar from "./components/NavigationBar";
18 | import ScrollableView from "./components/ScrollableView";
19 |
20 | const Main = lazy(() => import("./pages/Main"));
21 | const Document = lazy(() => import("./pages/Document"));
22 | const Quotes = lazy(() => import("./pages/Quotes"));
23 | const Calendar = lazy(() => import("./pages/Calendar"));
24 | const FrameApp = lazy(() => import("./pages/FrameApp"));
25 |
26 | const useStyles = makeStyles((theme) => ({
27 | root: {
28 | display: "flex",
29 | flexDirection: "column",
30 | placeItems: "stretch",
31 | overflowY: "auto",
32 | },
33 | particles: {
34 | position: "fixed",
35 | width: "100%",
36 | top: 0,
37 | left: 0,
38 | zIndex: 1,
39 | },
40 | modal: {
41 | display: "flex",
42 | alignItems: "center",
43 | justifyContent: "center",
44 | position: "relative",
45 | color: "#fff",
46 | },
47 | backdrop: {
48 | backgroundColor: theme.palette.primary.dark,
49 | },
50 | container: {
51 | outline: "none",
52 | padding: theme.spacing(2, 4, 3),
53 | textAlign: "center",
54 | display: "flex",
55 | flexDirection: "column",
56 | placeItems: "center",
57 | placeContent: "space-around",
58 | height: "100%",
59 | },
60 | }));
61 |
62 | export default function App() {
63 | const isBigScreen = useBigScreen();
64 | const classes = useStyles();
65 |
66 | if (config.showOverloadedScreen)
67 | return (
68 |
69 |
70 |
71 | );
72 |
73 | return (
74 |
75 |
81 |
82 |
83 |
84 |
85 | }>
86 |
87 |
88 | (
91 |
92 | )}
93 | />
94 | (
97 |
98 | )}
99 | />
100 | }
103 | />
104 | }
107 | />
108 |
109 | {config.apps.map((app) => (
110 | (
114 |
118 | )}
119 | />
120 | ))}
121 |
122 |
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
130 | function LightweightLoader() {
131 | return (
132 |
141 |
142 |
143 | );
144 | }
145 |
146 | function CustomParticles() {
147 | const isBigScreen = useBigScreen();
148 | const classes = useStyles();
149 |
150 | if (!isBigScreen) return null;
151 | return ;
152 | }
153 |
154 | function OverloadScreen() {
155 | const classes = useStyles();
156 |
157 | // refresh automatically
158 | useEffect(() => {
159 | setTimeout(() => (window.location.href = window.location), 30000);
160 | }, []);
161 |
162 | return (
163 | void 0}
167 | closeAfterTransition
168 | BackdropComponent={Backdrop}
169 | BackdropProps={{
170 | timeout: 500,
171 | className: classes.backdrop,
172 | }}
173 | >
174 |
175 |
176 |
177 | איזה עומס!
178 |
179 |
180 |
181 |
182 | לא ציפינו לכזו כמות של תעבורה,
183 |
184 |
185 | נחזור במהרה יותר מוכנים לכמות המשתמשים 😋
186 |
187 |
188 |
189 |
190 |
191 | );
192 | }
193 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState, useEffect, useRef } from "react";
2 | import { useHistory, useLocation } from "react-router-dom";
3 | import useMediaQuery from "@material-ui/core/useMediaQuery";
4 | import axiosRetry from "axios-retry";
5 | import axios from "axios";
6 |
7 | axiosRetry(axios, { retries: 3 });
8 |
9 | /** helpers **/
10 | export const toNiceDate = (d, hours = false) => {
11 | let date = `${d.getDate()}.${d.getMonth() + 1}.${(d.getFullYear() % 100)
12 | .toString()
13 | .padStart(2, "0")}`;
14 | if (hours)
15 | date = `${d.getHours().toString().padStart(2, "0")}:${d
16 | .getMinutes()
17 | .toString()
18 | .padStart(2, "0")}, ${date}`;
19 | return date;
20 | };
21 |
22 | export function cleanTextFromDB(text) {
23 | if (text[0] === "״" || text[0] === '"') {
24 | text = text.substring(1);
25 | }
26 | if (text[text.length - 1] === "״" || text[text.length - 1] === '"') {
27 | text = text.substring(0, text.length - 1);
28 | }
29 |
30 | return text.replace(/"+/g, '"').replace(/״+/g, "״");
31 | }
32 |
33 | export function shuffleArray(array) {
34 | for (let i = array.length - 1; i > 0; i--) {
35 | const j = Math.floor(Math.random() * (i + 1));
36 | [array[i], array[j]] = [array[j], array[i]];
37 | }
38 | }
39 |
40 | export function imageOrDefault(url, identifier, size = 32) {
41 | if (url) return url;
42 |
43 | return `https://www.gravatar.com/avatar/${hashCode(
44 | identifier
45 | )}?s=${size}&d=identicon&r=PG`;
46 | }
47 |
48 | function hashCode(str) {
49 | var hash = 0,
50 | i,
51 | chr;
52 | for (i = 0; i < str.length; i++) {
53 | chr = str.charCodeAt(i);
54 | hash = (hash << 5) - hash + chr;
55 | hash |= 0; // Convert to 32bit integer
56 | }
57 | return hash;
58 | }
59 |
60 | export const getFullName = (p) => {
61 | if (!p) return "";
62 | const { FirstName, LastName } = p;
63 | return `${FirstName} ${LastName}`;
64 | };
65 |
66 | /** hooks **/
67 | export function useSearchParams() {
68 | return new URLSearchParams(useLocation().search);
69 | }
70 |
71 | export function useQuery() {
72 | const urlQuery = useSearchParams();
73 | return (urlQuery.get("q") || "").replace("״", '"').replace("׳", "'");
74 | }
75 |
76 | export function usePersonID() {
77 | const urlQuery = useSearchParams();
78 | const raw = urlQuery.get("personID");
79 | if (!raw) return undefined;
80 | return parseInt(raw);
81 | }
82 |
83 | export function useIndex() {
84 | const urlQuery = useSearchParams();
85 | const raw = urlQuery.get("index");
86 | if (!raw) return undefined;
87 | return parseInt(raw);
88 | }
89 |
90 | export function useNavigate() {
91 | const currentQuery = useQuery();
92 | const currentPersonID = usePersonID();
93 | const currentLocation = useLocation();
94 | const history = useHistory();
95 |
96 | return useCallback(
97 | ({ location, hash, q, personID, ...params }) => {
98 | const urlParams = new URLSearchParams(params);
99 |
100 | q = q !== undefined ? q : currentQuery;
101 | if (q) urlParams.append("q", q);
102 | personID = personID !== undefined ? personID : currentPersonID;
103 | if (personID) urlParams.append("personID", personID);
104 |
105 | history.push(
106 | `${location || currentLocation.pathname}?${urlParams}${
107 | hash || ""
108 | }`
109 | );
110 | },
111 | [currentQuery, currentPersonID, currentLocation, history]
112 | );
113 | }
114 |
115 | async function serverFetch({ url, params }) {
116 | return (
117 | await axios.get(url, {
118 | params,
119 | timeout: 10000,
120 | })
121 | ).data;
122 | }
123 |
124 | export function useWindowSize() {
125 | // Initialize state with undefined width/height so server and client renders match
126 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
127 | const [windowSize, setWindowSize] = useState({
128 | width: window.innerWidth,
129 | height: window.innerHeight,
130 | });
131 |
132 | useEffect(() => {
133 | // Handler to call on window resize
134 | function handleResize() {
135 | // Set window width/height to state
136 | setWindowSize({
137 | width: window.innerWidth,
138 | height: window.innerHeight,
139 | });
140 | }
141 |
142 | // Add event listener
143 | window.addEventListener("resize", handleResize);
144 |
145 | // Call handler right away so state gets updated with initial window size
146 | handleResize();
147 |
148 | // Remove event listener on cleanup
149 | return () => window.removeEventListener("resize", handleResize);
150 | }, []); // Empty array ensures that effect is only run on mount
151 |
152 | return windowSize;
153 | }
154 |
155 | export function useBigScreen() {
156 | return useMediaQuery("(min-width:600px) and (min-height:400px)");
157 | }
158 |
159 | // from https://usehooks.com/useLocalStorage/
160 | export function useSessionStorage(key, initialValue) {
161 | // State to store our value
162 | // Pass initial state function to useState so logic is only executed once
163 | const [storedValue, setStoredValue] = useState(() => {
164 | try {
165 | // Get from session storage by key
166 | const item = window.sessionStorage.getItem(key);
167 | // Parse stored json or if none return initialValue
168 | return item ? JSON.parse(item) : initialValue;
169 | } catch (error) {
170 | // If error also return initialValue
171 | console.log(error);
172 | return initialValue;
173 | }
174 | });
175 |
176 | // Return a wrapped version of useState's setter function that ...
177 | // ... persists the new value to sessionStorage.
178 | const setValue = (value) => {
179 | try {
180 | // Allow value to be a function so we have same API as useState
181 | const valueToStore =
182 | value instanceof Function ? value(storedValue) : value;
183 | // Save state
184 | setStoredValue(valueToStore);
185 | // Save to session storage
186 | window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
187 | } catch (error) {
188 | // A more advanced implementation would handle the error case
189 | console.log(error);
190 | }
191 | };
192 |
193 | return [storedValue, setValue];
194 | }
195 |
196 | export function useCancellableFetch() {
197 | const cancelSource = useRef(null);
198 | return useCallback(async (url) => {
199 | if (cancelSource.current) {
200 | cancelSource.current.cancel("Cancelled by hook");
201 | }
202 | const cancelToken = axios.CancelToken;
203 | cancelSource.current = cancelToken.source();
204 | try {
205 | return await serverFetch({
206 | url,
207 | cancelToken: cancelSource.current.token,
208 | });
209 | } catch (thrown) {
210 | if (axios.isCancel(thrown)) {
211 | console.debug("Request canceled", thrown.message);
212 | } else {
213 | throw thrown;
214 | }
215 | }
216 | }, []);
217 | }
218 |
--------------------------------------------------------------------------------
/src/components/QuotesSearch.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState, useLayoutEffect, useRef } from "react";
2 | import Dialog from "./Dialog";
3 | import { Typography } from "@material-ui/core";
4 | import TextField from "@material-ui/core/TextField";
5 | import SearchIcon from "@material-ui/icons/Search";
6 | import ClearIcon from "@material-ui/icons/Clear";
7 | import InputAdornment from "@material-ui/core/InputAdornment";
8 | import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
9 | import { FormatQuote } from "@material-ui/icons";
10 | import IconButton from "@material-ui/core/IconButton";
11 | import { useQuery, useNavigate } from "../utils";
12 |
13 | const QuotesSearch = React.memo(function ({
14 | style,
15 | variant,
16 | placeholder,
17 | showReset = true,
18 | }) {
19 | const textRef = useRef();
20 | const navigate = useNavigate();
21 | const query = useQuery();
22 | const [queryInput, setQueryInput] = useState(query);
23 | const [guideOpen, setGuideOpen] = useState(false);
24 |
25 | const search = useCallback(
26 | () => navigate({ q: queryInput }),
27 | [queryInput, navigate]
28 | );
29 |
30 | useLayoutEffect(() => {
31 | setQueryInput(query);
32 | }, [query]);
33 |
34 | return (
35 |
36 |
115 |
116 | );
117 | });
118 |
119 | export function SearchDialog(props) {
120 | const { setOpen } = props;
121 | const navigate = useNavigate();
122 |
123 | const showQuotes = (e, q) => {
124 | e.preventDefault();
125 | navigate({ q });
126 | setOpen(false);
127 | };
128 |
129 | return (
130 |
131 |
132 | חיפוש מתקדם
133 |
134 |
135 | איך מחפשים מונח?
136 | פשוט מקלידים את המילה ומחפשים, למשל:
137 |
138 |
139 |
140 | showQuotes(e, "פנסיה")}>
141 | פנסיה
142 |
143 |
144 |
145 |
146 | איך מחפשים משפט?
147 | פשוט מקלידים את המשפט ומחפשים, למשל:
148 |
149 |
150 |
151 | showQuotes(e, "ילדי תימן")}>
152 | ילדי תימן
153 |
154 |
155 |
156 |
157 | איך מחפשים כמה מונחים בו זמנית?
158 | פשוט מקלידים את המילים או המשפטים מופרדים עם התו ^, למשל:
159 |
160 |
161 |
162 | showQuotes(e, "בעד^הקהילה הגאה")}
165 | >
166 | בעד^הקהילה הגאה
167 |
168 |
169 |
170 |
171 |
172 | אם עדין אין לכם רעיון מה לחפש, מוזמנים לבחור באחת מבועות הנושאים
173 | שלנו.
174 |
175 |
176 | );
177 | }
178 |
179 | export default QuotesSearch;
180 |
181 | export function WhiteQuotesSearch({ style, ...props }) {
182 | return (
183 |
192 | );
193 | }
194 |
--------------------------------------------------------------------------------
/src/pages/Calendar/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FullCalendar from "@fullcalendar/react";
3 | import heLocale from "@fullcalendar/core/locales/he";
4 | import dayGridPlugin from "@fullcalendar/daygrid";
5 | import timeGridPlugin from "@fullcalendar/timegrid";
6 | import listPlugin from "@fullcalendar/list";
7 | import interactionPlugin from "@fullcalendar/interaction";
8 | import Typography from "@material-ui/core/Typography";
9 |
10 | import { ScrollPage } from "../../components/ScrollableView";
11 | import "./index.css";
12 | import config from "../../config";
13 | import { useBigScreen } from "../../utils";
14 |
15 | const CalendarComponent = React.memo(function ({ loading, ...props }) {
16 | return (
17 |
18 |
19 |
30 |
31 |
0 ? 1 : 0,
41 | zIndex: 10,
42 | bottom: 0,
43 | transition: "opacity .4s ease-in-out",
44 | pointerEvents: "none",
45 | }}
46 | >
47 | טוען...
48 |
49 |
50 | );
51 | });
52 |
53 | class CalendarView extends React.PureComponent {
54 | state = {
55 | currentEvents: {},
56 | loading: 0,
57 | };
58 |
59 | render() {
60 | return (
61 |
78 | );
79 | }
80 |
81 | handleEventClick = (selectInfo) => {
82 | console.debug(selectInfo);
83 | };
84 |
85 | handleEvents = (events) => {
86 | this.setState({
87 | currentEvents: events,
88 | });
89 | };
90 | handleDates(fetchInfo) {
91 | this.setState((s) => ({ loading: s.loading + 1 }));
92 | // TODO copy from https://fullcalendar.io/docs/events-function
93 | fetch(
94 | `${
95 | config.server
96 | }/KnessetSchedule?StartDate=${fetchInfo.start.toISOString()}&FinishDate=${fetchInfo.end.toISOString()}`,
97 | fetchInfo
98 | )
99 | .then((res) => res.json())
100 | .then((data) => {
101 | const events = {};
102 | for (const r of data) {
103 | events[r.result.sessionID] = {
104 | id: r.result.sessionID,
105 | title: r.result.name,
106 | start: r.result.startDate,
107 | end: r.result.finishDate,
108 | items: r.items,
109 | color: r.result.sessionType === 1 ? "purple" : null, // Should be done in render clause
110 | ...r.result,
111 | };
112 | }
113 | this.setState((s) => ({
114 | currentEvents: Object.assign(s.currentEvents, events),
115 | }));
116 | })
117 | .finally(() => this.setState((s) => ({ loading: s.loading - 1 })));
118 | }
119 | }
120 |
121 | function Event(eventInfo) {
122 | const { event } = eventInfo;
123 | const { extendedProps } = event;
124 |
125 | switch (extendedProps.sessionType) {
126 | case 1:
127 | return ;
128 | case 2:
129 | return ;
130 | default:
131 | return <>>;
132 | }
133 | }
134 |
135 | function CommitteeEvent({ event, items, filePath }) {
136 | return (
137 |
139 | window.open(
140 | `https://docs.google.com/gview?url=${filePath}`,
141 | "_blank"
142 | )
143 | }
144 | >
145 | {/*
{eventInfo.timeText} */}
146 | {/* TODO -
{eventInfo.event.title} (need to parse it nicely, maybe in server)*/}
147 |
{event.title}
148 |
149 |
150 | {truncateName(
151 | items
152 | .slice(0, 5)
153 | .map((i) => i.itemName)
154 | .join(", "),
155 | 100
156 | )}{" "}
157 | -{" "}
158 | {items.length === 1 ? "נושא אחד" : `${items.length} נושאים`}
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | function PlenumEvent({ event, items, filePath }) {
166 | return (
167 |
169 | window.open(
170 | `https://docs.google.com/viewer?url=${filePath}`,
171 | "_blank"
172 | )
173 | }
174 | >
175 | {/*
{eventInfo.timeText} */}
176 | {/* TODO -
{eventInfo.event.title} (need to parse it nicely, maybe in server)*/}
177 |
ישיבת מליאה
178 |
179 |
180 | {truncateName(
181 | items
182 | .slice(0, 5)
183 | .map((i) => i.itemName)
184 | .join(", "),
185 | 100
186 | )}{" "}
187 | -{" "}
188 | {items.length === 1 ? "נושא אחד" : `${items.length} נושאים`}
189 |
190 |
191 |
192 | );
193 | }
194 |
195 | class MobileCalendarView extends CalendarView {
196 | render() {
197 | return (
198 |
215 | );
216 | }
217 | }
218 |
219 | function truncateName(n, l) {
220 | if (n.length < l) return n;
221 | return n.substring(0, l) + "...";
222 | }
223 |
224 | export default React.memo(function Calendar() {
225 | const isBigScreen = useBigScreen();
226 | return (
227 |
228 |
243 |
244 | );
245 | });
246 |
--------------------------------------------------------------------------------
/src/components/NavigationBar/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import IconButton from "@material-ui/core/IconButton";
5 | import GitHubIcon from "@material-ui/icons/GitHub";
6 | import MenuIcon from "@material-ui/icons/Menu";
7 | import AppBar from "@material-ui/core/AppBar";
8 | import Divider from "@material-ui/core/Divider";
9 | import Button from "@material-ui/core/Button";
10 | import Tooltip from "@material-ui/core/Tooltip";
11 | import Typography from "@material-ui/core/Typography";
12 | import Drawer from "@material-ui/core/Drawer";
13 | import List from "@material-ui/core/List";
14 | import ListItem from "@material-ui/core/ListItem";
15 | import ListItemText from "@material-ui/core/ListItemText";
16 |
17 | import { useNavigate, useBigScreen } from "../../utils";
18 | import config from "../../config.json";
19 | import AboutDialog from "./About";
20 | import ContactUsDialog from "./ContactUs";
21 | import DisclaimerDialog from "./Disclaimer";
22 |
23 | const useStyles = makeStyles({
24 | title: {
25 | whiteSpace: "nowrap",
26 | fontSize: "2em",
27 | color: "white",
28 | letterSpacing: "-2px",
29 | transition: "all .2s ease-in",
30 | transform: "scale(1)",
31 | "& $bigLetter": {
32 | fontSize: "120%",
33 | },
34 | "&:hover, & a:active": {
35 | transform: "scale(1.03)",
36 | },
37 | },
38 | gridContainer: {
39 | display: "grid",
40 | gridTemplateColumns: "1fr min-content 1fr",
41 | gridTemplateRows: "1fr",
42 | gap: "0% 1%",
43 | gridTemplateAreas: '"nav logo links"',
44 | padding: ".2em 0",
45 | },
46 | nav: {
47 | gridArea: "nav",
48 | justifyItems: "flex-start",
49 | display: "flex",
50 | alignItems: "center",
51 |
52 | "& button, & a": {
53 | fontFamily: '"Varela Round", sans-serif',
54 | fontSize: "120%",
55 | flexGrow: 1,
56 | color: "white",
57 | textDecoration: "none",
58 | borderRadius: 0,
59 | },
60 | },
61 | logo: {
62 | gridArea: "logo",
63 | },
64 | links: {
65 | gridArea: "links",
66 | justifyItems: "flex-end",
67 | display: "flex",
68 | alignItems: "center",
69 |
70 | "& button, & a": {
71 | fontFamily: '"Varela Round", sans-serif',
72 | fontSize: "120%",
73 | flexGrow: 1,
74 | color: "white",
75 | textDecoration: "none",
76 | borderRadius: 0,
77 | },
78 | },
79 |
80 | flexSpacer: {
81 | flexGrow: 12,
82 | },
83 | drawerPaper: {
84 | minWidth: 150,
85 | },
86 |
87 | mobileAppBar: {
88 | display: "flex",
89 | flexDirection: "row",
90 | placeContent: "space-between",
91 | placeItems: "center",
92 | padding: " 0 1em 0 0",
93 | "& $logo": {
94 | fontSize: "85%",
95 | },
96 | },
97 | bigLetter: {},
98 | });
99 |
100 | export default function NavigationBar() {
101 | const [aboutOpen, setAboutOpen] = useState(false);
102 | const [contactUsOpen, setContactUsOpen] = useState(false);
103 | const [disclaimerOpen, setDisclaimerOpen] = useState(false);
104 | const isBigScreen = useBigScreen();
105 |
106 | const nav = [
107 | {
108 | navigate: { location: "/", hash: "#top" },
109 | contents: "ראשי",
110 | },
111 | {
112 | navigate: { location: "/", hash: "#person" },
113 | contents: "ח״כים",
114 | },
115 | {
116 | navigate: { location: "/quotes" },
117 | contents: "ציטוטים",
118 | },
119 | {
120 | navigate: { location: "/calendar" },
121 | contents: "לו״ז",
122 | },
123 | {
124 | navigate: { location: "/apps/votes" },
125 | contents: "הצבעות",
126 | },
127 | ];
128 |
129 | const links = [
130 | {
131 | callback: () => setAboutOpen(true),
132 | contents: "אודות",
133 | },
134 | {
135 | callback: () => setContactUsOpen(true),
136 | contents: "צרו קשר",
137 | },
138 | {
139 | callback: () => setDisclaimerOpen(true),
140 | contents: "גילוי נאות",
141 | },
142 | ];
143 |
144 | const main = isBigScreen ? (
145 |
146 | ) : (
147 |
148 | );
149 |
150 | return (
151 | <>
152 |
153 |
154 |
158 | {main}
159 | >
160 | );
161 | }
162 |
163 | function Logo() {
164 | const classes = useStyles();
165 | return (
166 |
167 |
174 |
180 | ב טא מחוקקי
181 | ם
182 |
183 |
184 |
185 | );
186 | }
187 |
188 | const NavBar = React.memo(function ({ nav, links }) {
189 | const classes = useStyles();
190 | const navigateFn = useNavigate();
191 | return (
192 | <>
193 |
194 |
195 |
196 | {nav.map(({ contents, navigate, callback }) => {
197 | const cb = navigate
198 | ? () => navigateFn(navigate)
199 | : callback;
200 | return (
201 |
202 | {contents}
203 |
204 | );
205 | })}
206 |
207 |
208 |
209 |
210 |
211 | {links.map(({ contents, navigate, callback }) => {
212 | const cb = navigate
213 | ? () => navigateFn(navigate)
214 | : callback;
215 | return (
216 |
217 | {contents}
218 |
219 | );
220 | })}
221 |
מזלגו אותנו ב-GitHub} arrow>
222 |
224 | window.open(config.gitUrl, "_blank")
225 | }
226 | >
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 | >
235 | );
236 | });
237 |
238 | const NavDrawer = React.memo(function ({ nav, links }) {
239 | const classes = useStyles();
240 | const navigateFn = useNavigate();
241 | const [drawerOpen, setDrawerOpen] = useState(false);
242 |
243 | const createToggleDrawer = (open) => (event) => {
244 | if (
245 | event.type === "keydown" &&
246 | (event.key === "Tab" || event.key === "Shift")
247 | ) {
248 | return;
249 | }
250 |
251 | setDrawerOpen(open);
252 | };
253 |
254 | return (
255 | <>
256 |
257 |
258 |
259 |
260 |
261 |
269 |
270 | {nav.map(({ contents, navigate, callback }) => {
271 | const cb = () => {
272 | if (navigate) navigateFn(navigate);
273 | else callback();
274 | setDrawerOpen(false);
275 | };
276 | return (
277 |
278 |
279 |
280 | );
281 | })}
282 |
283 | {links.map(({ contents, navigate, callback }) => {
284 | const cb = navigate
285 | ? () => navigateFn(navigate)
286 | : callback;
287 | return (
288 |
289 |
290 |
291 | );
292 | })}
293 | window.open(config.gitUrl, "_blank")}
296 | >
297 | } />
298 |
299 |
300 |
301 |
302 | >
303 | );
304 | });
305 |
--------------------------------------------------------------------------------
/src/components/Chat/index.css:
--------------------------------------------------------------------------------
1 | /* M E N U */
2 |
3 | .menu {
4 | position: fixed;
5 | top: 0px;
6 | right: 0px;
7 | left: 0px;
8 | width: 100%;
9 | height: 50px;
10 | background: rgba(82, 179, 217, 0.9);
11 | z-index: 100;
12 | -webkit-touch-callout: none;
13 | -webkit-user-select: none;
14 | -moz-user-select: none;
15 | -ms-user-select: none;
16 | }
17 |
18 | .back {
19 | position: absolute;
20 | width: 90px;
21 | height: 50px;
22 | top: 0px;
23 | right: 0px;
24 | color: #eee;
25 | line-height: 50px;
26 | font-size: 30px;
27 | padding-right: 10px;
28 | cursor: pointer;
29 | }
30 | .back img {
31 | position: absolute;
32 | top: 5px;
33 | right: 30px;
34 | width: 40px;
35 | height: 40px;
36 | background-color: rgba(255, 255, 255, 0.98);
37 | border-radius: 100%;
38 | -webkit-border-radius: 100%;
39 | -moz-border-radius: 100%;
40 | -ms-border-radius: 100%;
41 | margin-right: 15px;
42 | }
43 | .back:active {
44 | background: rgba(255, 255, 255, 0.2);
45 | }
46 | .name {
47 | position: absolute;
48 | top: 3px;
49 | right: 110px;
50 | font-family: "Lato";
51 | font-size: 25px;
52 | font-weight: 300;
53 | color: rgba(255, 255, 255, 0.98);
54 | cursor: default;
55 | }
56 | .last {
57 | position: absolute;
58 | top: 30px;
59 | right: 115px;
60 | font-family: "Lato";
61 | font-size: 11px;
62 | font-weight: 400;
63 | color: rgba(255, 255, 255, 0.6);
64 | cursor: default;
65 | }
66 |
67 | /* M E S S A G E S */
68 |
69 | .chat {
70 | list-style: none;
71 | background: none;
72 | margin: 0;
73 | padding: 0 0 50px 0;
74 | margin-bottom: 10px;
75 | max-width: 800px;
76 | }
77 | .chat li {
78 | margin: 0;
79 | margin-top: 1.5em;
80 | padding: 0.5rem 0 0.5rem 0.5rem;
81 | overflow: hidden;
82 | display: flex;
83 | }
84 | .chat .avatar {
85 | width: 40px;
86 | height: 40px;
87 | position: relative;
88 | display: block;
89 | z-index: 2;
90 | border-radius: 100%;
91 | -webkit-border-radius: 100%;
92 | -moz-border-radius: 100%;
93 | -ms-border-radius: 100%;
94 | background-color: rgba(245, 245, 245, 0.9);
95 | }
96 | .chat .avatar .img {
97 | width: 40px;
98 | height: 40px;
99 | border-radius: 100%;
100 | -webkit-border-radius: 100%;
101 | -moz-border-radius: 100%;
102 | -ms-border-radius: 100%;
103 | background-color: rgba(245, 245, 245, 0.9);
104 | background-position: center;
105 | background-size: 100% auto;
106 | background-repeat: no-repeat;
107 | }
108 | .chat .day {
109 | position: relative;
110 | display: block;
111 | text-align: center;
112 | color: #c0c0c0;
113 | height: 20px;
114 | text-shadow: 7px 0px 0px #e5e5e5, 6px 0px 0px #e5e5e5, 5px 0px 0px #e5e5e5,
115 | 4px 0px 0px #e5e5e5, 3px 0px 0px #e5e5e5, 2px 0px 0px #e5e5e5,
116 | 1px 0px 0px #e5e5e5, 1px 0px 0px #e5e5e5, 0px 0px 0px #e5e5e5,
117 | -1px 0px 0px #e5e5e5, -2px 0px 0px #e5e5e5, -3px 0px 0px #e5e5e5,
118 | -4px 0px 0px #e5e5e5, -5px 0px 0px #e5e5e5, -6px 0px 0px #e5e5e5,
119 | -7px 0px 0px #e5e5e5;
120 | box-shadow: inset 20px 0px 0px #e5e5e5, inset -20px 0px 0px #e5e5e5,
121 | inset 0px -2px 0px #d7d7d7;
122 | line-height: 38px;
123 | margin-top: 5px;
124 | margin-bottom: 20px;
125 | cursor: default;
126 | -webkit-touch-callout: none;
127 | -webkit-user-select: none;
128 | -moz-user-select: none;
129 | -ms-user-select: none;
130 | }
131 |
132 | .other {
133 | justify-content: flex-end;
134 | align-items: flex-start;
135 | }
136 |
137 | .self {
138 | display: flex;
139 | flex-direction: row-reverse;
140 | }
141 |
142 | .self .msg {
143 | border-top-right-radius: 0px;
144 | box-shadow: -1px 2px 0px #d4d4d4;
145 | }
146 | .self .avatar:after {
147 | content: "";
148 | position: relative;
149 | display: inline-block;
150 | top: -41px;
151 | right: 0px;
152 | width: 0px;
153 | height: 0px;
154 | border: 7px solid rgba(245, 245, 245, 0.9);
155 | border-left-color: transparent;
156 | border-bottom-color: transparent;
157 | }
158 |
159 | .other {
160 | justify-content: flex-start;
161 | align-items: flex-end;
162 | }
163 | .other .msg {
164 | border-bottom-left-radius: 0px;
165 | box-shadow: 1px 2px 0px #d4d4d4;
166 | }
167 | .other .avatar {
168 | }
169 | .other:before {
170 | content: "";
171 | position: relative;
172 | display: inline-block;
173 | bottom: 0px;
174 | left: 0px;
175 | right: 40px;
176 | width: 0px;
177 | height: 0px;
178 | border: 5px solid rgba(211, 222, 255, 0.9);
179 | border-right-color: transparent;
180 | border-top-color: transparent;
181 | box-shadow: 0px 2px 0px #d4d4d4;
182 | }
183 |
184 | .msg {
185 | background: rgba(245, 245, 245, 0.9);
186 | min-width: 50px;
187 | padding: 10px;
188 | border-radius: 2px;
189 | box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.07);
190 | position: relative;
191 | transition: background-color 0.2s;
192 | }
193 |
194 | .not-in-protocol .msg:hover {
195 | background: rgba(222, 232, 255, 0.9);
196 | }
197 |
198 | .not-in-protocol.expanded .msg {
199 | background: rgba(233, 211, 191, 0.9) !important;
200 | }
201 |
202 | .in-protocol .msg {
203 | max-width: 80%;
204 | }
205 |
206 | .other .msg {
207 | background: rgba(211, 222, 255, 0.9);
208 | }
209 |
210 | .msg p {
211 | font-size: 0.8rem;
212 | margin: 0 0 0.2rem 0;
213 | color: #777;
214 | transition: all 0.2s;
215 | word-spacing: nowrap;
216 | }
217 |
218 | .msg .go-suffix {
219 | font-weight: bold;
220 | transition: color 0.2s ease-in;
221 | }
222 | .expanded .msg .go-suffix,
223 | .expanded .msg svg,
224 | .msg:hover .go-suffix,
225 | .msg:hover svg {
226 | color: rgb(43, 86, 160);
227 | }
228 |
229 | .other .msg p {
230 | color: #474747;
231 | }
232 |
233 | .msg .speaker,
234 | .other .msg .speaker {
235 | font-weight: bold;
236 | color: #7092a3;
237 | font-size: 70%;
238 | }
239 |
240 | .msg img {
241 | position: relative;
242 | display: block;
243 | width: 450px;
244 | border-radius: 5px;
245 | box-shadow: 0px 0px 3px #eee;
246 | transition: all 0.4s cubic-bezier(0.565, -0.26, 0.255, 1.41);
247 | cursor: default;
248 | -webkit-touch-callout: none;
249 | -webkit-user-select: none;
250 | -moz-user-select: none;
251 | -ms-user-select: none;
252 | }
253 | @media screen and (max-width: 800px) {
254 | .msg img {
255 | width: 300px;
256 | }
257 | }
258 | @media screen and (max-width: 550px) {
259 | .msg img {
260 | width: 200px;
261 | }
262 | }
263 |
264 | .msg time {
265 | display: flex;
266 | width: 100%;
267 | justify-content: space-between;
268 | font-size: 0.7rem;
269 | color: #bbb;
270 | margin-top: 3px;
271 | float: left;
272 | cursor: default;
273 | -webkit-touch-callout: none;
274 | -webkit-user-select: none;
275 | -moz-user-select: none;
276 | -ms-user-select: none;
277 | }
278 |
279 | .msg time .date-string:after {
280 | content: "◴";
281 | color: #ddd;
282 | font-family: FontAwesome;
283 | display: inline-block;
284 | margin-right: 4px;
285 | }
286 |
287 | .msg time a {
288 | color: #6e96ac;
289 | font-size: 110%;
290 | vertical-align: middle;
291 | transition: color 0.25s;
292 | }
293 |
294 | .msg time a:hover,
295 | .expanded time a {
296 | color: #2b617e;
297 | }
298 |
299 | .other .msg time {
300 | color: #888;
301 | }
302 | .other .msg time .date-string:after {
303 | color: #aaa;
304 | }
305 |
306 | .continuation .avatar,
307 | .continuation.other::before {
308 | visibility: hidden;
309 | }
310 | .continuation .speaker {
311 | display: none;
312 | }
313 |
314 | .chat .continuation {
315 | margin: 0;
316 | }
317 |
318 | emoji {
319 | display: inline-block;
320 | height: 18px;
321 | width: 18px;
322 | background-size: cover;
323 | background-repeat: no-repeat;
324 | margin-top: -7px;
325 | margin-left: 2px;
326 | transform: translate3d(0px, 3px, 0px);
327 | }
328 | emoji.please {
329 | background-image: url(https://imgur.com/ftowh0s.png);
330 | }
331 | emoji.lmao {
332 | background-image: url(https://i.imgur.com/MllSy5N.png);
333 | }
334 | emoji.happy {
335 | background-image: url(https://imgur.com/5WUpcPZ.png);
336 | }
337 | emoji.pizza {
338 | background-image: url(https://imgur.com/voEvJld.png);
339 | }
340 | emoji.cryalot {
341 | background-image: url(https://i.imgur.com/UUrRRo6.png);
342 | }
343 | emoji.books {
344 | background-image: url(https://i.imgur.com/UjZLf1R.png);
345 | }
346 | emoji.moai {
347 | background-image: url(https://imgur.com/uSpaYy8.png);
348 | }
349 | emoji.suffocated {
350 | background-image: url(https://i.imgur.com/jfTyB5F.png);
351 | }
352 | emoji.scream {
353 | background-image: url(https://i.imgur.com/tOLNJgg.png);
354 | }
355 | emoji.hearth_blue {
356 | background-image: url(https://i.imgur.com/gR9juts.png);
357 | }
358 | emoji.funny {
359 | background-image: url(https://i.imgur.com/qKia58V.png);
360 | }
361 |
362 | @-webikt-keyframes pulse {
363 | from {
364 | opacity: 0;
365 | }
366 | to {
367 | opacity: 0.5;
368 | }
369 | }
370 |
371 | /* T Y P E */
372 |
373 | input.textarea {
374 | position: fixed;
375 | bottom: 0px;
376 | right: 0px;
377 | left: 0px;
378 | width: 100%;
379 | height: 50px;
380 | z-index: 99;
381 | background: #fafafa;
382 | border: none;
383 | outline: none;
384 | padding-right: 55px;
385 | padding-left: 55px;
386 | color: #666;
387 | font-weight: 400;
388 | }
389 | .emojis {
390 | position: fixed;
391 | display: block;
392 | bottom: 8px;
393 | right: 7px;
394 | width: 34px;
395 | height: 34px;
396 | background-image: url(https://i.imgur.com/5WUpcPZ.png);
397 | background-repeat: no-repeat;
398 | background-size: cover;
399 | z-index: 100;
400 | cursor: pointer;
401 | }
402 | .emojis:active {
403 | opacity: 0.9;
404 | }
405 |
406 | .chat li .share-buttons {
407 | text-align: center;
408 | opacity: 0;
409 | transform: scaleY(0) scaleX(0.9);
410 | transition: opacity 0.05s ease-in-out,
411 | transform 0.15s cubic-bezier(1, 0.32, 0.82, 1.6);
412 | }
413 | .chat li:hover .share-buttons {
414 | opacity: 1;
415 | transform: scale(1);
416 | }
417 |
418 | @media screen and (max-width: 600px), screen and (max-height: 400px) {
419 | .chat li .share-buttons {
420 | opacity: 1;
421 | transform: scale(1);
422 | }
423 | }
424 |
425 | .click-for-more {
426 | opacity: 0;
427 | cursor: pointer;
428 | position: absolute;
429 | height: 30px;
430 | top: 0;
431 | left: 0;
432 | padding: 0 2em 0 0.5em;
433 | display: flex;
434 | justify-items: flex-end;
435 | align-items: center;
436 | transition: opacity 0.2s ease-in;
437 | font-family: "Secular One", sans-serif;
438 | }
439 | .msg:hover .click-for-more {
440 | opacity: 1;
441 | }
442 | .click-for-more > * {
443 | padding: 0 0.25em;
444 | }
445 | .click-for-more a {
446 | color: #333;
447 | text-decoration: none;
448 | }
449 |
--------------------------------------------------------------------------------
/src/pages/Main/PersonProfile/PersonBills/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 | import { IconButton, Tooltip } from "@material-ui/core";
4 | import clsx from "clsx";
5 | import config from "../../../../config";
6 | import { makeStyles } from "@material-ui/core/styles";
7 | import Accordion from "@material-ui/core/Accordion";
8 | import AccordionDetails from "@material-ui/core/AccordionDetails";
9 | import AccordionSummary from "@material-ui/core/AccordionSummary";
10 | import Typography from "@material-ui/core/Typography";
11 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
12 | import CloseRoundedIcon from "@material-ui/icons/CloseRounded";
13 | import AccessTimeRoundedIcon from "@material-ui/icons/AccessTimeRounded";
14 | import CheckRoundedIcon from "@material-ui/icons/CheckRounded";
15 | import LoopRoundedIcon from "@material-ui/icons/LoopRounded";
16 | import DescriptionRoundedIcon from "@material-ui/icons/DescriptionRounded";
17 | import HelpOutlineRoundedIcon from "@material-ui/icons/HelpOutlineRounded";
18 |
19 | import Dialog from "../../../../components/Dialog";
20 | import { useCancellableFetch } from "../../../../utils";
21 |
22 | export default React.memo(function PersonBills({ personID, filter }) {
23 | const [loading, setLoading] = useState(true);
24 | const [data, setData] = useState([]);
25 | const classes = useStyles();
26 | const serverFetch = useCancellableFetch();
27 |
28 | useEffect(() => {
29 | if (!personID) return;
30 | (async () => {
31 | setLoading(true);
32 | const res = await serverFetch(
33 | `${config.server}/PersonBills?personId=${personID}`
34 | );
35 | setData(res);
36 | setLoading(false);
37 | })();
38 | }, [personID, serverFetch]);
39 |
40 | if (loading) {
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | const filteredData = data
49 | .filter((d) => filter == null || getReducedType(d) === filter)
50 | .slice(0, 100);
51 |
52 | return (
53 |
57 | );
58 | });
59 |
60 | const useStyles = makeStyles((theme) => ({
61 | loader: {
62 | width: "88%",
63 | textAlign: "center",
64 | },
65 | root: {
66 | display: "flex",
67 | flexDirection: "column",
68 | alignItems: "stretch",
69 | justifyItems: "center",
70 | padding: "1em",
71 | width: "100%",
72 | },
73 | heading: {
74 | fontSize: theme.typography.pxToRem(15),
75 | flexBasis: "100%",
76 | flexShrink: 0,
77 | },
78 | secondaryHeading: {
79 | fontSize: theme.typography.pxToRem(12),
80 | color: theme.palette.text.secondary,
81 | },
82 | accordionRow: {
83 | backgroundColor: "#f3f9ff",
84 | },
85 | accordionRowOpen: {
86 | boxShadow:
87 | "inset 0 0 11px 7px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 3px 0px rgb(0 0 0 / 12%)",
88 | },
89 | accordionIcon: {
90 | paddingLeft: "0.7em",
91 | alignSelf: "center",
92 | },
93 | }));
94 |
95 | function ControlledAccordions({ data, isFiltered }) {
96 | const classes = useStyles();
97 | const [expanded, setExpanded] = React.useState(false);
98 | const [dialog, setDialog] = React.useState(null);
99 | const handleChange = (panel) => (event, isExpanded) => {
100 | setExpanded(isExpanded ? panel : false);
101 | };
102 |
103 | return (
104 |
105 |
106 |
setDialog(null)}
109 | closeText="סגור"
110 | >
111 | {dialog?.billName}
112 | {dialog?.summaryLaw}
113 |
114 | {data.length > 0 ? (
115 | data.map((x, i) => (
116 |
126 | }
128 | aria-controls={`panel${i}bh-content`}
129 | id={`panel${i}bh-header`}
130 | >
131 |
132 |
133 |
134 |
135 | {x.billName}
136 |
137 |
138 |
139 | {x.billSubName && (
140 |
145 | {x.billSubName} - הכנסת ה-
146 | {x.knessetNum}
147 |
148 | )}
149 | {!x.billSubName && (
150 |
155 | הכנסת ה-{x.knessetNum}
156 |
157 | )}
158 |
159 |
160 | {x.summaryLaw && (
161 | {
163 | e.preventDefault();
164 | setDialog(x);
165 | }}
166 | >
167 |
168 |
169 | )}
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | ))
178 | ) : (
179 |
187 | לא מצאנו הצעות חוק
188 | {!isFiltered && " שהח״כ יזם"}
189 |
190 | )}
191 |
192 |
193 | );
194 | }
195 | function ClickableDocs({ documents }) {
196 | return (
197 | <>
198 | {documents.map((x) => (
199 |
227 | ))}
228 | >
229 | );
230 | }
231 |
232 | function StatusToIcon({ statusID, desc }) {
233 | const classes = useStyles();
234 | let icon = <>>;
235 | switch (statusID) {
236 | case 177:
237 | icon = (
238 |
242 | );
243 | break;
244 | case 118:
245 | icon = (
246 |
250 | );
251 | break;
252 | case 120:
253 | case 126:
254 | case 158:
255 | case 161:
256 | case 162:
257 | case 165:
258 | case 169:
259 | case 175:
260 | icon = (
261 |
265 | );
266 | break;
267 | default:
268 | icon = (
269 |
273 | );
274 | }
275 | return (
276 | {desc}} arrow>
277 | {icon}
278 |
279 | );
280 | }
281 |
282 | function getReducedType(bill) {
283 | switch (bill.statusID) {
284 | case 177:
285 | return "נעצרה";
286 | case 118:
287 | return "אושרה";
288 | case 120:
289 | case 126:
290 | case 158:
291 | case 161:
292 | case 162:
293 | case 165:
294 | case 169:
295 | case 175:
296 | return "אחר";
297 | default:
298 | return "בתהליך";
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/src/pages/Main/PersonProfile/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from "react";
2 | import Typography from "@material-ui/core/Typography";
3 | import CircularProgress from "@material-ui/core/CircularProgress";
4 | import Button from "@material-ui/core/Button";
5 | import PeopleIcon from "@material-ui/icons/People";
6 |
7 | import PersonQuotes from "./PersonQuotes";
8 | import WordCloud from "../../../components/WordCloud";
9 | import PersonBills from "./PersonBills";
10 | import PersonBillsStats from "./PersonBillsStats";
11 | import config from "../../../config";
12 | import {
13 | useBigScreen,
14 | useCancellableFetch,
15 | useNavigate,
16 | imageOrDefault,
17 | usePersonID,
18 | getFullName,
19 | } from "../../../utils";
20 | import { ScrollPage } from "../../../components/ScrollableView";
21 | import PersonSearch from "../../../components/PersonSearch";
22 | import "./index.css";
23 | import { makeStyles } from "@material-ui/core";
24 |
25 | const useStyles = makeStyles({
26 | selection: {
27 | padding: "1em",
28 | width: "100%",
29 | height: "100%",
30 | display: "flex",
31 | flexDirection: "column",
32 | overflowY: "auto",
33 | },
34 |
35 | people: {
36 | display: "flex",
37 | flexWrap: "wrap",
38 | justifyItems: "center",
39 | alignItems: "space-between",
40 | placeContent: "space-around",
41 | width: "100%",
42 | flexGrow: 1,
43 | },
44 | });
45 |
46 | const PersonProfile = React.memo(function () {
47 | const personID = usePersonID();
48 | const isBigScreen = useBigScreen();
49 | const [persons, setPersons] = useState({});
50 | const [fallbackPerson, setFallbackPerson] = useState(null);
51 | const serverFetch = useCancellableFetch();
52 | const person = useMemo(() => persons[personID], [persons, personID]);
53 |
54 | useEffect(() => {
55 | (async () => {
56 | const res = await serverFetch(`${config.server}/PersonGetAll`);
57 | const mapping = {};
58 | for (const p of res) {
59 | mapping[parseInt(p.PersonID)] = p;
60 | }
61 | setPersons(mapping);
62 | })();
63 | }, [serverFetch]);
64 |
65 | useEffect(() => {
66 | if (person || !personID) return;
67 |
68 | (async () => {
69 | setFallbackPerson(null);
70 | const res = await serverFetch(
71 | `${config.server}/PersonEnrichment?personID=${personID}`
72 | );
73 | setFallbackPerson(parseResponse(res));
74 | })();
75 | }, [personID, person, serverFetch]);
76 |
77 | const chosenPerson = person || fallbackPerson;
78 |
79 | return (
80 |
85 | {personID && }
86 | {!personID && }
87 |
88 | );
89 | });
90 | export default PersonProfile;
91 |
92 | const PersonView = React.memo(function ({ persons, person }) {
93 | const [billsFilter, setBillsFilter] = useState(null);
94 |
95 | return (
96 |
100 |
101 |
111 | {person ? (
112 |
113 | ) : (
114 |
115 | )}
116 |
117 |
118 |
119 |
120 |
127 |
134 |
144 |
145 | );
146 | });
147 |
148 | const PersonSelectionView = React.memo(function ({ persons }) {
149 | const classes = useStyles();
150 | const navigate = useNavigate();
151 |
152 | const rands = new Set();
153 | const ids = Object.keys(persons);
154 | while (rands.size < 5 && rands.size < ids.length) {
155 | const index = parseInt(Math.random() * ids.length);
156 | rands.add(ids[index]);
157 | }
158 |
159 | return (
160 |
161 |
166 |
167 | {[...rands].map((id) => (
168 |
navigate({ personID: id, q: null })}
171 | style={{ maxWidth: 350 }}
172 | >
173 |
174 |
175 | ))}
176 |
navigate({ personID: null })}>
177 |
185 |
198 |
208 | עוד
209 |
210 |
219 | ח״כים מהשנים האחרונות
220 |
221 |
222 |
223 |
224 |
225 | );
226 | });
227 |
228 | const PersonAvatar = React.memo(function ({ person }) {
229 | if (!person) return ;
230 |
231 | const name = getFullName(person);
232 | const desc = [];
233 | if (person.FactionName) desc.push(person.FactionName);
234 | if (person.KnessetNum) desc.push(`הכנסת ה-${person.KnessetNum}`);
235 |
236 | return (
237 |
245 |
255 |
265 | {name}
266 |
267 |
276 | {desc.join(", ")}
277 |
278 |
279 | );
280 | });
281 |
282 | function parseResponse(contents) {
283 | if (contents.length === 0) return null;
284 |
285 | // take generic values
286 | const general = {};
287 | for (const property of [
288 | "PersonID",
289 | "FirstName",
290 | "LastName",
291 | "Email",
292 | "GenderDesc",
293 | "imgPath",
294 | "BirthCountry",
295 | "BirthDate",
296 | "CityName",
297 | "FamilyStatus",
298 | "ChildrenNumber",
299 | ]) {
300 | general[property] = contents[0][property];
301 | }
302 |
303 | general["Name"] = `${general.FirstName} ${general.LastName}`;
304 |
305 | const positions = contents.map((p) => {
306 | const ret = {};
307 | for (const property of [
308 | "PersonToPositionID",
309 | "DutyDesc",
310 | "GovMinistryName",
311 | "FactionName",
312 | "KnessetNum",
313 | "DutyDesc",
314 | "IsCurrent",
315 | "PositionStartDate",
316 | "PositionFinishDate",
317 | ]) {
318 | ret[property] = p[property];
319 | }
320 | return ret;
321 | });
322 |
323 | return {
324 | ...general,
325 | positions,
326 | positionsByKnesset: getPositionsByKnesset(positions),
327 | };
328 | }
329 |
330 | function getPositionsByKnesset(positions) {
331 | const res = {};
332 | for (const pos of positions) {
333 | if (!res[pos.KnessetNum]) res[pos.KnessetNum] = [];
334 | res[pos.KnessetNum].push(pos);
335 | }
336 | return res;
337 | }
338 |
--------------------------------------------------------------------------------
/src/pages/Document/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import clsx from "clsx";
3 | import { useParams } from "react-router-dom";
4 | import Drawer from "@material-ui/core/Drawer";
5 | import List from "@material-ui/core/List";
6 | import ListItem from "@material-ui/core/ListItem";
7 | import ListItemIcon from "@material-ui/core/ListItemIcon";
8 | import ListItemText from "@material-ui/core/ListItemText";
9 | import { makeStyles } from "@material-ui/core/styles";
10 | import Container from "@material-ui/core/Container";
11 | import CardActions from "@material-ui/core/CardActions";
12 | import CardContent from "@material-ui/core/CardContent";
13 | import Button from "@material-ui/core/Button";
14 | import Typography from "@material-ui/core/Typography";
15 | import DescriptionIcon from "@material-ui/icons/Description";
16 | import LiveTvIcon from "@material-ui/icons/LiveTv";
17 | import ChatBubbleIcon from "@material-ui/icons/ChatBubble";
18 | import Fab from "@material-ui/core/Fab";
19 | import QuotesLoader from "../../components/QuotesLoader";
20 | import Chat from "../../components/Chat";
21 | import config from "../../config";
22 | import {
23 | useCancellableFetch,
24 | useBigScreen,
25 | useQuery,
26 | useIndex,
27 | usePersonID,
28 | toNiceDate,
29 | } from "../../utils";
30 | import { ScrollPage } from "../../components/ScrollableView";
31 | import ChatLoader from "../../components/ChatLoader";
32 |
33 | const useStyles = makeStyles({
34 | root: {
35 | position: "relative",
36 | width: "100%",
37 | height: "100%",
38 | zIndex: 2,
39 | },
40 | grid: {
41 | display: "grid",
42 | height: "100%",
43 | gridTemplateColumns: "3fr 1fr",
44 | gridTemplateRows: "1fr",
45 | gap: "0% 2%",
46 | gridTemplateAreas: ". .",
47 | "& $metadata": {
48 | minWidth: 275,
49 | margin: "1em",
50 | alignSelf: "flex-start",
51 | },
52 | },
53 |
54 | flex: {
55 | overflowY: "auto",
56 | height: "100%",
57 | },
58 |
59 | bullet: {
60 | display: "inline-block",
61 | margin: "0 2px",
62 | transform: "scale(0.8)",
63 | },
64 | title: {
65 | fontSize: 14,
66 | },
67 | pos: {
68 | marginBottom: 12,
69 | },
70 |
71 | bigScreen: {
72 | "& $drawerPaper": {
73 | maxWidth: "35vw",
74 | },
75 | },
76 | smallScreen: {
77 | "& $drawerPaper": {
78 | maxHeight: "50vh",
79 | },
80 | },
81 | drawer: {},
82 |
83 | metadata: {},
84 | drawerPaper: {},
85 | });
86 |
87 | const DocumentQuotes = React.memo(function ({ type }) {
88 | const classes = useStyles();
89 | const { id } = useParams();
90 | const [loading, setLoading] = useState(true);
91 | const isBigScreen = useBigScreen();
92 | const [drawerOpen, setDrawerOpen] = useState(true);
93 | const createToggleDrawer = (open) => (event) => {
94 | if (
95 | event &&
96 | event.type === "keydown" &&
97 | (event.key === "Tab" || event.key === "Shift")
98 | ) {
99 | return;
100 | }
101 |
102 | setDrawerOpen(open);
103 | };
104 |
105 | return (
106 |
111 |
112 |
113 |
114 |
119 |
131 |
132 |
133 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | });
149 | export default DocumentQuotes;
150 |
151 | const QuoteView = React.memo(function ({
152 | documentID,
153 | documentType,
154 | setLoading,
155 | }) {
156 | const [data, setData] = useState([]);
157 |
158 | const personID = usePersonID();
159 | const query = useQuery();
160 | const index = useIndex();
161 | const serverFetch = useCancellableFetch();
162 |
163 | useEffect(() => {
164 | (async () => {
165 | if (!documentID) return;
166 | setLoading(true);
167 | try {
168 | const quotes = await serverFetch(
169 | `${config.server}/DocumentQuotes?documentId=${documentID}&documentType=${documentType}&index=${index}`
170 | );
171 | setData(quotes);
172 | } catch (e) {
173 | // TODO handle errors
174 | console.error(e);
175 | setData([]);
176 | } finally {
177 | setLoading(false);
178 | }
179 | })();
180 | }, [documentID, documentType, index, setLoading, serverFetch]);
181 |
182 | let prevSpeaker = null;
183 | return (
184 |
194 |
195 | {
197 | const ret = {
198 | highlight: query,
199 | isSpeaker: personID === d.PersonID,
200 | isContinuation: prevSpeaker === d.Speaker,
201 | isInProtocol: true,
202 | ...d,
203 | };
204 | prevSpeaker = d.Speaker;
205 | return ret;
206 | })}
207 | />
208 |
209 |
210 | );
211 | });
212 |
213 | const Metadata = React.memo(function ({ documentID, documentType }) {
214 | const classes = useStyles();
215 | const [metadata, setMetadata] = useState(null);
216 | const serverFetch = useCancellableFetch();
217 |
218 | useEffect(() => {
219 | (async () => {
220 | if (!documentID) return;
221 | try {
222 | const enrichment = await serverFetch(
223 | `${config.server}/DocumentTopics?documentId=${documentID}&documentType=${documentType}`
224 | );
225 | if (!enrichment.length) {
226 | console.error("No metadata found, this is a server error");
227 | }
228 | setMetadata(enrichment);
229 | } catch (e) {
230 | // TODO handle errors
231 | console.error(e);
232 | setMetadata(null);
233 | }
234 | })();
235 | }, [documentID, documentType, serverFetch]);
236 |
237 | if (!metadata) return ;
238 |
239 | const md = metadata[0];
240 | const bull = • ;
241 |
242 | return (
243 |
244 |
245 |
250 | צפיה בפרוטוקול
251 |
252 |
253 | {documentType === "committee" &&
254 | (md?.Name?.length ? md.Name : "ועדה (פרטים חלקיים)")}
255 | {documentType === "plenum" && "ישיבת מליאה"}
256 |
257 | {md?.KnessetNum && (
258 |
259 | הכנסת ה-{md.KnessetNum}
260 | {bull}
261 | {toNiceDate(new Date(md.StartDate))}
262 |
263 | )}
264 |
265 |
266 | {metadata
267 | ?.filter((m) => m.ItemName?.length)
268 | .map((m) => (
269 |
270 |
271 |
272 |
273 |
279 |
280 | ))}
281 |
282 |
283 |
284 |
285 |
292 | {md?.FilePath && (
293 |
298 | }
299 | size="small"
300 | variant="contained"
301 | href={md.FilePath}
302 | target="_blank"
303 | rel="noreferrer"
304 | >
305 | לפרוטוקול המקורי
306 |
307 | )}
308 | {md?.BroadcastUrl && (
309 |
312 | }
313 | size="small"
314 | variant="contained"
315 | href={md.BroadcastUrl}
316 | target="_blank"
317 | rel="noreferrer"
318 | >
319 | לשידור הישיבה
320 |
321 | )}
322 |
323 |
324 |
325 | );
326 | });
327 |
328 | const FullProtocolButton = React.memo(function ({ url }) {
329 | if (!url) return null;
330 | return (
331 |
340 |
352 | }
353 | >
354 | לפרוטוקול המלא...
355 |
356 |
357 | );
358 | });
359 |
--------------------------------------------------------------------------------
/src/pages/Main/Search/Bubble/index.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState, useEffect } from "react";
2 | import "react-vis/dist/style.css";
3 | import Highlighter from "react-highlight-words";
4 | import { Treemap } from "react-vis";
5 | import { makeStyles, withStyles } from "@material-ui/core/styles";
6 | import Tooltip from "@material-ui/core/Tooltip";
7 | import Link from "@material-ui/core/Link";
8 | import Snackbar from "@material-ui/core/Snackbar";
9 | import Drawer from "@material-ui/core/Drawer";
10 | import Container from "@material-ui/core/Container";
11 | import CardActions from "@material-ui/core/CardActions";
12 | import CardContent from "@material-ui/core/CardContent";
13 | import PersonIcon from "@material-ui/icons/Person";
14 | import Button from "@material-ui/core/Button";
15 | import { useHistory } from "react-router-dom";
16 |
17 | import QuotesLoader from "../../../../components/QuotesLoader";
18 | import {
19 | useCancellableFetch,
20 | useBigScreen,
21 | useQuery,
22 | useWindowSize,
23 | toNiceDate,
24 | imageOrDefault,
25 | shuffleArray,
26 | useNavigate,
27 | } from "../../../../utils";
28 | import config from "../../../../config";
29 |
30 | import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
31 | import { SearchDialog } from "../../../../components/QuotesSearch";
32 |
33 | import defaultBubbles from "../../../../defaultTopics";
34 | import { Typography } from "@material-ui/core";
35 | import { getDocumentLink } from "../../../../components/DocumentLink";
36 | import { v4 as uuidv4 } from 'uuid';
37 |
38 | const uuid = uuidv4();
39 | const minSize = 50;
40 | const maxWidth = 750;
41 | const maxHeightRatio = 0.75;
42 | const minRatioForImage = 0.035;
43 | const minRatioForImageSmallScreen = Infinity;
44 | const maxBubblesForSmallScreen = 25;
45 |
46 | const useStyles = makeStyles({
47 | drawerPaper: {
48 | maxHeight: "65vh",
49 | },
50 | personCardFlex: {
51 | maxHeight: "65vh",
52 | display: "flex",
53 | flexDirection: "column",
54 | },
55 | });
56 |
57 | const makeDefaultBubbles = (queries, isBigScreen) => {
58 | shuffleArray(queries);
59 | const queriesCopy = queries.slice(
60 | 0,
61 | isBigScreen
62 | ? config.defaultBubblesCount
63 | : config.defaultBubblesCountMobile
64 | );
65 | const sizes = new Array(queriesCopy.length)
66 | .fill(null)
67 | .map((_) => 5 + Math.random() * 10);
68 | const max = Math.max(...sizes);
69 |
70 | const res = queriesCopy.map((s, i) =>
71 | makeDefaultBubble(s, sizes[i], max, isBigScreen)
72 | );
73 | res.push({
74 | element: ,
75 | query: null,
76 | size: max / 2,
77 | color: max / 2,
78 | style: {
79 | color: "black",
80 | fontStyle: "italic",
81 | fontSize: `175%`,
82 | animation: `pulse ${4 + Math.random() * 10}s ease-in-out infinite`,
83 | animationDelay: `-${Math.random() * 10}s`,
84 | animationDirection: "alternate",
85 | backgroundColor: `#ddd`,
86 | },
87 | });
88 | return res;
89 | };
90 | const makeDefaultBubble = (s, size, max, isBigScreen) => {
91 | const lightness = 0.4 + Math.random() * 0.5;
92 | return {
93 | element: ,
94 | query: s,
95 | size: size,
96 | color: size,
97 | style: {
98 | color: lightness > 0.65 ? "#333" : "white",
99 | fontSize: `${(0.65 + size / max) * 100 * (isBigScreen ? 1 : 0.7)}%`,
100 | animation: `pulse ${4 + Math.random() * 10}s ease-in-out infinite`,
101 | animationDelay: `-${Math.random() * 10}s`,
102 | animationDirection: "alternate",
103 | backgroundColor: `hsl(${Math.round(230 + Math.random() * 5)}, ${
104 | (0.25 + Math.random() * 0.4) * 100
105 | }%, ${lightness * 100}%)`,
106 | },
107 | };
108 | };
109 |
110 | const PersonTooltip = withStyles((theme) => ({
111 | tooltip: {
112 | boxShadow: theme.shadows[1],
113 | },
114 | }))(Tooltip);
115 |
116 | const Bubble = React.memo(function ({ viewPersonQuotes }) {
117 | const [loading, setLoading] = useState(false);
118 | const query = useQuery();
119 |
120 | return (
121 | <>
122 |
123 |
128 | >
129 | );
130 | });
131 | export default Bubble;
132 |
133 | const Chart = React.memo(function ({ query, setLoading }) {
134 | const windowSize = useWindowSize();
135 | const isBigScreen = useBigScreen();
136 | const defaults = useMemo(
137 | () => makeDefaultBubbles(defaultBubbles, isBigScreen),
138 | [isBigScreen]
139 | );
140 | const [personPreview, setPersonPreview] = useState(null);
141 | const [data, setData] = useState(defaults);
142 | const [error, setError] = useState(null);
143 | const [guideOpen, setGuideOpen] = useState(false);
144 | const navigate = useNavigate();
145 | const serverFetch = useCancellableFetch();
146 |
147 | const cleanQuery = query.replace("״", '"').replace("׳", "'");
148 |
149 | useEffect(() => {
150 | (async () => {
151 | if (!cleanQuery.length) {
152 | setData(defaults);
153 | return;
154 | }
155 | setLoading(true);
156 | setError(null);
157 | try {
158 | let res = await serverFetch(
159 | `${config.server}/Keywords?keyword=${cleanQuery}&uuid=${uuid}`
160 | );
161 | if (!res.length) {
162 | setError(
163 |
164 | אין תוצאות :( נסו ללחוץ על אחת הבועות?
165 |
166 | );
167 | setData(defaults);
168 | return;
169 | }
170 |
171 | const minRatio = isBigScreen
172 | ? minRatioForImage
173 | : minRatioForImageSmallScreen;
174 | const total = res
175 | .map((r) => r.Counter)
176 | .reduce((a, b) => a + b, 0);
177 |
178 | const filteredRes = res
179 | .filter(
180 | (r) =>
181 | r.Counter / total >= minRatio ||
182 | r.mk_imgPath !== null
183 | )
184 | .sort(function (a, b) {
185 | return b.Counter - a.Counter;
186 | })
187 | .slice(
188 | 0,
189 | isBigScreen ? undefined : maxBubblesForSmallScreen
190 | );
191 |
192 | setData(
193 | filteredRes.map((r) => ({
194 | result: r,
195 | element: (
196 |
202 | ),
203 | size: r.Counter,
204 | color: r.Counter,
205 | style: {
206 | background: `url(${imageOrDefault(
207 | r.mk_imgPath,
208 | r.PersonID.toString(),
209 | 256
210 | )}) no-repeat center center`,
211 | backgroundSize: "cover",
212 | color: "#ddd",
213 | cursor: "pointer",
214 | },
215 | }))
216 | );
217 | } catch (e) {
218 | console.error(e);
219 | setError("סורי, לא הצלחנו להביא תוצאות. שווה לנסות שוב");
220 | setData(defaults);
221 | } finally {
222 | setLoading(false);
223 | }
224 | })();
225 | }, [cleanQuery, setLoading, defaults, isBigScreen, serverFetch]);
226 |
227 | return (
228 | <>
229 |
230 |
231 |
232 | x.element}
248 | onLeafClick={(n) => {
249 | if (n.data.result) {
250 | if (isBigScreen) return;
251 | else setPersonPreview(n.data.result);
252 | } else if (n.data.query) navigate({ q: n.data.query });
253 | else if (n.data.query !== undefined) {
254 | setData(
255 | makeDefaultBubbles(defaultBubbles, isBigScreen)
256 | );
257 | setGuideOpen(true);
258 | }
259 | }}
260 | />
261 |
262 | {!isBigScreen && (
263 | setPersonPreview(null)}
267 | />
268 | )}
269 | >
270 | );
271 | });
272 |
273 | const PersonPreview = React.memo(function ({ onClose, details, query }) {
274 | const classes = useStyles();
275 | return (
276 |
282 |
283 |
284 | );
285 | });
286 |
287 | const PersonShortName = React.memo(function ({ ...props }) {
288 | const { PersonID, FirstName, LastName, ratio } = props;
289 | const isBigScreen = useBigScreen();
290 | const navigate = useNavigate();
291 | const minRatio = isBigScreen
292 | ? minRatioForImage
293 | : minRatioForImageSmallScreen;
294 |
295 | return (
296 | }
298 | placement="left"
299 | arrow
300 | interactive
301 | enterNextDelay={200}
302 | >
303 |
306 | isBigScreen
307 | ? navigate({ personID: PersonID, hash: "#person" })
308 | : void 0
309 | }
310 | style={{
311 | cursor: "pointer",
312 | display: "flex",
313 | justifyItems: "stretch",
314 | alignItems: "flex-end",
315 | width: "100%",
316 | }}
317 | >
318 | {ratio >= minRatio && (
319 |
320 |
321 | {FirstName} {LastName}
322 |
323 |
324 | )}
325 |
326 |
327 | );
328 | });
329 |
330 | const MobilePersonCard = React.memo(function ({
331 | PersonID,
332 | FirstName,
333 | LastName,
334 | Text,
335 | FactionName,
336 | KnessetNum,
337 | StartDate,
338 | Counter,
339 | Type,
340 | DocumentID,
341 | Index,
342 | mk_imgPath,
343 | query,
344 | onClose,
345 | }) {
346 | const classes = useStyles();
347 | const navigate = useNavigate();
348 | const history = useHistory();
349 | const gotoPerson = () => {
350 | navigate({ personID: PersonID, hash: "#person" });
351 | onClose();
352 | };
353 |
354 | return (
355 |
356 |
357 |
366 |
383 |
384 |
392 |
397 | {FirstName} {LastName}
398 |
399 |
403 | {Counter} תוצאות
404 |
405 |
406 | {(FactionName || KnessetNum) && (
407 |
411 | {FactionName && `${FactionName}, `}הכנסת ה-
412 | {KnessetNum}
413 |
414 | )}
415 |
416 |
417 |
418 |
419 |
420 |
421 | ״
422 |
430 | ״
431 |
432 | {StartDate && toNiceDate(new Date(StartDate))}
433 |
434 |
435 |
436 |
437 |
438 |
447 |
458 | }
459 | style={{ marginLeft: "1em" }}
460 | onClick={(e) => {
461 | history.push(
462 | getDocumentLink({
463 | SessionType: Type,
464 | DocumentID,
465 | Index,
466 | PersonID,
467 | })
468 | );
469 | e.preventDefault();
470 | }}
471 | >
472 | לפרוטוקול המלא
473 |
474 |
485 | }
486 | onClick={gotoPerson}
487 | >
488 | לפרופיל ח"כ
489 |
490 |
491 |
492 |
493 | );
494 | });
495 |
496 | const PersonCard = React.memo(function ({
497 | PersonID,
498 | FirstName,
499 | LastName,
500 | Text,
501 | FactionName,
502 | KnessetNum,
503 | StartDate,
504 | Counter,
505 | DocumentID,
506 | Index,
507 | Type,
508 | query,
509 | }) {
510 | const navigate = useNavigate();
511 | const history = useHistory();
512 | const gotoPerson = () => {
513 | navigate({ personID: PersonID, hash: "#person" });
514 | };
515 |
516 | return (
517 |
518 |
526 |
527 | {FirstName} {LastName}
528 |
529 |
535 | {Counter} תוצאות
536 |
537 |
538 | {(FactionName || KnessetNum) && (
539 |
545 | {FactionName && `${FactionName}, `}הכנסת ה-{KnessetNum}
546 |
547 | )}
548 |
559 | ״
560 |
568 | ״
569 |
570 |
577 | {toNiceDate(new Date(StartDate))}
578 |
579 |
588 |
599 | }
600 | onClick={(e) => {
601 | history.push(
602 | getDocumentLink({
603 | SessionType: Type,
604 | DocumentID,
605 | Index,
606 | PersonID,
607 | })
608 | );
609 | e.preventDefault();
610 | }}
611 | >
612 | לפרוטוקול המלא
613 |
614 |
625 | }
626 | onClick={gotoPerson}
627 | >
628 | לפרופיל ח"כ
629 |
630 |
631 |
632 | );
633 | });
634 |
635 | const SuggestionBubble = React.memo(function ({ query }) {
636 | return (
637 |
640 | );
641 | });
642 |
643 | const MoreSuggestionsBubble = React.memo(function () {
644 | return (
645 |
656 | );
657 | });
658 |
659 | function getDimensions(windowSize) {
660 | const size = Math.min(maxWidth, windowSize.width);
661 | return Math.max(
662 | minSize,
663 | Math.min(size, windowSize.height * maxHeightRatio)
664 | );
665 | }
666 |
--------------------------------------------------------------------------------