Skip to main content
Sign in
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • cf2025
  • main
  • cf2024
  • cf2023-euro
  • cf2023-offline
5 results

Target

Select target project
  • TO / cf-online-ui
  • vojtech.pikal / cf2021
2 results
Select Git revision
  • master
1 result
Show changes
58 files
+ 20776
17043
Compare changes
  • Side-by-side
  • Inline

Files

+3 −3
Original line number Diff line number Diff line
REACT_APP_STYLEGUIDE_URL=https://styleguide.pir-test.eu/2.3.0
REACT_APP_API_BASE_URL=https://cf2021.pir-test.eu/api
REACT_APP_STYLEGUIDE_URL=https://styleguide.pirati.cz/2.11.0
REACT_APP_API_BASE_URL=https://cf2024.online/api
REACT_APP_MATOMO_ID=135
REACT_APP_SENTRY_DSN=https://aa80453ff4d54b9a9c1b49e79060498a@sentry.pir-test.eu/14
REACT_APP_SENTRY_DSN=https://aa80453ff4d54b9a9c1b49e79060498a@sentry.pirati.cz/14
+7 −6
Original line number Diff line number Diff line
image: node:buster
image: node:18.20.4-bullseye

variables:
  SITE_NAME: cf2021.pirati.cz
  SITE_NAME: cf2024.online

  ARTIFACTS_PATH: build
  REACT_APP_API_BASE_URL: /api
  WEBHOOK_URL: https://ha-web.pirati.cz
@@ -12,11 +13,11 @@ before_script:
build:
  stage: build
  script:
    - npm install
    - npm install --legacy-peer-deps
    - npm run build
  after_script:
    - echo "{\"job_token\":\"$CI_JOB_TOKEN\", \"name\":\"$SITE_NAME\" }" > request.json
    - "curl -H 'Content-Type: application/json' -X POST -d @request.json $WEBHOOK_URL"
    - echo "{\"name\":\"$SITE_NAME\" }" > request.json
    - "curl -k -H 'Content-Type: application/json' -X POST -d @request.json $WEBHOOK_URL"
  artifacts:
    expire_in: 30 min
    paths:
+1 −1
Original line number Diff line number Diff line
14.13
 No newline at end of file
18.20.4
+7 −6
Original line number Diff line number Diff line
FROM node:14.13-alpine3.12 as build
FROM node:18.20.4-alpine AS build

ARG BUILD_REACT_APP_STYLEGUIDE_URL

ENV REACT_APP_STYLEGUIDE_URL=${BUILD_REACT_APP_STYLEGUIDE_URL}
ENV REACT_APP_STYLEGUIDE_URL="https://styleguide.pirati.cz/2.12.1"
ENV REACT_APP_API_BASE_URL=https://cf2024.online/api
ENV REACT_APP_MATOMO_ID=135
ENV REACT_APP_SENTRY_DSN=https://aa80453ff4d54b9a9c1b49e79060498a@sentry.pirati.cz/14

RUN mkdir -p /home/node/cf2021

@@ -10,7 +11,7 @@ WORKDIR /home/node/cf2021

COPY package*.json ./

RUN npm ci
RUN npm ci --legacy-peer-deps

COPY . .

@@ -18,7 +19,7 @@ RUN npm run build

# ---

FROM nginx:alpine as production
FROM nginx:alpine AS production

COPY --from=build /home/node/cf2021/build /usr/share/nginx/html/
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
+12 −12
Original line number Diff line number Diff line
FROM node:14.13-alpine3.12
FROM node:18.20.4-alpine

#  libs for development -- most of theme needed for canvas support in tests
RUN apk add --no-cache \
@@ -17,7 +17,7 @@ WORKDIR /app/cf2021

COPY package*.json ./

RUN npm ci
RUN npm ci --legacy-peer-deps

COPY . .

compose.yaml

0 → 100644
+6 −0
Original line number Diff line number Diff line
services:
  app:
    image: cf-online
    ports:
      - "3000:80"

eslint.config.mjs

0 → 100644
+67 −0
Original line number Diff line number Diff line
import { fixupConfigRules, fixupPluginRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import jestDom from "eslint-plugin-jest-dom";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import testingLibrary from "eslint-plugin-testing-library";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

const eslintConfig = [
  ...fixupConfigRules(
    compat.extends(
      "react-app",
      "plugin:prettier/recommended",
      "plugin:testing-library/dom",
      "plugin:jest-dom/recommended",
    ),
  ),
  {
    plugins: {
      "simple-import-sort": simpleImportSort,
      "testing-library": fixupPluginRules(testingLibrary),
      "jest-dom": fixupPluginRules(jestDom),
    },

    rules: {
      "sort-imports": "off",
      "prettier/prettier": "warn",
      "react/no-unknown-property": [1],

      "simple-import-sort/imports": [
        "warn",
        {
          groups: [
            ["^react", "^@?\\w"],
            [
              "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|markdown|ws)(/.*|$)",
            ],
            ["^(test-utils)(/.*|$)"],
            ["^\\u0000"],
            ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
            ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
            ["^.+\\.s?css$"],
          ],
        },
      ],

      "testing-library/await-async-queries": "error",
      "testing-library/no-await-sync-queries": "error",
      "testing-library/no-debugging-utils": "warn",
      "jest-dom/prefer-checked": "error",
      "jest-dom/prefer-enabled-disabled": "error",
      "jest-dom/prefer-required": "error",
      "jest-dom/prefer-to-have-attribute": "error",
    },
  },
];

export default eslintConfig;
+20024 −16445

File changed.

Preview size limit exceeded, changes collapsed.

+48 −104
Original line number Diff line number Diff line
@@ -3,36 +3,35 @@
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@react-keycloak/web": "^2.1.4",
    "@rooks/use-interval": "^4.5.0",
    "@rooks/use-window-size": "^4.5.0",
    "@sentry/integrations": "^5.29.2",
    "@sentry/react": "^5.29.2",
    "@sentry/tracing": "^5.29.2",
    "classnames": "^2.2.6",
    "crypto-js": "^3.3.0",
    "date-fns": "^2.16.1",
    "i": "^0.3.6",
    "immer": "^7.0.15",
    "keycloak-js": "^10.0.2",
    "lodash": "^4.17.20",
    "npm": "^6.14.10",
    "pullstate": "^1.20.5",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-helmet-async": "^1.0.7",
    "react-hint": "^3.2.0",
    "react-intersection-observer": "^8.31.0",
    "react-joyride": "^2.3.0",
    "react-mde": "^11.0.0",
    "react-modal": "^3.12.1",
    "react-player": "^2.7.2",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3",
    "showdown": "^1.9.1",
    "unfetch": "^4.2.0",
    "@react-keycloak/web": "^3.4.0",
    "@rooks/use-interval": "^4.11.2",
    "@rooks/use-outside-click": "^4.11.2",
    "@rooks/use-timeout": "^4.11.2",
    "@rooks/use-window-size": "^4.11.2",
    "@sentry/integrations": "^7.119.2",
    "@sentry/react": "^8.34.0",
    "@sentry/tracing": "^7.119.2",
    "ajv": "^8.17.1",
    "classnames": "^2.5.1",
    "crypto-js": "^4.2.0",
    "date-fns": "^4.1.0",
    "keycloak-js": "^26.0.0",
    "lodash": "^4.17.21",
    "pullstate": "^1.25.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-helmet-async": "^2.0.5",
    "react-hint": "^3.2.1",
    "react-intersection-observer": "^9.13.1",
    "react-joyride": "^2.9.2",
    "react-mde": "^11.5.0",
    "react-modal": "^3.16.1",
    "react-player": "^2.16.0",
    "react-router-dom": "^6.27.0",
    "react-scripts": "^5.0.1",
    "showdown": "^2.1.0",
    "wait-queue": "^1.1.4",
    "xss": "^1.0.8"
    "xss": "^1.0.15"
  },
  "scripts": {
    "start": "react-scripts start",
@@ -43,65 +42,6 @@
    "lint:fix": "eslint --cache --fix 'src/**/*.{js,jsx}'",
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "plugin:prettier/recommended",
      "plugin:testing-library/recommended",
      "plugin:jest-dom/recommended"
    ],
    "plugins": [
      "simple-import-sort",
      "testing-library",
      "jest-dom"
    ],
    "rules": {
      "sort-imports": "off",
      "prettier/prettier": "warn",
      "react/no-unknown-property": [
        1
      ],
      "simple-import-sort/sort": [
        "warn",
        {
          "groups": [
            [
              "^react",
              "^@?\\w"
            ],
            [
              "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|markdown|ws)(/.*|$)"
            ],
            [
              "^(test-utils)(/.*|$)"
            ],
            [
              "^\\u0000"
            ],
            [
              "^\\.\\.(?!/?$)",
              "^\\.\\./?$"
            ],
            [
              "^\\./(?=.*/)(?!/?$)",
              "^\\.(?!/?$)",
              "^\\./?$"
            ],
            [
              "^.+\\.s?css$"
            ]
          ]
        }
      ],
      "testing-library/await-async-query": "error",
      "testing-library/no-await-sync-query": "error",
      "testing-library/no-debug": "warn",
      "jest-dom/prefer-checked": "error",
      "jest-dom/prefer-enabled-disabled": "error",
      "jest-dom/prefer-required": "error",
      "jest-dom/prefer-to-have-attribute": "error"
    }
  },
  "browserslist": {
    "production": [
      ">0.2%",
@@ -115,23 +55,27 @@
    ]
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^4.0.0",
    "@testing-library/react": "^10.4.4",
    "@testing-library/react-hooks": "^3.4.2",
    "@testing-library/user-event": "^12.1.7",
    "@eslint/compat": "^1.2.0",
    "@eslint/eslintrc": "^3.1.0",
    "@eslint/js": "^9.12.0",
    "@testing-library/jest-dom": "^6.5.0",
    "@testing-library/react": "^16.0.1",
    "@testing-library/react-hooks": "^8.0.1",
    "@testing-library/user-event": "^14.5.2",
    "babel-core": "^6.26.3",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.8.0",
    "eslint-config-airbnb": "^18.2.0",
    "eslint-config-prettier": "^6.11.0",
    "eslint-plugin-import": "^2.22.0",
    "eslint-plugin-jest-dom": "^3.6.3",
    "eslint-plugin-jsx-a11y": "^6.3.1",
    "eslint-plugin-prettier": "^3.1.4",
    "eslint-plugin-react": "^7.20.6",
    "eslint-plugin-simple-import-sort": "^5.0.3",
    "eslint-plugin-testing-library": "^3.8.0",
    "prettier": "^2.1.1",
    "source-map-explorer": "^2.5.1"
    "eslint": "^9.12.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-import": "^2.31.0",
    "eslint-plugin-jest-dom": "^5.4.0",
    "eslint-plugin-jsx-a11y": "^6.10.0",
    "eslint-plugin-prettier": "^5.2.1",
    "eslint-plugin-react": "^7.37.1",
    "eslint-plugin-simple-import-sort": "^12.1.1",
    "eslint-plugin-testing-library": "^6.3.0",
    "prettier": "^3.3.3",
    "source-map-explorer": "^2.5.3",
    "typescript": "^5.6.3"
  }
}
+1.13 KiB (58.2 KiB)

File changed.

No diff preview for this file type.

public/img/og.png

deleted100644 → 0
−122 KiB

121.60 KiB

public/img/og2021.png

0 → 100644
+124 KiB

124.02 KiB

public/img/og2024.png

0 → 100644
+43.1 KiB

43.08 KiB

+11 −11
Original line number Diff line number Diff line
@@ -5,23 +5,23 @@
    <!-- Favicons -->
    <link rel="apple-touch-icon"  href="%REACT_APP_STYLEGUIDE_URL%/images/favicons/favicon-196x196.png">
    <link rel="icon" type="image/png" href="%REACT_APP_STYLEGUIDE_URL%/images/favicons/favicon-196x196.png" sizes="196x196">
    <meta name="application-name" content="CF2021">
    <meta name="application-name" content="CF2023">
    <meta name="msapplication-TileColor" content="#000000">
    <meta name="msapplication-TileImage" content="%REACT_APP_STYLEGUIDE_URL%/images/favicons/mstile-144x144.png">
    <meta name="msapplication-square70x70logo" content="%REACT_APP_STYLEGUIDE_URL%/images/favicons/mstile-70x70.png">
    <meta name="msapplication-square150x150logo" content="%REACT_APP_STYLEGUIDE_URL%/images/favicons/mstile-150x150.png">
    <meta name="msapplication-wide310x150logo" content="%REACT_APP_STYLEGUIDE_URL%/images/favicons/mstile-310x150.png">
    <meta name="msapplication-square310x310logo" content="%REACT_APP_STYLEGUIDE_URL%/images/favicons/mstile-310x310.png">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="theme-color" content="#000000" />
    <meta property="og:url" content="https://cf2021.pirati.cz/" />
    <meta property="og:url" content="https://cf2024.online/" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="CF 2021 | Pirátská strana" />
    <meta property="og:image" content="https://cf2021.pirati.cz/img/og.png" />
    <meta property="og:description" content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021." />
    <meta name="description" content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021." />
    <meta property="og:title" content="CF 2024 | Pirátská strana" />
    <meta property="og:image" content="https://cf2023.online/img/og2024.png" />
    <meta property="og:description" content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024." />
    <meta name="description" content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024." />

    <title>CF 2021 | Pirátská strana</title>
    <title>CF 2023 | Pirátská strana</title>
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -37,11 +37,11 @@
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <link rel="stylesheet" href="%REACT_APP_STYLEGUIDE_URL%/css/styles.css" />
    <script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
    <!--<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>-->
  </head>
  <body class="h-screen">
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root" class="h-screen h-full w-full"></div>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
+2 −2
Original line number Diff line number Diff line
{
  "short_name": "CF2021",
  "name": "Celostátní fórum 2021",
  "short_name": "CF2023",
  "name": "Celostátní fórum 2023",
  "icons": [
    {
      "src": "https://styleguide.pir-test.eu/latest/images/favicons/favicon-32x32.png",
+19 −19
Original line number Diff line number Diff line
import React, { Suspense, useEffect } from "react";
import { Helmet, HelmetProvider } from "react-helmet-async";
import ReactHintFactory from "react-hint";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { KeycloakProvider } from "@react-keycloak/web";
import { ExtraErrorData } from "@sentry/integrations/dist/extraerrordata";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import { ReactKeycloakProvider as KeycloakProvider } from "@react-keycloak/web";
import { extraErrorDataIntegration } from "@sentry/integrations";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { browserTracingIntegration } from "@sentry/browser"

import { loadAnnouncements } from "actions/announcements";
import { loadConfig } from "actions/global-info";
@@ -33,7 +33,7 @@ if (process.env.REACT_APP_SENTRY_DSN) {
  Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    tracesSampleRate: 0.1,
    integrations: [new ExtraErrorData(), new Integrations.BrowserTracing()],
    integrations: [extraErrorDataIntegration(), browserTracingIntegration()],
  });
}

@@ -48,7 +48,7 @@ const onKeycloakEvent = async (event) => {
  if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) {
    Sentry.setUser(keycloak.tokenParsed);

    const kcRoles = keycloak.tokenParsed.roles;
    const kcRoles = keycloak.tokenParsed.roles || [];
    let role = null;

    if (kcRoles.includes("chairman")) {
@@ -89,7 +89,7 @@ const LoadingComponent = (
          className="w-16 mb-2"
          alt="Pirátská strana"
        />
        <h1 className="head-alt-md md:head-alt-lg">Celostátní fórum 2021</h1>
        <h1 className="head-alt-md md:head-alt-lg">Celostátní fórum 2024</h1>
      </div>
      <p className="text-center head-xs md:head-base">Načítám aplikaci ...</p>
    </div>
@@ -105,25 +105,25 @@ const BaseApp = () => {
    <HelmetProvider>
      <Router>
        <Helmet>
          <title>CF 2021 | Pirátská strana</title>
          <title>CF 2024 | Pirátská strana</title>
          <meta
            name="description"
            content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
            content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
          />
          <meta property="og:title" content="CF 2021 | Pirátská strana" />
          <meta property="og:title" content="CF 2024 | Pirátská strana" />
          <meta
            property="og:description"
            content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
            content="Oficiální stránka letošního ročníku on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
          />
        </Helmet>
        <Navbar />
        <Switch>
          <Route exact path="/" children={<Home />} />
          <Route exact path="/program" children={<Program />} />
          <Route exact path="/protocol" children={<Protocol />} />
          <Route exact path="/about" children={<About />} />
          <Route component={NotFound} />
        </Switch>
        <Routes>
          <Route exact path="/" element={<Home />} />
          <Route exact path="/program" element={<Program />} />
          <Route exact path="/protocol" element={<Protocol />} />
          <Route exact path="/about" element={<About />} />
          <Route element={NotFound} />
        </Routes>
        <Footer />
      </Router>
      <ReactHint autoPosition events attribute="data-tip" className="tooltip" />
@@ -155,7 +155,7 @@ const AuthenticatedApp = () => {
  return (
    <>
      <KeycloakProvider
        keycloak={keycloak}
        authClient={keycloak}
        initConfig={keycloakInitConfig}
        LoadingComponent={LoadingComponent}
        onEvent={onKeycloakEvent}
+3 −3
Original line number Diff line number Diff line
import React from "react";
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";

import App from "./App";

test("renders learn react link", () => {
  const { getByText } = render(<App />);
  const linkElement = getByText(/learn react/i);
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
+9 −9
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@ import keyBy from "lodash/keyBy";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";

import { fetch } from "api";
import { fetchApi } from "api";
import { AnnouncementStore } from "stores";
import {
  announcementTypeMappingRev,
@@ -15,7 +15,7 @@ import {
export const loadAnnouncements = createAsyncAction(
  async () => {
    try {
      const resp = await fetch("/announcements");
      const resp = await fetchApi("/announcements");
      const data = await resp.json();
      return successResult(data.data);
    } catch (err) {
@@ -33,7 +33,7 @@ export const loadAnnouncements = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -47,7 +47,7 @@ export const addAnnouncement = createAsyncAction(
        link,
        type: announcementTypeMappingRev[type],
      });
      const resp = await fetch("/announcements", {
      const resp = await fetchApi("/announcements", {
        method: "POST",
        body,
        expectedStatus: 201,
@@ -57,7 +57,7 @@ export const addAnnouncement = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

/**
@@ -70,7 +70,7 @@ export const deleteAnnouncement = createAsyncAction(
   */
  async (item) => {
    try {
      await fetch(`/announcements/${item.id}`, {
      await fetchApi(`/announcements/${item.id}`, {
        method: "DELETE",
        expectedStatus: 204,
      });
@@ -78,7 +78,7 @@ export const deleteAnnouncement = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

/**
@@ -93,7 +93,7 @@ export const updateAnnouncement = createAsyncAction(
  async ({ item, payload }) => {
    try {
      const body = JSON.stringify(payload);
      await fetch(`/announcements/${item.id}`, {
      await fetchApi(`/announcements/${item.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -102,7 +102,7 @@ export const updateAnnouncement = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

const { markSeen: storeSeen } = createSeenWriter(seenAnnouncementsLSKey);
+5 −6
Original line number Diff line number Diff line
import isArray from "lodash/isArray";
import { createAsyncAction, errorResult, successResult } from "pullstate";
import baseFetch from "unfetch";

import { fetch } from "api";
import { fetchApi } from "api";
import { markdownConverter } from "markdown";
import { GlobalInfoStore } from "stores";

export const loadConfig = createAsyncAction(
  async () => {
    try {
      const resp = await fetch("/config");
      const resp = await fetchApi("/config");
      const payload = await resp.json();

      if (!isArray(payload)) {
@@ -39,7 +38,7 @@ export const loadConfig = createAsyncAction(
        });
      }
    },
  }
  },
);

export const loadProtocol = createAsyncAction(
@@ -47,7 +46,7 @@ export const loadProtocol = createAsyncAction(
    const { protocolUrl } = GlobalInfoStore.getRawState();

    try {
      const resp = await baseFetch(protocolUrl);
      const resp = await fetch(protocolUrl);

      if (resp.status !== 200) {
        return errorResult([], `Unexpected status code ${resp.status}`);
@@ -66,5 +65,5 @@ export const loadProtocol = createAsyncAction(
        });
      }
    },
  }
  },
);
+20 −22
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@ import keyBy from "lodash/keyBy";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";

import { fetch } from "api";
import { fetchApi } from "api";
import { AuthStore, PostStore } from "stores";
import {
  createSeenWriter,
@@ -16,7 +16,7 @@ import {
export const loadPosts = createAsyncAction(
  async () => {
    try {
      const resp = await fetch("/posts", { expectedStatus: 200 });
      const resp = await fetchApi("/posts", { expectedStatus: 200 });
      const data = await resp.json();
      return successResult(data.data);
    } catch (err) {
@@ -36,13 +36,11 @@ export const loadPosts = createAsyncAction(
          state.window = {
            items: filteredPosts.map(property("id")),
            itemCount: filteredPosts.length,
            page: 1,
            perPage: 20,
          };
        });
      }
    },
  }
  },
);

export const like = createAsyncAction(
@@ -51,7 +49,7 @@ export const like = createAsyncAction(
   */
  async (post) => {
    try {
      await fetch(`/posts/${post.id}/like`, {
      await fetchApi(`/posts/${post.id}/like`, {
        method: "PATCH",
        expectedStatus: 204,
      });
@@ -71,7 +69,7 @@ export const like = createAsyncAction(
        });
      }
    },
  }
  },
);

export const dislike = createAsyncAction(
@@ -80,7 +78,7 @@ export const dislike = createAsyncAction(
   */
  async (post) => {
    try {
      await fetch(`/posts/${post.id}/dislike`, {
      await fetchApi(`/posts/${post.id}/dislike`, {
        method: "PATCH",
        expectedStatus: 204,
      });
@@ -100,7 +98,7 @@ export const dislike = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -112,7 +110,7 @@ export const addPost = createAsyncAction(async ({ content }) => {
      content,
      type: postsTypeMappingRev["post"],
    });
    await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 });
    await fetchApi(`/posts`, { method: "POST", body, expectedStatus: 201 });
    return successResult();
  } catch (err) {
    return errorResult([], err.toString());
@@ -128,7 +126,7 @@ export const addProposal = createAsyncAction(async ({ content }) => {
      content,
      type: postsTypeMappingRev["procedure-proposal"],
    });
    await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 });
    await fetchApi(`/posts`, { method: "POST", body, expectedStatus: 201 });
    return successResult();
  } catch (err) {
    return errorResult([], err.toString());
@@ -144,7 +142,7 @@ export const hide = createAsyncAction(
   */
  async (post) => {
    try {
      await fetch(`/posts/${post.id}`, {
      await fetchApi(`/posts/${post.id}`, {
        method: "DELETE",
        expectedStatus: 204,
      });
@@ -152,7 +150,7 @@ export const hide = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

/**
@@ -167,7 +165,7 @@ export const edit = createAsyncAction(
      const body = JSON.stringify({
        content: newContent,
      });
      await fetch(`/posts/${post.id}`, {
      await fetchApi(`/posts/${post.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -191,7 +189,7 @@ export const edit = createAsyncAction(

      return false;
    },
  }
  },
);

/**
@@ -206,7 +204,7 @@ export const archive = createAsyncAction(
      const body = JSON.stringify({
        is_archived: true,
      });
      await fetch(`/posts/${post.id}`, {
      await fetchApi(`/posts/${post.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -215,7 +213,7 @@ export const archive = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

/**
@@ -228,7 +226,7 @@ const updateProposalState = async (proposal, state, additionalPayload) => {
    state: postsStateMappingRev[state],
    ...(additionalPayload || {}),
  });
  await fetch(`/posts/${proposal.id}`, {
  await fetchApi(`/posts/${proposal.id}`, {
    method: "PUT",
    body,
    expectedStatus: 204,
@@ -258,7 +256,7 @@ export const announceProposal = createAsyncAction(

      return false;
    },
  }
  },
);

/**
@@ -283,7 +281,7 @@ export const acceptProposal = createAsyncAction(

      return false;
    },
  }
  },
);

/**
@@ -308,7 +306,7 @@ export const rejectProposal = createAsyncAction(

      return false;
    },
  }
  },
);

/**
@@ -335,7 +333,7 @@ export const rejectProposalByChairman = createAsyncAction(

      return false;
    },
  }
  },
);

const { markSeen: storeSeen } = createSeenWriter(seenPostsLSKey);
+16 −16
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ import pick from "lodash/pick";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";

import { fetch } from "api";
import { fetchApi } from "api";
import { markdownConverter } from "markdown";
import { ProgramStore } from "stores";

@@ -13,7 +13,7 @@ import { loadPosts } from "./posts";
export const loadProgram = createAsyncAction(
  async () => {
    try {
      const resp = await fetch("/program");
      const resp = await fetchApi("/program");
      const mappings = await resp.json();
      return successResult(mappings);
    } catch (err) {
@@ -49,17 +49,17 @@ export const loadProgram = createAsyncAction(
                expectedStartAt: parse(
                  entry.expected_start_at,
                  "yyyy-MM-dd HH:mm:ss",
                  new Date()
                  new Date(),
                ),
                expectedFinishAt: entry.expected_finish_at
                  ? parse(
                      entry.expected_finish_at,
                      "yyyy-MM-dd HH:mm:ss",
                      new Date()
                      new Date(),
                    )
                  : undefined,
              };
            }
            },
          )
          .sort((a, b) => a.expectedStartAt - b.expectedStartAt);

@@ -75,7 +75,7 @@ export const loadProgram = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -87,7 +87,7 @@ export const renameProgramPoint = createAsyncAction(
      const body = JSON.stringify({
        title: newTitle,
      });
      await fetch(`/program/${programEntry.id}`, {
      await fetchApi(`/program/${programEntry.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -108,7 +108,7 @@ export const renameProgramPoint = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -124,7 +124,7 @@ export const endProgramPoint = createAsyncAction(
      const body = JSON.stringify({
        is_live: false,
      });
      await fetch(`/program/${programEntry.id}`, {
      await fetchApi(`/program/${programEntry.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -142,7 +142,7 @@ export const endProgramPoint = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -158,7 +158,7 @@ export const activateProgramPoint = createAsyncAction(
      const body = JSON.stringify({
        is_live: true,
      });
      await fetch(`/program/${programEntry.id}`, {
      await fetchApi(`/program/${programEntry.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -179,7 +179,7 @@ export const activateProgramPoint = createAsyncAction(
        loadPosts.run({}, { respectCache: false });
      }
    },
  }
  },
);

/**
@@ -195,7 +195,7 @@ export const openDiscussion = createAsyncAction(
      const body = JSON.stringify({
        discussion_opened: true,
      });
      await fetch(`/program/${programEntry.id}`, {
      await fetchApi(`/program/${programEntry.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -215,7 +215,7 @@ export const openDiscussion = createAsyncAction(
        });
      }
    },
  }
  },
);

/**
@@ -227,7 +227,7 @@ export const closeDiscussion = createAsyncAction(
      const body = JSON.stringify({
        discussion_opened: false,
      });
      await fetch(`/program/${programEntry.id}`, {
      await fetchApi(`/program/${programEntry.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
@@ -247,5 +247,5 @@ export const closeDiscussion = createAsyncAction(
        });
      }
    },
  }
  },
);
+17 −11
Original line number Diff line number Diff line
import * as Sentry from "@sentry/react";
import { createAsyncAction, errorResult, successResult } from "pullstate";

import { fetch } from "api";
import { fetchApi } from "api";
import keycloak from "keycloak";
import { AuthStore, PostStore } from "stores";
import { updateWindowPosts } from "utils";
@@ -12,7 +12,7 @@ export const loadMe = createAsyncAction(
   */
  async () => {
    try {
      const response = await fetch(`/users/me`, {
      const response = await fetchApi(`/users/me`, {
        method: "GET",
        expectedStatus: 200,
      });
@@ -35,7 +35,7 @@ export const loadMe = createAsyncAction(
        });
      }
    },
  }
  },
);

export const ban = createAsyncAction(
@@ -44,7 +44,7 @@ export const ban = createAsyncAction(
   */
  async (user) => {
    try {
      await fetch(`/users/${user.id}/ban`, {
      await fetchApi(`/users/${user.id}/ban`, {
        method: "PATCH",
        expectedStatus: 204,
      });
@@ -52,7 +52,7 @@ export const ban = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

export const unban = createAsyncAction(
@@ -61,7 +61,7 @@ export const unban = createAsyncAction(
   */
  async (user) => {
    try {
      await fetch(`/users/${user.id}/unban`, {
      await fetchApi(`/users/${user.id}/unban`, {
        method: "PATCH",
        expectedStatus: 204,
      });
@@ -69,7 +69,7 @@ export const unban = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

export const inviteToJitsi = createAsyncAction(
@@ -81,7 +81,7 @@ export const inviteToJitsi = createAsyncAction(
      const body = JSON.stringify({
        allowed: true,
      });
      await fetch(`/users/${user.id}/jitsi`, {
      await fetchApi(`/users/${user.id}/jitsi`, {
        method: "PATCH",
        body,
        expectedStatus: 204,
@@ -90,16 +90,22 @@ export const inviteToJitsi = createAsyncAction(
    } catch (err) {
      return errorResult([], err.toString());
    }
  }
  },
);

export const refreshAccessToken = async () => {
  const { isAuthenticated } = AuthStore.getRawState();

  if (!isAuthenticated) {
    return;
  }

  try {
    await keycloak.updateToken(300);
    await keycloak.updateToken(60);
    console.info("[auth] access token refreshed");
  } catch (exc) {
    console.warn(
      "[auth] could not refresh the access token, refresh token possibly expired, logging out"
      "[auth] could not refresh the access token, refresh token possibly expired, logging out",
    );

    Sentry.setUser(null);
+4 −5
Original line number Diff line number Diff line
import baseFetch from "unfetch";

import { AuthStore } from "./stores";

export const fetch = async (
export const fetchApi = async (
  url,
  { headers = {}, expectedStatus = 200, method = "GET", body = null } = {}
  { headers = {}, expectedStatus = 200, method = "GET", body = null } = {},
) => {
  const { isAuthenticated, user } = AuthStore.getRawState();

@@ -16,10 +14,11 @@ export const fetch = async (
    headers["Content-Type"] = "application/json";
  }

  const response = await baseFetch(process.env.REACT_APP_API_BASE_URL + url, {
  const response = await fetch(process.env.REACT_APP_API_BASE_URL + url, {
    body,
    method,
    headers,
    redirect: "follow",
  });

  if (!!expectedStatus && response.status !== expectedStatus) {
+30 −7
Original line number Diff line number Diff line
import React from "react";
import React, { useState } from "react";
import { NavLink } from "react-router-dom";
import useWindowSize from "@rooks/use-window-size";
import classNames from "classnames";

const Footer = () => {
  const { innerWidth } = useWindowSize();
  const [showCfMenu, setShowCfMenu] = useState(false);
  const [showOtherMenu, setShowOtherMenu] = useState(false);
  const isLg = innerWidth >= 1024;

  return (
    <footer className="footer bg-grey-700 text-white">
      <div className="footer__main py-4 lg:py-16 container container--default">
@@ -12,17 +19,25 @@ const Footer = () => {
            className="w-32 md:w-40 pb-6"
          />
          <p className="para hidden md:block md:mb-4 lg:mb-0 text-grey-200">
            Piráti, 2021. Všechna práva vyhlazena. Sdílejte a nechte ostatní
            Piráti, 2024. Všechna práva vyhlazena. Sdílejte a nechte ostatní
            sdílet za stejných podmínek.
          </p>
        </section>
        <section className="footer__main-links bg-grey-700 text-white lg:grid grid-cols-2 gap-4">
          <div className="pt-8 pb-4 lg:py-0">
            <div className="footer-collapsible">
              <span className="text-xl uppercase text-white footer-collapsible__toggle">
                CF 2021
              <span
                className={classNames(
                  "text-xl uppercase text-white footer-collapsible__toggle",
                  {
                    "footer-collapsible__toggle--open": showCfMenu,
                  }
                )}
                onClick={() => setShowCfMenu(!showCfMenu)}
              >
                CF 2024
              </span>{" "}
              <div className="">
              <div className={showCfMenu || isLg ? "" : "hidden"}>
                <ul className="mt-6 space-y-2 text-grey-200">
                  <li>
                    <NavLink to="/">Přímý přenos</NavLink>
@@ -42,10 +57,18 @@ const Footer = () => {
          </div>
          <div className="py-4 lg:py-0 border-t border-grey-400 lg:border-t-0">
            <div className="footer-collapsible">
              <span className="text-xl uppercase text-white footer-collapsible__toggle">
              <span
                className={classNames(
                  "text-xl uppercase text-white footer-collapsible__toggle",
                  {
                    "footer-collapsible__toggle--open": showOtherMenu,
                  }
                )}
                onClick={() => setShowOtherMenu(!showOtherMenu)}
              >
                Otevřenost
              </span>{" "}
              <div className="">
              <div className={showOtherMenu || isLg ? "" : "hidden"}>
                <ul className="mt-6 space-y-2 text-grey-200">
                  <li>
                    <a href="https://ucet.pirati.cz">Transparentní účet</a>
+1 −1
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ const Navbar = ({ onGetHelp }) => {
            to="/"
            className="pl-4 font-bold text-xl lg:border-r lg:border-grey-300 lg:pr-8 hover:no-underline"
          >
            Celostátní fórum 2021
            Celostátní fórum 2024
          </NavLink>
        </div>
        <div className="navbar__menutoggle my-4 flex justify-end lg:hidden">
+1 −1
Original line number Diff line number Diff line
@@ -78,7 +78,7 @@ const AnnouncementEditModal = ({
  return (
    <Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}>
      <form onSubmit={confirm}>
        <Card>
        <Card className="elevation-21">
          <CardBody>
            <div className="flex items-center justify-between mb-4">
              <CardHeadline>Upravit oznámení</CardHeadline>
+8 −4
Original line number Diff line number Diff line
import React from "react";
import classNames from "classnames";

const Card = ({ children, elevation = 21, className }) => {
  const cls = classNames("card", `elevation-${elevation}`, className);
  return <div className={cls}>{children}</div>;
const Card = ({ children, className }, ref) => {
  const cls = classNames("card", className);
  return (
    <div className={cls} ref={ref}>
      {children}
    </div>
  );
};

export default Card;
export default React.forwardRef(Card);
+6 −2
Original line number Diff line number Diff line
import React from "react";
import classNames from "classnames";

const CardBody = ({ children, className }) => {
const CardBody = ({ children, className, ...props }) => {
  const cls = classNames("card__body", className);
  return <div className={cls}>{children}</div>;
  return (
    <div className={cls} {...props}>
      {children}
    </div>
  );
};

export default CardBody;
+1 −1
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ const NotYetStarted = ({ startAt }) => (
      Jejda ...
    </div>
    <h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
      Jednání ještě nebylo zahájeno :(
      Jednání ještě nebylo zahájeno
    </h1>
    <p className="text-xl leading-snug mb-8">
      <span>Jednání celostátního fóra ještě nezačalo. </span>
+6 −8
Original line number Diff line number Diff line
@@ -7,13 +7,10 @@ import { markdownConverter } from "markdown";
import "react-mde/lib/styles/css/react-mde-toolbar.css";
import "./MarkdownEditor.css";

const MarkdownEditor = ({
  value,
  onChange,
  error,
  placeholder = "",
  ...props
}) => {
const MarkdownEditor = (
  { value, onChange, error, placeholder = "", ...props },
  ref
) => {
  const [selectedTab, setSelectedTab] = useState("write");

  const classes = {
@@ -36,6 +33,7 @@ const MarkdownEditor = ({
  return (
    <div className={classNames("form-field", { "form-field--error": !!error })}>
      <ReactMde
        ref={ref}
        value={value}
        onChange={onChange}
        selectedTab={selectedTab}
@@ -53,4 +51,4 @@ const MarkdownEditor = ({
  );
};

export default MarkdownEditor;
export default React.forwardRef(MarkdownEditor);
+1 −1
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@ const ModalConfirm = ({
}) => {
  return (
    <Modal onRequestClose={onClose} {...props}>
      <Card>
      <Card className="elevation-21">
        <CardBody>
          <div className="flex items-center justify-between mb-4">
            <CardHeadline>{title}</CardHeadline>
+13 −40
Original line number Diff line number Diff line
import React from "react";

import Chip from "components/Chip";
// import Chip from "components/Chip";

export { default as Beacon } from "./Beacon";

@@ -9,11 +9,12 @@ export const steps = [
    target: "body",
    content: (
      <>
        <h1 className="head-alt-sm mb-4">Vítej na celostátním fóru 2021</h1>
        <h1 className="head-alt-sm mb-4">Vítej na celostátním fóru 2024</h1>
        <p className="leading-snug text-base">
          Letošní Pirátské fórum bude online. Abychom to celé zvládli,
          připravili jsme tuhle aplikaci, která se snaží alespoň částečně
          nahradit fyzickou přítomnost. Nejprve si vysvětlíme, jak funguje.
            Víme, že volebního zasedání se nemohou zúčastnit všichni.
            Abychom nepřítomným umožnili zasedání lépe sledovat, připravili
            jsme tuhle aplikaci, která umožňuje zasáhnout do rozpravy.
            Nejprve si vysvětlíme, jak funguje.
        </p>
      </>
    ),
@@ -60,24 +61,6 @@ export const steps = [
          <p>
            <strong>Běžné příspěvky</strong> se zobrazí ihned po přidání.
          </p>
          <p>
            <strong>Návrhy postupu</strong> po přidání nejprve zkontroluje
            předsedající a pokud sezná, že je takový návrh přípusný, prohlásí ho
            za{" "}
            <Chip color="blue-300" condensed>
              hlasovatelný
            </Chip>
            . Pro vyjádření podpory používej palce. Na základě míry podpory
            předsedající buď návrh označí za{" "}
            <Chip color="green-400" condensed>
              schválený
            </Chip>
            , nebo za{" "}
            <Chip color="red-600" condensed>
              zamítnutý
            </Chip>
            .
          </p>
          <p>
            U příspěvků se též zobrazuje celková míra podpory. Legenda barevného
            odlišení je následující:
@@ -110,10 +93,15 @@ export const steps = [
              je označen příspěvek, který zatím není ohodnocen.
            </li>
          </ul>
          <p>
            <strong>Návrhy postupui</strong> po přidání nejprve zkontroluje předsedající a pokud sezná,
            že je takový návrh přípusný, prohlásí ho za hlasovatelný a předloží k hlasování
            v plénu. Na základě toho návrh předsedající označí za schválený, nebo za zamítnutý.
          </p>
        </div>
      </>
    ),
    placement: "right",
    placement: "center",
  },
  {
    target: ".joyride-filters",
@@ -132,21 +120,6 @@ export const steps = [
    ),
    placement: "bottom",
  },
  {
    target: ".joyride-pagination",
    content: (
      <>
        <h1 className="head-alt-sm mb-4">Stránkování příspěvků</h1>
        <div className="leading-snug text-base space-y-2">
          <p>
            Příspěvky jsou stránkované <strong>po dvaceti</strong>. Stránky
            můžeš přepínat těmito šipkami.
          </p>
        </div>
      </>
    ),
    placement: "bottom",
  },
  {
    target: ".joyride-announcements",
    content: (
@@ -167,7 +140,7 @@ export const steps = [
      <>
        <h1 className="head-alt-sm mb-4">To je vše!</h1>
        <p className="leading-snug text-base">
          Ať se ti letošní „CFko“ líbí i v těchto ztížených podmínkách.
          Ať se ti letošní „CFko“ líbí.
        </p>
      </>
    ),
+14 −14
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ const Post = ({
  currentUser,
  supportThreshold,
  canThumb,
  reportSeen = true,
  onLike,
  onDislike,
  onHide,
@@ -36,20 +37,21 @@ const Post = ({
  onEdit,
  onArchive,
  onSeen,
  ...props
}) => {
  const { ref, inView } = useInView({
    threshold: 0.8,
    trackVisibility: true,
    delay: 1500,
    skip: seen,
    delay: 1000,
    skip: !reportSeen,
    triggerOnce: true,
  });

  useEffect(() => {
    if (!seen && inView && onSeen) {
    if (inView && onSeen) {
      onSeen();
    }
  });
  }, [inView, onSeen]);

  const wrapperClassName = classNames(
    "flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2 transition duration-500",
@@ -193,13 +195,13 @@ const Post = ({
  const thumbsVisible = !archived && (type === "post" || state === "announced");

  return (
    <div className={wrapperClassName} ref={ref}>
    <div className={wrapperClassName} ref={ref} {...props}>
      <img
        src={`https://a.pirati.cz/piratar/200/${author.username}.jpg`}
        className="w-8 h-8 lg:w-14 lg:h-14 mr-3 rounded object-cover"
        alt={author.name}
      />
      <div className="flex-1 w-full">
      <div className="flex-1 overflow-hidden">
        <div className="mb-1">
          <div className="flex justify-between items-start xl:items-center">
            <div className="flex flex-col xl:flex-row xl:items-center">
@@ -246,7 +248,7 @@ const Post = ({
                supportThreshold={supportThreshold}
              />
              {showActions && (
                <DropdownMenu right className="pl-4">
                <DropdownMenu right className="pl-4 static">
                  {showAnnounceAction && (
                    <DropdownMenuItem
                      onClick={onAnnounceProcedureProposal}
@@ -325,15 +327,13 @@ const Post = ({
        <div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
          {labels}
        </div>
        <div className="overflow-hidden">
        <div
            className="text-sm lg:text-base text-black leading-normal content-block overflow-x-scroll"
          className="text-sm lg:text-base text-black leading-normal content-block overflow-x-auto overflow-y-hidden mt-1"
          dangerouslySetInnerHTML={htmlContent}
        ></div>
      </div>
    </div>
    </div>
  );
};

export default Post;
export default React.memo(Post);
+1 −1
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ const PostEditModal = ({
  return (
    <Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}>
      <form onSubmit={confirm}>
        <Card>
        <Card className="elevation-21">
          <CardBody>
            <div className="flex items-center justify-between mb-4">
              <CardHeadline>Upravit text příspěvku</CardHeadline>
+55 −40
Original line number Diff line number Diff line
import React from "react";
import React, { useCallback, useMemo, useState } from "react";
import classNames from "classnames";

import Post from "./Post";
@@ -30,6 +30,9 @@ const PostList = ({
    responderFn(post);
  };

  const windowSize = 20;
  const [window, setWindow] = useState(windowSize);

  const onPostLike = buildHandler(onLike);
  const onPostDislike = buildHandler(onDislike);
  const onPostEdit = buildHandler(onEdit);
@@ -47,15 +50,27 @@ const PostList = ({
    onRejectProcedureProposalByChairman
  );

  const onPostSeen = (post) => () => {
  const onPostSeen = useCallback(
    (post) => () => {
      if (!post.seen) {
        onSeen(post);
  };
      }

      // Once last post in window is reached, attempt show more.
      if (items.indexOf(post) === window - 1) {
        setWindow(window + windowSize);
      }
    },
    [items, onSeen, window]
  );

  const windowItems = useMemo(() => {
    return items.slice(0, window).filter((item) => !item.hidden);
  }, [items, window]);

  return (
    <div className={classNames("space-y-px", className)}>
      {items
        .filter((item) => !item.hidden)
        .map((item) => (
      {windowItems.map((item, idx) => (
        <Post
          key={item.id}
          datetime={item.datetime}
@@ -64,9 +79,9 @@ const PostList = ({
          state={item.state}
          content={item.contentHtml}
          ranking={item.ranking}
            historyLog={item.historyLog}
          modified={item.modified}
          seen={item.seen}
          reportSeen={!item.seen || idx === window - 1}
          archived={item.archived}
          dimIfArchived={dimArchived}
          currentUser={currentUser}
+1 −1
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ const RejectPostModalConfirm = ({
}) => {
  return (
    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
      <Card>
      <Card className="elevation-21">
        <CardBody>
          <div className="flex items-center justify-between mb-4">
            <CardHeadline>{title}</CardHeadline>
+1 −1
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ const ProgramEntryEditModal = ({

  return (
    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
      <Card>
      <Card className="elevation-21">
        <CardBody>
          <div className="flex items-center justify-between mb-4">
            <CardHeadline>Upravit název programového bodu</CardHeadline>
+5 −3
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ const AddAnnouncementForm = ({ className }) => {
  };

  const onAdd = async (evt) => {
    evt.preventDefault();

    let preventAction = false;
    const payload = {
      content: text,
@@ -82,7 +84,7 @@ const AddAnnouncementForm = ({ className }) => {
  };

  return (
    <div className={className}>
    <form className={className} onSubmit={onAdd}>
      {addingError && (
        <ErrorMessage>
          Při přidávání oznámení došlo k problému: {addingError}.
@@ -150,7 +152,7 @@ const AddAnnouncementForm = ({ className }) => {
      </div>

      <Button
        onClick={onAdd}
        type="submit"
        className="text-sm mt-4"
        hoverActive
        loading={adding}
@@ -159,7 +161,7 @@ const AddAnnouncementForm = ({ className }) => {
      >
        Přidat oznámení
      </Button>
    </div>
    </form>
  );
};

+185 −82
Original line number Diff line number Diff line
import React, { useState } from "react";
import React, { useCallback, useRef, useState } from "react";
import useOutsideClick from "@rooks/use-outside-click";
import useTimeout from "@rooks/use-timeout";
import classNames from "classnames";

import { addPost, addProposal } from "actions/posts";
import Button from "components/Button";
import { Card, CardBody } from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import MarkdownEditor from "components/mde/MarkdownEditor";
import { useActionState } from "hooks";

const AddPostForm = ({ className }) => {
const AddPostForm = ({ className, canAddProposal }) => {
  const cardRef = useRef();
  const editorRef = useRef();
  const [expanded, setExpanded] = useState(false);
  const [text, setText] = useState("");
  const [showAddConfirm, setShowAddConfirm] = useState(false);
  const [type, setType] = useState("post");
  const [error, setError] = useState(null);
  const [addingPost, addingPostError] = useActionState(addPost, {
    content: text,
  });
  const [addingProposal, addingProposalError] = useActionState(addPost, {
  const [addingProposal, addingProposalError] = useActionState(addProposal, {
    content: text,
  });

  const apiError = addingPostError || addingProposalError;
  const is429ApiError =
    apiError &&
    apiError.toString().indexOf("Unexpected status code 429") !== -1;

  const onOutsideClick = useCallback(() => {
    setExpanded(false);
  }, [setExpanded]);

  const onWrite = useCallback(
    (evt) => {
      setShowAddConfirm(false);

      if (!expanded) {
        setExpanded(true);
        setTimeout(() => {
          if (
            editorRef.current &&
            editorRef.current.finalRefs.textarea.current
          ) {
            editorRef.current.finalRefs.textarea.current.focus();
          }
        }, 0);
      }
    },
    [setExpanded, expanded, setShowAddConfirm]
  );

  const hideAddConfirm = useCallback(() => {
    setShowAddConfirm(false);
  }, [setShowAddConfirm]);

  useOutsideClick(cardRef, onOutsideClick);

  const { start: enqueueHideAddConfirm } = useTimeout(hideAddConfirm, 2000);

  const onTextInput = (newText) => {
    setText(newText);

@@ -30,6 +74,8 @@ const AddPostForm = ({ className }) => {
  };

  const onAdd = async (evt) => {
    evt.preventDefault();

    if (!!text) {
      if (!error) {
        const result = await (type === "post" ? addPost : addProposal).run({
@@ -38,6 +84,9 @@ const AddPostForm = ({ className }) => {

        if (!result.error) {
          setText("");
          setExpanded(false);
          setShowAddConfirm(true);
          enqueueHideAddConfirm();
        }
      }
    } else {
@@ -45,20 +94,57 @@ const AddPostForm = ({ className }) => {
    }
  };

  const wrapperClass = classNames(
    className,
    "hover:elevation-16 transition duration-500",
    {
      "elevation-4 cursor-text": !expanded && !showAddConfirm,
      "lg:elevation-16 container-padding--zero lg:container-padding--auto": expanded,
    }
  );

  return (
    <div className={className}>
      {addingPostError && (
        <ErrorMessage>
          Při přidávání příspěvku došlo k problému: {addingPostError}.
        </ErrorMessage>
    <Card className={wrapperClass} ref={cardRef}>
      <span
        className={classNames("alert items-center transition duration-500", {
          "alert--success": showAddConfirm,
          "alert--light": !showAddConfirm,
          hidden: expanded,
        })}
        onClick={onWrite}
      >
        <i
          className={classNames("alert__icon text-lg mr-4", {
            "ico--checkmark": showAddConfirm,
            "ico--pencil": !showAddConfirm,
          })}
        />
        {showAddConfirm && <span>Příspěvek byl přidán.</span>}
        {!showAddConfirm && <span>Napiš nový příspěvek ...</span>}
      </span>
      <CardBody
        className={
          "p-4 lg:p-8 " + (showAddConfirm || !expanded ? "hidden" : "")
        }
      >
        <form className="space-y-4" onSubmit={onAdd}>
          {apiError && is429ApiError && (
            <div className="alert alert--warning">
              <i className="alert__icon ico--clock text-lg" />
              <span>
                <strong>Zpomal!</strong> Další příspěvek můžeš přidat nejdříve
                po 1 minutě od přidání posledního.
              </span>
            </div>
          )}
      {addingProposalError && (
          {apiError && !is429ApiError && (
            <ErrorMessage>
          Při přidávání příspěvku došlo k problému: {addingProposalError}.
              Při přidávání příspěvku došlo k problému: {apiError}.
            </ErrorMessage>
          )}

          <MarkdownEditor
            ref={editorRef}
            value={text}
            onChange={onTextInput}
            error={error}
@@ -70,11 +156,20 @@ const AddPostForm = ({ className }) => {
            ]}
          />

      <div className="form-field" onChange={(evt) => setType(evt.target.value)}>
          {canAddProposal && (
            <div
              className="form-field"
              onChange={(evt) => setType(evt.target.value)}
            >
              <div className="form-field__wrapper form-field__wrapper--freeform flex-col sm:flex-row">
                <div className="radio form-field__control">
                  <label>
              <input type="radio" name="postType" value="post" defaultChecked />
                    <input
                      type="radio"
                      name="postType"
                      value="post"
                      defaultChecked
                    />
                    <span className="text-sm sm:text-base">
                      Přidávám <strong>běžný příspěvek</strong>
                    </span>
@@ -83,7 +178,11 @@ const AddPostForm = ({ className }) => {

                <div className="radio form-field__control ml-0 mt-4 sm:mt-0 sm:ml-4">
                  <label>
              <input type="radio" name="postType" value="procedure-proposal" />
                    <input
                      type="radio"
                      name="postType"
                      value="procedure-proposal"
                    />
                    <span className="text-sm sm:text-base">
                      Přidávám <strong>návrh postupu</strong>
                    </span>
@@ -91,6 +190,7 @@ const AddPostForm = ({ className }) => {
                </div>
              </div>
            </div>
          )}

          {type === "procedure-proposal" && (
            <p className="alert alert--light text-sm">
@@ -105,11 +205,12 @@ const AddPostForm = ({ className }) => {

          <div className="space-x-4">
            <Button
          onClick={onAdd}
              type="submit"
              disabled={error || addingPost || addingProposal}
              loading={addingPost || addingProposal}
              fullwidth
              hoverActive
              className="text-sm xl:text-base"
            >
              {type === "post" && "Přidat příspěvek"}
              {type === "procedure-proposal" && "Navrhnout postup"}
@@ -128,7 +229,9 @@ const AddPostForm = ({ className }) => {
              .
            </span>
          </div>
    </div>
        </form>
      </CardBody>
    </Card>
  );
};

+4 −0
Original line number Diff line number Diff line
@@ -8,6 +8,10 @@ import { useActionState } from "hooks";
import { AuthStore } from "stores";

const JitsiInviteCard = () => {

  // docasne zablokovano
  return null;

  const { showJitsiInvitePopup, jitsiPopupDismissed } = AuthStore.useState();
  const [loading, errorMessage] = useActionState(loadMe);

+3 −54
Original line number Diff line number Diff line
import React, { useCallback } from "react";
import pick from "lodash/pick";
import React from "react";

import Chip from "components/Chip";
import Dropdown from "components/Dropdown";
import { PostStore } from "stores";
import { updateWindowPosts } from "utils";

const PostFilters = () => {
  const { window, filters } = PostStore.useState((state) =>
    pick(state, ["window", "filters", "items"])
  );
  const filters = PostStore.useState((state) => state.filters);

  const flagsOptions = [
    { title: "Vše", value: "all" },
@@ -25,19 +21,13 @@ const PostFilters = () => {
    { title: "Jen návrhy", value: "proposalsOnly" },
    { title: "Jen příspěvky", value: "discussionOnly" },
  ];
  const hasNextPage = window.page * window.perPage < window.itemCount;
  const hasPrevPage = window.page > 1;

  const setFilter = (prop, newValue, resetPage = true) => {
  const setFilter = (prop, newValue) => {
    PostStore.update((state) => {
      state.filters[prop] = newValue;
      state.window.itemCount = state.window.items.length;

      updateWindowPosts(state);

      if (resetPage) {
        state.window.page = 1;
      }
    });
  };

@@ -45,24 +35,6 @@ const PostFilters = () => {
  const onSortChange = (newValue) => setFilter("sort", newValue, false);
  const onTypeChange = (newValue) => setFilter("type", newValue);

  const onNextPage = useCallback(() => {
    if (hasNextPage) {
      PostStore.update((state) => {
        state.window.page = state.window.page + 1;
      });
    }
  }, [hasNextPage]);
  const onPrevPage = useCallback(() => {
    if (hasPrevPage) {
      PostStore.update((state) => {
        state.window.page = state.window.page - 1;
      });
    }
  }, [hasPrevPage]);

  const enabledPaginatorClass = "cursor-pointer text-xs";
  const disabledPaginatorClass = "opacity-25 cursor-not-allowed text-xs";

  return (
    <div className="flex flex-col space-y-2 xl:space-y-0 xl:space-x-8 xl:flex-row xl:items-center">
      <div className="-mx-1 joyride-filters">
@@ -85,29 +57,6 @@ const PostFilters = () => {
          className="text-xs ml-1 mt-2 xl:mt-0"
        />
      </div>

      <div className="joyride-pagination">
        <Chip
          color="grey-125"
          className={
            hasPrevPage ? enabledPaginatorClass : disabledPaginatorClass
          }
          hoveractive
          onClick={onPrevPage}
        >
          <span className="ico--chevron-left"></span>
        </Chip>
        <Chip
          color="grey-125"
          className={
            hasNextPage ? enabledPaginatorClass : disabledPaginatorClass
          }
          hoveractive
          onClick={onNextPage}
        >
          <span className="ico--chevron-right"></span>
        </Chip>
      </div>
    </div>
  );
};
+5 −5
Original line number Diff line number Diff line
import React, { useCallback } from "react";
import React, { useCallback, useMemo } from "react";
import pick from "lodash/pick";

import {
@@ -191,9 +191,9 @@ const PostsContainer = ({ className, showAddPostCta }) => {
    [setUserToInvite]
  );

  const sliceStart = (window.page - 1) * window.perPage;
  const sliceEnd = window.page * window.perPage;
  const windowItems = window.items.map((postId) => items[postId]);
  const windowItems = useMemo(() => {
    return window.items.map((postId) => items[postId]);
  }, [items, window.items]);

  return (
    <>
@@ -203,7 +203,7 @@ const PostsContainer = ({ className, showAddPostCta }) => {
        </ErrorMessage>
      )}
      <PostList
        items={windowItems.slice(sliceStart, sliceEnd)}
        items={windowItems}
        showAddPostCta={showAddPostCta}
        canThumb={isAuthenticated}
        onLike={like.run}
+2 −2
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ export const useItemActionConfirm = (actionFn, actionParamsBuilder = null) => {
      if (item) {
        const newActionArgs = (actionParamsBuilder || baseActionParamsBuilder)(
          item,
          args
          args,
        );
        setActionArgs(newActionArgs);
        const result = await actionFn.run(newActionArgs);
@@ -23,7 +23,7 @@ export const useItemActionConfirm = (actionFn, actionParamsBuilder = null) => {
        }
      }
    },
    [item, setItem, actionFn, actionParamsBuilder, setActionArgs]
    [item, setItem, actionFn, actionParamsBuilder, setActionArgs],
  );

  const onActionCancel = useCallback(() => {
+5 −7
Original line number Diff line number Diff line
import React from "react";
import ReactDOM from "react-dom";
import ReactDOM from "react-dom/client";
import ReactModal from "react-modal";

import { refreshAccessToken } from "actions/users";
@@ -7,7 +7,7 @@ import { refreshAccessToken } from "actions/users";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

const root = document.getElementById("root");
const root = ReactDOM.createRoot(document.getElementById("root"));

function handleVisibilityChange() {
  if (!document.hidden) {
@@ -17,14 +17,12 @@ function handleVisibilityChange() {

document.addEventListener("visibilitychange", handleVisibilityChange, false);

ReactDOM.render(
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  root
  </React.StrictMode>
);

ReactModal.setAppElement(root);
ReactModal.setAppElement(document.getElementById("root"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
+1 −1
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@ import Keycloak from "keycloak-js";

// Setup Keycloak instance as needed
// Pass initialization options as required or leave blank to load from 'keycloak.json'
const keycloak = Keycloak({
const keycloak = new Keycloak({
  url: "https://auth.pirati.cz/auth",
  realm: "pirati",
  clientId: "cf-online",
+26 −16
Original line number Diff line number Diff line
@@ -6,20 +6,30 @@ import { markdownConverter } from "markdown";
const content = markdownConverter.makeHtml(`
**Celostátní fórum Pirátské strany** je [podle Stanov](https://wiki.pirati.cz/rules/st#cl_8_celostatni_forum) nejvyšším orgánem strany a zasedání se podle možností účastní každý člen strany.

Celostátní fórum ve výlučné působnosti:
> #### Celostátní fórum ve výlučné působnosti:
>
> * a. volí a odvolává republikové předsednictvo,
> * b. volí a odvolává členy republikového výboru volené celostátním fórem,
> * c. zřizuje a ruší komise a odbory na celostátní úrovni,
> * d. volí a odvolává členy komise a vedoucího odboru,
> * e. schvaluje změny stanov,
> * f. projednává a schvaluje výroční zprávu předsedy strany,
> * g. mimořádně přezkoumává rozhodnutí orgánu strany,
> * h. schvaluje zakládací dokument politického institutu,
> * i. může schválit Předpis o institutu,
> * j. může volit a odvolávat některé členy správní rady politického institutu.
>
> #### Celostátní fórum dále
>
> * a. přijímá v mezích stanov další předpisy,
> * b. ukládá úkoly republikovému předsednictvu a republikovému výboru,
> * c. může projednávat a schvalovat základní programové a ideové dokumenty,
> * d. má veškerou působnost, kterou stanovy neurčují jinému orgánu strany.

a. volí a odvolává republikové předsednictvo,
b. volí a odvolává členy republikového výboru volené celostátním fórem,
c. zřizuje a ruší komise a odbory,
d. volí a odvolává členy komise a vedoucího odboru,
e. schvaluje změny stanov,
f. projednává a schvaluje výroční zprávu předsedy strany,
g. projednává a schvaluje výroční finanční zprávu podle ZPS,
h. mimořádně přezkoumává rozhodnutí orgánu strany

### Zasedání na Internetu

Zimní zasedání Celostátního fóru, z důvodu mimořádných okolnosti spojenych s mimořádným stavem, bude probihat **na Internetu**. postup zasedání na Internetu je definovan [§42a](https://wiki.pirati.cz/rules/jdr#zasedani_na_internetu) Jednacího řádu Celostátního fóra v nasledujim znění:
Zasedání Celostátního fóra může z důvodu mimořádných okolností probíhat na Internetu. Postup zasedání na Internetu je definován §42a Jednacího řádu Celostátního fóra v následujícím znění:

> **(1)** Pokud mimořádné okolnosti nedovolují konání běžného zasedání, může, v rámci krizového řízení, republikové předsednictvo pověřit předsedu strany svoláním zasedání na Internetu nebo změnou již svolaného běžného zasedání na zasedání na Internetu.
>
@@ -37,10 +47,10 @@ Zimní zasedání Celostátního fóru, z důvodu mimořádných okolnosti spoje
>
> **(4)** Právo účasti v jednání zvukem a obrazem mají zejména:
>
> a) předsedající a další činovníci jednání,
> b) osoby s právem na závěrečné slovo v rozpravě k bodům k rozhodnutí,
> c) osoby určené navrhovatelem bodu v rozpravě k jiným bodům,
> d) další osoby, pro něž je schválen takový postup.
> * a) předsedající a další činovníci jednání,
> * b) osoby s právem na závěrečné slovo v rozpravě k bodům k rozhodnutí,
> * c) osoby určené navrhovatelem bodu v rozpravě k jiným bodům,
> * d) další osoby, pro něž je schválen takový postup.
>
> **(5)** Jinak se při zasedání na Internetu postupuje přiměřeně jako při běžném zasedání.
>
@@ -58,14 +68,14 @@ const About = () => {
  return (
    <>
      <Helmet>
        <title>Co je to celostátní fórum? | CF 2021 | Pirátská strana</title>
        <title>Co je to celostátní fórum? | CF 2024 | Pirátská strana</title>
        <meta
          name="description"
          content="Nevíte co je to celostátní fórum České pirátské strany? Tady se dočtete vše potřebné."
        />
        <meta
          property="og:title"
          content="Co je to celostátní fórum? | CF 2021 | Pirátská strana"
          content="Co je to celostátní fórum? | CF 2024 | Pirátská strana"
        />
        <meta
          property="og:description"
+63 −34
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import Joyride, { EVENTS } from "react-joyride";
import ReactPlayer from "react-player/lazy";
import { useKeycloak } from "@react-keycloak/web";
import useWindowSize from "@rooks/use-window-size";

import {
@@ -11,7 +12,6 @@ import {
  renameProgramPoint,
} from "actions/program";
import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu";
import ErrorMessage from "components/ErrorMessage";
import {
  AlreadyFinished,
  BreakInProgress,
@@ -67,6 +67,11 @@ const Home = () => {
    onEndProgramPointConfirm,
    onEndProgramPointCancel,
  ] = useActionConfirm(endProgramPoint, programEntry);
  const { keycloak } = useKeycloak();

  const login = useCallback(() => {
    keycloak.login();
  }, [keycloak]);

  useEffect(() => {
    if (isLg && !localStorage.getItem(tourLSKey)) {
@@ -131,18 +136,18 @@ const Home = () => {
  return (
    <>
      <Helmet>
        <title>Přímý přenos | CF 2021 | Pirátská strana</title>
        <title>Přímý přenos | CF 2024 | Pirátská strana</title>
        <meta
          name="description"
          content="Přímý přenos a diskuse z on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Přímý přenos a diskuse z on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
        <meta
          property="og:title"
          content="Přímý přenos | CF 2021 | Pirátská strana"
          content="Přímý přenos | CF 2024 | Pirátská strana"
        />
        <meta
          property="og:description"
          content="Přímý přenos a diskuse z on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Přímý přenos a diskuse z on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
      </Helmet>
      <Joyride
@@ -208,14 +213,14 @@ const Home = () => {
          <div className="pl-4 pt-1 lg:pt-5">
            <div className="space-x-4 inline-flex items-center">
              <button
                className="ico--question text-grey-200 hover:text-black text-lg"
                className="ico--question text-grey-200 hidden lg:block hover:text-black text-lg"
                aria-label="Potřebuješ pomoc? Spusť si znovu nápovědu jak tuhle aplikaci používat."
                data-tip="Potřebuješ pomoc? Spusť si znovu nápovědu jak tuhle aplikaci používat."
                data-tip-at="left"
                data-tip-at="top"
                onClick={showTutorial}
              />
              {displayActions && (
                <DropdownMenu right triggerSize="lg">
                <DropdownMenu right triggerSize="lg" className="z-20">
                  <DropdownMenuItem
                    onClick={() => setShowProgramEditModal(true)}
                    icon="ico--pencil"
@@ -253,7 +258,11 @@ const Home = () => {
            </div>
          </div>
        </div>
        <section className="cf2021__video">
        <section
          className="cf2021__video"
          // This prevents overflowing on very long lines without spaces on mobile, 2rem compensates container-padding--zero.
          style={{ maxWidth: "calc(100vw - 2rem)" }}
        >
          <div className="container-padding--zero md:container-padding--auto">
            {streamUrl && (
              <div className="iframe-container joyride-player">
@@ -269,10 +278,12 @@ const Home = () => {
              </div>
            )}
            {!streamUrl && (
              <p>
                Server neposlal informaci o aktuálním streamu. Vyčkejte na
                aktualizaci.
              </p>
              <div className="px-4 py-16 lg:py-48 flex items-center justify-center bg-grey-400 text-center">
                <span className="text-lg lg:text-xl text-grey-200">
                  <i className="ico--warning mr-2" /> Stream teď není k
                  dispozici. Vyčkej na aktualizaci.
                </span>
              </div>
            )}
            <GlobalStats />
          </div>
@@ -289,42 +300,60 @@ const Home = () => {
          </div>
        </section>

        <section className="cf2021__posts joyride-posts">
        {/* Relative is for fixing the dropdowns on the right which are detached from their immediate container. */}
        <section
          className="cf2021__posts relative joyride-posts"
          // This prevents overflowing on very long lines without spaces on mobile, 2rem compensates container-padding--zero.
          style={{ maxWidth: "calc(100vw - 2rem)" }}
        >
          <div className="flex flex-col xl:flex-row xl:justify-between xl:items-center mb-4">
            <h2 className="head-heavy-xs md:head-heavy-sm whitespace-no-wrap">
              <span>Příspěvky v rozpravě</span>
              {!programEntry.discussionOpened && (
                <i
                  className="ico--lock text-black ml-2 opacity-50 hover:opacity-100 transition duration-500 text-xl"
                  title="Rozprava je uzavřena"
                />
              )}
            </h2>
            <PostFilters />
          </div>

          <PostsContainer
            className="container-padding--zero lg:container-padding--auto"
            showAddPostCta={programEntry.discussionOpened}
          />
          {!programEntry.discussionOpened &&
            (!isAuthenticated || (isAuthenticated && !user.isBanned)) && (
              <p className="leading-normal">
              <p className="alert alert--light items-center mb-4 elevation-4">
                <i className="alert__icon ico--lock text-lg" />
                Rozprava je uzavřena - příspěvky teď nelze přidávat.
              </p>
            )}
          {programEntry.discussionOpened && !isAuthenticated && (
            <p className="alert alert--light items-center mb-4">
              <i className="alert__icon ico--info text-lg" />
              <span>
                Pokud chceš přidat nový příspěvek,{"  "}
                <button onClick={login} className="underline cursor-pointer">
                  přihlaš se pomocí Pirátské identity
                </button>
                .
              </span>
            </p>
          )}
          {programEntry.discussionOpened && isAuthenticated && user.isBanned && (
            <p className="alert alert--error items-center mb-4">
              <i className="alert__icon ico--warning text-lg" />
              Jejda! Nemůžeš přidávat příspěvky, protože máš ban. Vyčkej než ti
              ho předsedající odebere.
            </p>
          )}
          {programEntry.discussionOpened &&
            isAuthenticated &&
            !user.isBanned && <AddPostForm className="my-8 space-y-4" />}

          {programEntry.discussionOpened &&
            isAuthenticated &&
            user.isBanned && (
              <ErrorMessage className="mt-8">
                Jejda! Nemůžeš přidávat příspěvky, protože máš ban. Vyčkej než
                ti ho předsedající odebere.
              </ErrorMessage>
            !user.isBanned && (
              <AddPostForm
                className="mb-8"
                canAddProposal={
                  user.role === "member" || user.role === "chairman"
                }
              />
            )}

          <PostsContainer
            className="container-padding--zero lg:container-padding--auto"
            showAddPostCta={programEntry.discussionOpened}
          />
        </section>
      </article>
      <ProgramEntryEditModal
+2 −2
Original line number Diff line number Diff line
@@ -6,9 +6,9 @@ import Button from "components/Button";
const NotFound = () => (
  <>
    <Helmet>
      <title>404ka | CF 2021 | Pirátská strana</title>
      <title>404ka | CF 2024 | Pirátská strana</title>
      <meta name="description" content="Tahle stránka tu není." />
      <meta property="og:title" content="404ka | CF 2021 | Pirátská strana" />
      <meta property="og:title" content="404ka | CF 2024 | Pirátská strana" />
      <meta property="og:description" content="Tahle stránka tu není." />
    </Helmet>
    <article className="container container--default py-8 lg:py-24">
+7 −4
Original line number Diff line number Diff line
@@ -28,22 +28,25 @@ const Schedule = () => {
  return (
    <>
      <Helmet>
        <title>Program zasedání | CF 2021 | Pirátská strana</title>
        <title>Program zasedání | CF 2024 | Pirátská strana</title>
        <meta
          name="description"
          content="Přečtěte si program on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Přečtěte si program on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
        <meta
          property="og:title"
          content="Program zasedání | CF 2021 | Pirátská strana"
          content="Program zasedání | CF 2024 | Pirátská strana"
        />
        <meta
          property="og:description"
          content="Přečtěte si program on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Přečtěte si program on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
      </Helmet>
      <article className="container container--default py-8 lg:py-24">
        <h1 className="head-alt-md lg:head-alt-lg mb-8">Program zasedání</h1>
        <div class="my-4">
           Program zde neobsahuje z technických důvodů všechny podrobnosti. Kompletní program naleznete na <a href="https://cf2024.pirati.cz/program">webu</a>.
        </div>
        <div className="flex flex-col">
          {scheduleIds.map((id) => {
            const isCurrent = id === currentId;
+4 −4
Original line number Diff line number Diff line
@@ -57,18 +57,18 @@ const Protocol = () => {
  return (
    <>
      <Helmet>
        <title>Zápis ze zasedání | CF 2021 | Pirátská strana</title>
        <title>Zápis ze zasedání | CF 2024 | Pirátská strana</title>
        <meta
          name="description"
          content="Interaktivní zápis z on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Interaktivní zápis z on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
        <meta
          property="og:title"
          content="Zápis ze zasedání | CF 2021 | Pirátská strana"
          content="Zápis ze zasedání | CF 2024 | Pirátská strana"
        />
        <meta
          property="og:description"
          content="Interaktivní zápis z on-line zasedání Celostátního fóra České pirátské strany, 9. 1. 2021."
          content="Interaktivní zápis z on-line zasedání Celostátního fóra České pirátské strany, 13. 1. 2024."
        />
      </Helmet>
      <article className="container container--default py-8 lg:py-24">
+5 −5
Original line number Diff line number Diff line
@@ -16,8 +16,8 @@ const isLocalhost = Boolean(
    window.location.hostname === "[::1]" ||
    // 127.0.0.0/8 are considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
    ),
);

export function register(config) {
@@ -43,7 +43,7 @@ export function register(config) {
        navigator.serviceWorker.ready.then(() => {
          console.log(
            "This web app is being served cache-first by a service " +
              "worker. To learn more, visit https://bit.ly/CRA-PWA"
              "worker. To learn more, visit https://bit.ly/CRA-PWA",
          );
        });
      } else {
@@ -71,7 +71,7 @@ function registerValidSW(swUrl, config) {
              // content until all client tabs are closed.
              console.log(
                "New content is available and will be used when all " +
                  "tabs for this page are closed. See https://bit.ly/CRA-PWA."
                  "tabs for this page are closed. See https://bit.ly/CRA-PWA.",
              );

              // Execute callback
@@ -123,7 +123,7 @@ function checkValidServiceWorker(swUrl, config) {
    })
    .catch(() => {
      console.log(
        "No internet connection found. App is running in offline mode."
        "No internet connection found. App is running in offline mode.",
      );
    });
}
+1 −3
Original line number Diff line number Diff line
@@ -48,8 +48,6 @@ const postStoreInitial = {
  window: {
    items: [],
    itemCount: 0,
    page: 1,
    perPage: 20,
  },
  filters: {
    flags: "active",
@@ -65,7 +63,7 @@ export const getGroupByCode = memoize(
  (groupMappings, groupCode) => {
    return groupMappings.find((gm) => gm.code === groupCode);
  },
  (groupMappings, groupCode) => [groupMappings, groupCode]
  (groupMappings, groupCode) => [groupMappings, groupCode],
);

export const getGroupsByCode = memoize((groupMappings, groupCodes) => {
+6 −5
Original line number Diff line number Diff line
@@ -7,7 +7,8 @@ import WaitQueue from "wait-queue";

import { markdownConverter } from "markdown";

export const urlRegex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
export const urlRegex =
  /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
export const seenPostsLSKey = "cf2021_seen_posts";
export const seenAnnouncementsLSKey = "cf2021_seen_announcements";

@@ -38,7 +39,7 @@ export const filterPosts = (filters, allItems) => {

  if (!filters.showPendingProposals) {
    filteredItems = filteredItems.filter(
      (item) => item.type === "post" || item.state !== "pending"
      (item) => item.type === "post" || item.state !== "pending",
    );
  }

@@ -55,7 +56,7 @@ export const filterPosts = (filters, allItems) => {
 */
export const updateWindowPosts = (state) => {
  state.window.items = filterPosts(state.filters, values(state.items)).map(
    property("id")
    property("id"),
  );
};

@@ -168,7 +169,7 @@ export const parseRawAnnouncement = (rawAnnouncement) => {
    datetime: parse(
      rawAnnouncement.datetime,
      "yyyy-MM-dd HH:mm:ss",
      new Date()
      new Date(),
    ),
    type: announcementTypeMapping[rawAnnouncement.type],
    seen: isSeen(seenAnnouncementsLSKey, rawAnnouncement.id),
@@ -183,7 +184,7 @@ export const createSeenWriter = (localStorageKey) => {
  const seenWriterWorker = async () => {
    const id = await queue.shift();
    const seen = new Set(
      (localStorage.getItem(localStorageKey) || "").split(",")
      (localStorage.getItem(localStorageKey) || "").split(","),
    );

    seen.add(id);
+2 −2
Original line number Diff line number Diff line
@@ -103,7 +103,7 @@ export const connect = ({ url, onConnect }) => {
      });
      console.log(
        "[ws] Socket is closed. Reconnect will be attempted in 1 second.",
        event.reason
        event.reason,
      );

      clearInterval(keepAliveInterval);
@@ -114,7 +114,7 @@ export const connect = ({ url, onConnect }) => {
      console.error(
        "[ws] Socket encountered error: ",
        err.message,
        "Closing socket"
        "Closing socket",
      );
      ws.close();
      reject(err);
+1 −1
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@ export const handleAnnouncementChanged = (payload) => {
      if (has(payload, "content")) {
        state.items[payload.id].content = payload.content;
        state.items[payload.id].contentHtml = markdownConverter.makeHtml(
          payload.content
          payload.content,
        );
      }
      if (has(payload, "link")) {
+18 −7
Original line number Diff line number Diff line
import has from "lodash/has";
import throttle from "lodash/throttle";

import { markdownConverter } from "markdown";
import { PostStore } from "stores";
import { parseRawPost, postsStateMapping, updateWindowPosts } from "utils";

/**
 * Re-apply sorting by rank but no more than once every 3 seconds.
 */
const sortOnRankThrottled = throttle(() => {
  PostStore.update((state) => {
    if (state.filters.sort === "byScore") {
      updateWindowPosts(state);
    }
  });
}, 5000);

export const handlePostRanked = (payload) => {
  PostStore.update((state) => {
    if (state.items[payload.id]) {
@@ -12,12 +24,11 @@ export const handlePostRanked = (payload) => {
      state.items[payload.id].ranking.score =
        state.items[payload.id].ranking.likes -
        state.items[payload.id].ranking.dislikes;

      if (state.filters.sort === "byScore") {
        updateWindowPosts(state);
      }
    }
  });

  // Run sorting in a throttled manner.
  sortOnRankThrottled();
};

export const handlePostChanged = (payload) => {
@@ -26,21 +37,21 @@ export const handlePostChanged = (payload) => {
      if (has(payload, "content")) {
        state.items[payload.id].content = payload.content;
        state.items[payload.id].contentHtml = markdownConverter.makeHtml(
          payload.content
          payload.content,
        );
        state.items[payload.id].modified = true;
      }

      if (has(payload, "state")) {
        state.items[payload.id].state = postsStateMapping[payload.state];
        updateWindowPosts(state);
      }

      if (has(payload, "is_archived")) {
        state.items[payload.id].archived = payload.is_archived;
      }

        updateWindowPosts(state);
      }
    }
  });
};

+11 −1
Original line number Diff line number Diff line
import has from "lodash/has";

import { loadPosts } from "actions/posts";
import { markdownConverter } from "markdown";
import { ProgramStore } from "stores";

@@ -19,9 +20,18 @@ export const handleProgramEntryChanged = (payload) => {
      if (has(payload, "description")) {
        state.items[payload.id].description = payload.description;
        state.items[payload.id].htmlContent = markdownConverter.makeHtml(
          payload.description
          payload.description,
        );
      }

      if (has(payload, "is_live") && payload.is_live) {
        state.currentId = payload.id;
      }
    }
  });

  if (has(payload, "is_live") && payload.is_live) {
    // Re-load posts - these are bound directly to the program schedule entry.
    loadPosts.run({}, { respectCache: false });
  }
};
+0 −2
Original line number Diff line number Diff line
@@ -158,8 +158,6 @@ declare namespace CF2021 {
    window: {
      items: string[];
      itemCount: number;
      page: number;
      perPage: number;
    };
    filters: PostStoreFilters;
  }