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
100 files
+ 26150
13653
Compare changes
  • Side-by-side
  • Inline

Files

.dockerignore

0 → 100644
+7 −0
Original line number Diff line number Diff line
*
!docker
!public
!src
!typings
!package*.json
!jsconfig.json

.editorconfig

0 → 100644
+12 −0
Original line number Diff line number Diff line
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[Makefile]
indent_style = tab
indent_size = 4
+4 −1
Original line number Diff line number Diff line
REACT_APP_STYLEGUIDE_URL=http://localhost:3001/
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.pirati.cz/14

.env.development

0 → 100644
+1 −0
Original line number Diff line number Diff line
REACT_APP_STYLEGUIDE_URL=http://localhost:3001

.gitlab-ci.yml

0 → 100644
+25 −0
Original line number Diff line number Diff line
image: node:18.20.4-bullseye

variables:
  SITE_NAME: cf2024.online

  ARTIFACTS_PATH: build
  REACT_APP_API_BASE_URL: /api
  WEBHOOK_URL: https://ha-web.pirati.cz

before_script:
  - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -yq curl

build:
  stage: build
  script:
    - npm install --legacy-peer-deps
    - npm run build
  after_script:
    - 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:
      - $ARTIFACTS_PATH

.nvmrc

0 → 100644
+1 −0
Original line number Diff line number Diff line
18.20.4

Dockerfile

0 → 100644
+25 −0
Original line number Diff line number Diff line
FROM node:18.20.4-alpine AS build

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

WORKDIR /home/node/cf2021

COPY package*.json ./

RUN npm ci --legacy-peer-deps

COPY . .

RUN npm run build

# ---

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

Dockerfile.development

0 → 100644
+24 −0
Original line number Diff line number Diff line
FROM node:18.20.4-alpine

#  libs for development -- most of theme needed for canvas support in tests
RUN apk add --no-cache \
  build-base \
  g++ \
  libpng \
  libpng-dev \
  jpeg-dev \
  pango-dev \
  cairo-dev \
  giflib-dev \
  python3 \
  ;

WORKDIR /app/cf2021

COPY package*.json ./

RUN npm ci --legacy-peer-deps

COPY . .

CMD npm start

Makefile

0 → 100644
+6 −0
Original line number Diff line number Diff line
.PHONY: build-image
build-image:
	docker build \
        --build-arg BUILD_REACT_APP_STYLEGUIDE_URL=$${STYLEGUIDE_PUBLIC_URL:-https://styleguide.pir-test.eu}/$${STYLEGUIDE_VERSION:-latest} \
        --tag cf2021:latest \
        .

compose.yaml

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

docker/nginx.conf

0 → 100644
+43 −0
Original line number Diff line number Diff line
gzip on;
gzip_disable "MSIE [1-6]\\.(?!.*SV1)";
gzip_proxied any;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/x-javascript text/xml application/xml application/rss+xml text/javascript image/x-icon image/bmp image/svg+xml;
gzip_vary on;

server {
  listen 80;
  root   /usr/share/nginx/html;

  location / {
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;

    # X-Frame-Options is to prevent from clickJacking attack
    add_header X-Frame-Options SAMEORIGIN;
    # disable content-type sniffing on some browsers.
    add_header X-Content-Type-Options nosniff;
    # This header enables the Cross-site scripting (XSS) filter
    add_header X-XSS-Protection "1; mode=block";
    # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
    add_header Referrer-Policy "no-referrer-when-downgrade";
    add_header Cache-Control "no-store, no-cache, must-revalidate";
  }

  location /static {
    expires 1y;
    access_log off;

    # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
    add_header Referrer-Policy "no-referrer-when-downgrade";
    add_header Cache-Control "public";
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }
}

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;
+1 −1
Original line number Diff line number Diff line
@@ -7,6 +7,6 @@
  },
  "include": [
    "./src/**/*",
    "./typings/**/*",
    "./typings/**/*"
  ]
}
+19987 −12853

File changed.

Preview size limit exceeded, changes collapsed.

+51 −86
Original line number Diff line number Diff line
@@ -3,18 +3,35 @@
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@react-keycloak/web": "^2.1.4",
    "@sentry/react": "^5.23.0",
    "classnames": "^2.2.6",
    "date-fns": "^2.16.1",
    "keycloak-js": "^10.0.2",
    "pullstate": "^1.20.4",
    "react": "^16.13.1",
    "react-device-detect": "^1.13.1",
    "react-dom": "^16.13.1",
    "react-modal": "^3.11.2",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3"
    "@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.15"
  },
  "scripts": {
    "start": "react-scripts start",
@@ -22,66 +39,8 @@
    "test": "react-scripts test --env=jsdom-fourteen",
    "eject": "react-scripts eject",
    "lint": "eslint --cache 'src/**/*.{js,jsx}'",
    "lint:fix": "eslint --cache --fix 'src/**/*.{js,jsx}'"
  },
  "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"
            ],
            [
              "^(components|containers|pages|utils)(/.*|$)"
            ],
            [
              "^(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"
    }
    "lint:fix": "eslint --cache --fix 'src/**/*.{js,jsx}'",
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  },
  "browserslist": {
    "production": [
@@ -96,21 +55,27 @@
    ]
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/user-event": "^7.2.1",
    "@testing-library/react": "^9.5.0",
    "@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.6.0",
    "eslint-config-airbnb": "^18.2.0",
    "eslint-config-prettier": "^6.11.0",
    "eslint-plugin-import": "^2.22.0",
    "eslint-plugin-jest-dom": "^3.2.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"
    "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"
  }
}

public/favicon.ico

deleted100644 → 0
−3.08 KiB

3.08 KiB

public/img/og.afdesign

0 → 100644
+58.2 KiB

File added.

No diff preview for this file type.

public/img/og2021.png

0 → 100644
+124 KiB

124.02 KiB

public/img/og2024.png

0 → 100644
+43.1 KiB

43.08 KiB

+37 −10
Original line number Diff line number Diff line
@@ -2,14 +2,26 @@
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- 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="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, shrink-to-fit=no" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <meta property="og:url" content="https://cf2024.online/" />
    <meta property="og:type" content="website" />
    <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 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/
@@ -25,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" />
    <title>React App</title>
    <!--<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.
@@ -41,4 +53,19 @@
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
  <!-- Matomo -->
  <script type="text/javascript">
    var _paq = window._paq = window._paq || [];
    /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
    _paq.push(['trackPageView']);
    _paq.push(['enableLinkTracking']);
    (function() {
      var u="//matomo.pirati.cz/";
      _paq.push(['setTrackerUrl', u+'matomo.php']);
      _paq.push(['setSiteId', '%REACT_APP_MATOMO_ID%']);
      var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
      g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
    })();
  </script>
  <!-- End Matomo Code -->
</html>

public/logo192.png

deleted100644 → 0
−5.22 KiB

5.22 KiB

public/logo512.png

deleted100644 → 0
−9.44 KiB

9.44 KiB

+9 −9
Original line number Diff line number Diff line
{
  "short_name": "React App",
  "name": "Create React App Sample",
  "short_name": "CF2023",
  "name": "Celostátní fórum 2023",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "src": "https://styleguide.pir-test.eu/latest/images/favicons/favicon-32x32.png",
      "sizes": "32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
      "src": "https://styleguide.pir-test.eu/latest/images/favicons/favicon-96x96.png",
      "sizes": "96x96 64x64",
      "type": "image/x-icon"
    },
    {
      "src": "logo512.png",
      "src": "https://styleguide.pir-test.eu/latest/images/favicons/favicon-196x196.png",
      "type": "image/png",
      "sizes": "512x512"
      "sizes": "192x192 196x196"
    }
  ],
  "start_url": ".",
+123 −49
Original line number Diff line number Diff line
import React, { useCallback, useEffect } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { KeycloakProvider } from "@react-keycloak/web";
import React, { Suspense, useEffect } from "react";
import { Helmet, HelmetProvider } from "react-helmet-async";
import ReactHintFactory from "react-hint";
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 { AuthStore } from "stores";
import { browserTracingIntegration } from "@sentry/browser"

import { loadAnnouncements } from "actions/announcements";
import { loadConfig } from "actions/global-info";
import { loadPosts } from "actions/posts";
import { loadProgram } from "actions/program";
import { loadMe, refreshAccessToken } from "actions/users";
import { initializeWSChannel } from "actions/ws";
import Footer from "components/Footer";
import Navbar from "components/Navbar";
import About from "pages/About";
import Home from "pages/Home";
import NotFound from "pages/NotFound";
import Program from "pages/Program";
import Protocol from "pages/Protocol";
import { AuthStore, PostStore } from "stores";
import { updateWindowPosts } from "utils";

import keycloak from "./keycloak";

@@ -19,59 +33,114 @@ if (process.env.REACT_APP_SENTRY_DSN) {
  Sentry.init({
    dsn: process.env.REACT_APP_SENTRY_DSN,
    tracesSampleRate: 0.1,
    integrations: [extraErrorDataIntegration(), browserTracingIntegration()],
  });
}

const onKeycloakEvent = (event) => {
const ReactHint = ReactHintFactory(React);

const onKeycloakEvent = async (event) => {
  if (event === "onTokenExpired") {
    console.warn("[auth] access token expired, attempting refresh");
    refreshAccessToken();
  }

  if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) {
    Sentry.setUser(keycloak.tokenParsed);

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

    if (kcRoles.includes("chairman")) {
      role = "chairman";
    } else if (kcRoles.includes("member")) {
      role = "member";
    } else {
      role = "regp";
    }

    AuthStore.update((state) => {
      state.isAuthenticated = true;
      state.user = {
        name: keycloak.tokenParsed.name,
        groups: keycloak.tokenParsed.groups,
        username: keycloak.tokenParsed.preferred_username,
        role,
        accessToken: keycloak.token,
      };
    });

    // Once base user details has been stored, load me details from API.
    loadMe.run();

    PostStore.update((state) => {
      // Only display proposals verified by chairman to other users.
      state.filters.showPendingProposals = role === "chairman";
      updateWindowPosts(state);
    });
  }
};

const LoadingComponent = (
  <div className="h-screen w-screen flex justify-center items-center">
    <div className="text-center">
      <div className="flex flex-col md:flex-row items-center space-x-4 text-center mb-2">
        <img
          src={`${process.env.REACT_APP_STYLEGUIDE_URL}/images/logo-round-black.svg`}
        className="w-20 mb-2 inline-block"
          className="w-16 mb-2"
          alt="Pirátská strana"
        />
      <h1 className="head-heavy-base mb-2">Celostátní fórum 2021</h1>
      <p>Načítám aplikaci ...</p>
        <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>
  </div>
);

const BaseApp = () => {
  const loadGroupMappings = useCallback(async () => {
    const resp = await fetch("https://iapi.pirati.cz/v1/groups");
    const mappings = await resp.json();

    AuthStore.update((state) => {
      state.groupMappings = mappings;
    });
  }, []);

  useEffect(() => {
    loadGroupMappings();
  }, [loadGroupMappings]);
    initializeWSChannel.run();
  }, []);

  return (
    <HelmetProvider>
      <Router>
        <Helmet>
          <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, 13. 1. 2024."
          />
          <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, 13. 1. 2024."
          />
        </Helmet>
        <Navbar />
      <Switch>
        <Route exact path="/" children={<Home />} />
        <Route exact path="/program" children={<Program />} />
      </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" />
    </HelmetProvider>
  );
};

const ConfiguredApp = () => {
  loadConfig.read();
  loadProgram.read();
  loadAnnouncements.read();
  loadPosts.read();

  return (
    <Suspense fallback={LoadingComponent}>
      <BaseApp />
    </Suspense>
  );
};

@@ -86,18 +155,22 @@ const AuthenticatedApp = () => {
  return (
    <>
      <KeycloakProvider
        keycloak={keycloak}
        authClient={keycloak}
        initConfig={keycloakInitConfig}
        LoadingComponent={LoadingComponent}
        onEvent={onKeycloakEvent}
        autoRefreshToken={false}
      >
        <BaseApp />
        <Suspense fallback={LoadingComponent}>
          <ConfiguredApp />
        </Suspense>
      </KeycloakProvider>
    </>
  );
};

const ErrorBoundaryFallback = () => (
const ErrorBoundaryFallback = ({ error }) => {
  return (
    <div className="h-screen w-screen flex justify-center items-center">
      <div className="text-center">
        <h1 className="head-alt-xl text-red-600 mb-4">
@@ -114,6 +187,7 @@ const ErrorBoundaryFallback = () => (
      </div>
    </div>
  );
};

const App = Sentry.withProfiler(() => {
  return (
+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();
});
+120 −0
Original line number Diff line number Diff line
import keyBy from "lodash/keyBy";
import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate";

import { fetchApi } from "api";
import { AnnouncementStore } from "stores";
import {
  announcementTypeMappingRev,
  createSeenWriter,
  parseRawAnnouncement,
  seenAnnouncementsLSKey,
  syncAnnoucementItemIds,
} from "utils";

export const loadAnnouncements = createAsyncAction(
  async () => {
    try {
      const resp = await fetchApi("/announcements");
      const data = await resp.json();
      return successResult(data.data);
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
  {
    postActionHook: ({ result }) => {
      if (!result.error) {
        const announcements = result.payload.map(parseRawAnnouncement);

        AnnouncementStore.update((state) => {
          state.items = keyBy(announcements, property("id"));
          syncAnnoucementItemIds(state);
        });
      }
    },
  },
);

/**
 * Add new announcement.
 */
export const addAnnouncement = createAsyncAction(
  async ({ content, link, type }) => {
    try {
      const body = JSON.stringify({
        content,
        link,
        type: announcementTypeMappingRev[type],
      });
      const resp = await fetchApi("/announcements", {
        method: "POST",
        body,
        expectedStatus: 201,
      });
      const data = await resp.json();
      return successResult(data.data);
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
);

/**
 * Delete existing announcement.
 */
export const deleteAnnouncement = createAsyncAction(
  /**
   *
   * @param {CF2021.Announcement} item
   */
  async (item) => {
    try {
      await fetchApi(`/announcements/${item.id}`, {
        method: "DELETE",
        expectedStatus: 204,
      });
      return successResult({ item });
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
);

/**
 * Update an announcement.
 */
export const updateAnnouncement = createAsyncAction(
  /**
   *
   * @param {CF2021.Announcement} item
   * @param {Object} payload
   */
  async ({ item, payload }) => {
    try {
      const body = JSON.stringify(payload);
      await fetchApi(`/announcements/${item.id}`, {
        method: "PUT",
        body,
        expectedStatus: 204,
      });
      return successResult({ item, payload });
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
);

const { markSeen: storeSeen } = createSeenWriter(seenAnnouncementsLSKey);

/**
 * Mark down user saw this post already.
 * @param {CF2021.Post} post
 */
export const markSeen = (post) => {
  storeSeen(post.id);

  AnnouncementStore.update((state) => {
    state.items[post.id].seen = true;
  });
};
+69 −0
Original line number Diff line number Diff line
import isArray from "lodash/isArray";
import { createAsyncAction, errorResult, successResult } from "pullstate";

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

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

      if (!isArray(payload)) {
        return errorResult([], "Unexpected response format");
      }

      return successResult(payload);
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
  {
    postActionHook: ({ result }) => {
      if (!result.error) {
        GlobalInfoStore.update((state) => {
          result.payload.forEach((rawConfigItem) => {
            if (rawConfigItem.id === "stream_url") {
              state.streamUrl = rawConfigItem.value;
            }
            if (rawConfigItem.id === "websocket_url") {
              state.websocketUrl = rawConfigItem.value;
            }
            if (rawConfigItem.id === "record_url") {
              state.protocolUrl = rawConfigItem.value;
            }
          });
        });
      }
    },
  },
);

export const loadProtocol = createAsyncAction(
  async () => {
    const { protocolUrl } = GlobalInfoStore.getRawState();

    try {
      const resp = await fetch(protocolUrl);

      if (resp.status !== 200) {
        return errorResult([], `Unexpected status code ${resp.status}`);
      }

      return successResult(await resp.text());
    } catch (err) {
      return errorResult([], err.toString());
    }
  },
  {
    postActionHook: ({ result }) => {
      if (!result.error) {
        GlobalInfoStore.update((state) => {
          state.protocol = markdownConverter.makeHtml(result.payload);
        });
      }
    },
  },
);
Loading