Skip to content
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
  • cf2023-euro
  • cf2023-offline
  • cf2024
  • cf2025
  • main
5 results

Target

Select target project
  • to/cf-online-ui
  • vpfafrin/cf2021
2 results
Select Git revision
  • master
1 result
Show changes
Showing
with 401 additions and 211 deletions
import isArray from "lodash/isArray"; import isArray from "lodash/isArray";
import { createAsyncAction, errorResult, successResult } from "pullstate"; import { createAsyncAction, errorResult, successResult } from "pullstate";
import baseFetch from "unfetch";
import { fetch } from "api"; import { fetchApi } from "api";
import { markdownConverter } from "markdown"; import { markdownConverter } from "markdown";
import { GlobalInfoStore } from "stores"; import { GlobalInfoStore } from "stores";
export const loadConfig = createAsyncAction( export const loadConfig = createAsyncAction(
async () => { async () => {
try { try {
const resp = await fetch("/config"); const resp = await fetchApi("/config");
const payload = await resp.json(); const payload = await resp.json();
if (!isArray(payload)) { if (!isArray(payload)) {
...@@ -39,7 +38,7 @@ export const loadConfig = createAsyncAction( ...@@ -39,7 +38,7 @@ export const loadConfig = createAsyncAction(
}); });
} }
}, },
} },
); );
export const loadProtocol = createAsyncAction( export const loadProtocol = createAsyncAction(
...@@ -47,7 +46,7 @@ export const loadProtocol = createAsyncAction( ...@@ -47,7 +46,7 @@ export const loadProtocol = createAsyncAction(
const { protocolUrl } = GlobalInfoStore.getRawState(); const { protocolUrl } = GlobalInfoStore.getRawState();
try { try {
const resp = await baseFetch(protocolUrl); const resp = await fetch(protocolUrl);
if (resp.status !== 200) { if (resp.status !== 200) {
return errorResult([], `Unexpected status code ${resp.status}`); return errorResult([], `Unexpected status code ${resp.status}`);
...@@ -66,5 +65,5 @@ export const loadProtocol = createAsyncAction( ...@@ -66,5 +65,5 @@ export const loadProtocol = createAsyncAction(
}); });
} }
}, },
} },
); );
...@@ -2,8 +2,8 @@ import keyBy from "lodash/keyBy"; ...@@ -2,8 +2,8 @@ import keyBy from "lodash/keyBy";
import property from "lodash/property"; import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate"; import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api"; import { fetchApi } from "api";
import { PostStore } from "stores"; import { AuthStore, PostStore } from "stores";
import { import {
createSeenWriter, createSeenWriter,
filterPosts, filterPosts,
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
export const loadPosts = createAsyncAction( export const loadPosts = createAsyncAction(
async () => { async () => {
try { try {
const resp = await fetch("/posts", { expectedStatus: 200 }); const resp = await fetchApi("/posts", { expectedStatus: 200 });
const data = await resp.json(); const data = await resp.json();
return successResult(data.data); return successResult(data.data);
} catch (err) { } catch (err) {
...@@ -36,13 +36,11 @@ export const loadPosts = createAsyncAction( ...@@ -36,13 +36,11 @@ export const loadPosts = createAsyncAction(
state.window = { state.window = {
items: filteredPosts.map(property("id")), items: filteredPosts.map(property("id")),
itemCount: filteredPosts.length, itemCount: filteredPosts.length,
page: 1,
perPage: 20,
}; };
}); });
} }
}, },
} },
); );
export const like = createAsyncAction( export const like = createAsyncAction(
...@@ -51,7 +49,7 @@ export const like = createAsyncAction( ...@@ -51,7 +49,7 @@ export const like = createAsyncAction(
*/ */
async (post) => { async (post) => {
try { try {
await fetch(`/posts/${post.id}/like`, { await fetchApi(`/posts/${post.id}/like`, {
method: "PATCH", method: "PATCH",
expectedStatus: 204, expectedStatus: 204,
}); });
...@@ -71,7 +69,7 @@ export const like = createAsyncAction( ...@@ -71,7 +69,7 @@ export const like = createAsyncAction(
}); });
} }
}, },
} },
); );
export const dislike = createAsyncAction( export const dislike = createAsyncAction(
...@@ -80,7 +78,7 @@ export const dislike = createAsyncAction( ...@@ -80,7 +78,7 @@ export const dislike = createAsyncAction(
*/ */
async (post) => { async (post) => {
try { try {
await fetch(`/posts/${post.id}/dislike`, { await fetchApi(`/posts/${post.id}/dislike`, {
method: "PATCH", method: "PATCH",
expectedStatus: 204, expectedStatus: 204,
}); });
...@@ -100,7 +98,7 @@ export const dislike = createAsyncAction( ...@@ -100,7 +98,7 @@ export const dislike = createAsyncAction(
}); });
} }
}, },
} },
); );
/** /**
...@@ -112,7 +110,7 @@ export const addPost = createAsyncAction(async ({ content }) => { ...@@ -112,7 +110,7 @@ export const addPost = createAsyncAction(async ({ content }) => {
content, content,
type: postsTypeMappingRev["post"], type: postsTypeMappingRev["post"],
}); });
await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 }); await fetchApi(`/posts`, { method: "POST", body, expectedStatus: 201 });
return successResult(); return successResult();
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -128,7 +126,7 @@ export const addProposal = createAsyncAction(async ({ content }) => { ...@@ -128,7 +126,7 @@ export const addProposal = createAsyncAction(async ({ content }) => {
content, content,
type: postsTypeMappingRev["procedure-proposal"], type: postsTypeMappingRev["procedure-proposal"],
}); });
await fetch(`/posts`, { method: "POST", body, expectedStatus: 201 }); await fetchApi(`/posts`, { method: "POST", body, expectedStatus: 201 });
return successResult(); return successResult();
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
...@@ -144,7 +142,7 @@ export const hide = createAsyncAction( ...@@ -144,7 +142,7 @@ export const hide = createAsyncAction(
*/ */
async (post) => { async (post) => {
try { try {
await fetch(`/posts/${post.id}`, { await fetchApi(`/posts/${post.id}`, {
method: "DELETE", method: "DELETE",
expectedStatus: 204, expectedStatus: 204,
}); });
...@@ -152,7 +150,7 @@ export const hide = createAsyncAction( ...@@ -152,7 +150,7 @@ export const hide = createAsyncAction(
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
} }
} },
); );
/** /**
...@@ -167,7 +165,7 @@ export const edit = createAsyncAction( ...@@ -167,7 +165,7 @@ export const edit = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
content: newContent, content: newContent,
}); });
await fetch(`/posts/${post.id}`, { await fetchApi(`/posts/${post.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -176,7 +174,22 @@ export const edit = createAsyncAction( ...@@ -176,7 +174,22 @@ export const edit = createAsyncAction(
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
} }
},
{
shortCircuitHook: ({ args }) => {
const { user } = AuthStore.getRawState();
if (!user) {
return errorResult();
}
if (user && user.isBanned) {
return errorResult();
} }
return false;
},
},
); );
/** /**
...@@ -191,7 +204,7 @@ export const archive = createAsyncAction( ...@@ -191,7 +204,7 @@ export const archive = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
is_archived: true, is_archived: true,
}); });
await fetch(`/posts/${post.id}`, { await fetchApi(`/posts/${post.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -200,7 +213,7 @@ export const archive = createAsyncAction( ...@@ -200,7 +213,7 @@ export const archive = createAsyncAction(
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
} }
} },
); );
/** /**
...@@ -213,7 +226,7 @@ const updateProposalState = async (proposal, state, additionalPayload) => { ...@@ -213,7 +226,7 @@ const updateProposalState = async (proposal, state, additionalPayload) => {
state: postsStateMappingRev[state], state: postsStateMappingRev[state],
...(additionalPayload || {}), ...(additionalPayload || {}),
}); });
await fetch(`/posts/${proposal.id}`, { await fetchApi(`/posts/${proposal.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -243,7 +256,7 @@ export const announceProposal = createAsyncAction( ...@@ -243,7 +256,7 @@ export const announceProposal = createAsyncAction(
return false; return false;
}, },
} },
); );
/** /**
...@@ -268,7 +281,7 @@ export const acceptProposal = createAsyncAction( ...@@ -268,7 +281,7 @@ export const acceptProposal = createAsyncAction(
return false; return false;
}, },
} },
); );
/** /**
...@@ -293,7 +306,7 @@ export const rejectProposal = createAsyncAction( ...@@ -293,7 +306,7 @@ export const rejectProposal = createAsyncAction(
return false; return false;
}, },
} },
); );
/** /**
...@@ -320,7 +333,7 @@ export const rejectProposalByChairman = createAsyncAction( ...@@ -320,7 +333,7 @@ export const rejectProposalByChairman = createAsyncAction(
return false; return false;
}, },
} },
); );
const { markSeen: storeSeen } = createSeenWriter(seenPostsLSKey); const { markSeen: storeSeen } = createSeenWriter(seenPostsLSKey);
......
...@@ -4,7 +4,8 @@ import pick from "lodash/pick"; ...@@ -4,7 +4,8 @@ import pick from "lodash/pick";
import property from "lodash/property"; import property from "lodash/property";
import { createAsyncAction, errorResult, successResult } from "pullstate"; import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api"; import { fetchApi } from "api";
import { markdownConverter } from "markdown";
import { ProgramStore } from "stores"; import { ProgramStore } from "stores";
import { loadPosts } from "./posts"; import { loadPosts } from "./posts";
...@@ -12,7 +13,7 @@ import { loadPosts } from "./posts"; ...@@ -12,7 +13,7 @@ import { loadPosts } from "./posts";
export const loadProgram = createAsyncAction( export const loadProgram = createAsyncAction(
async () => { async () => {
try { try {
const resp = await fetch("/program"); const resp = await fetchApi("/program");
const mappings = await resp.json(); const mappings = await resp.json();
return successResult(mappings); return successResult(mappings);
} catch (err) { } catch (err) {
...@@ -37,22 +38,28 @@ export const loadProgram = createAsyncAction( ...@@ -37,22 +38,28 @@ export const loadProgram = createAsyncAction(
"title", "title",
"description", "description",
"proposer", "proposer",
"speakers",
]), ]),
fullTitle:
entry.number !== ""
? `${entry.number}. ${entry.title}`
: entry.title,
htmlContent: markdownConverter.makeHtml(entry.description),
discussionOpened: entry.discussion_opened, discussionOpened: entry.discussion_opened,
expectedStartAt: parse( expectedStartAt: parse(
entry.expected_start_at, entry.expected_start_at,
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
new Date() new Date(),
), ),
expectedFinishAt: entry.expected_finish_at expectedFinishAt: entry.expected_finish_at
? parse( ? parse(
entry.expected_finish_at, entry.expected_finish_at,
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
new Date() new Date(),
) )
: undefined, : undefined,
}; };
} },
) )
.sort((a, b) => a.expectedStartAt - b.expectedStartAt); .sort((a, b) => a.expectedStartAt - b.expectedStartAt);
...@@ -68,7 +75,7 @@ export const loadProgram = createAsyncAction( ...@@ -68,7 +75,7 @@ export const loadProgram = createAsyncAction(
}); });
} }
}, },
} },
); );
/** /**
...@@ -80,7 +87,7 @@ export const renameProgramPoint = createAsyncAction( ...@@ -80,7 +87,7 @@ export const renameProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
title: newTitle, title: newTitle,
}); });
await fetch(`/program/${programEntry.id}`, { await fetchApi(`/program/${programEntry.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -101,7 +108,7 @@ export const renameProgramPoint = createAsyncAction( ...@@ -101,7 +108,7 @@ export const renameProgramPoint = createAsyncAction(
}); });
} }
}, },
} },
); );
/** /**
...@@ -117,7 +124,7 @@ export const endProgramPoint = createAsyncAction( ...@@ -117,7 +124,7 @@ export const endProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
is_live: false, is_live: false,
}); });
await fetch(`/program/${programEntry.id}`, { await fetchApi(`/program/${programEntry.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -135,7 +142,7 @@ export const endProgramPoint = createAsyncAction( ...@@ -135,7 +142,7 @@ export const endProgramPoint = createAsyncAction(
}); });
} }
}, },
} },
); );
/** /**
...@@ -151,7 +158,7 @@ export const activateProgramPoint = createAsyncAction( ...@@ -151,7 +158,7 @@ export const activateProgramPoint = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
is_live: true, is_live: true,
}); });
await fetch(`/program/${programEntry.id}`, { await fetchApi(`/program/${programEntry.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -172,7 +179,7 @@ export const activateProgramPoint = createAsyncAction( ...@@ -172,7 +179,7 @@ export const activateProgramPoint = createAsyncAction(
loadPosts.run({}, { respectCache: false }); loadPosts.run({}, { respectCache: false });
} }
}, },
} },
); );
/** /**
...@@ -188,7 +195,7 @@ export const openDiscussion = createAsyncAction( ...@@ -188,7 +195,7 @@ export const openDiscussion = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
discussion_opened: true, discussion_opened: true,
}); });
await fetch(`/program/${programEntry.id}`, { await fetchApi(`/program/${programEntry.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -208,7 +215,7 @@ export const openDiscussion = createAsyncAction( ...@@ -208,7 +215,7 @@ export const openDiscussion = createAsyncAction(
}); });
} }
}, },
} },
); );
/** /**
...@@ -220,7 +227,7 @@ export const closeDiscussion = createAsyncAction( ...@@ -220,7 +227,7 @@ export const closeDiscussion = createAsyncAction(
const body = JSON.stringify({ const body = JSON.stringify({
discussion_opened: false, discussion_opened: false,
}); });
await fetch(`/program/${programEntry.id}`, { await fetchApi(`/program/${programEntry.id}`, {
method: "PUT", method: "PUT",
body, body,
expectedStatus: 204, expectedStatus: 204,
...@@ -240,5 +247,5 @@ export const closeDiscussion = createAsyncAction( ...@@ -240,5 +247,5 @@ export const closeDiscussion = createAsyncAction(
}); });
} }
}, },
} },
); );
import * as Sentry from "@sentry/react";
import { createAsyncAction, errorResult, successResult } from "pullstate"; import { createAsyncAction, errorResult, successResult } from "pullstate";
import { fetch } from "api"; import { fetchApi } from "api";
import { AuthStore } from "stores"; import keycloak from "keycloak";
import { AuthStore, PostStore } from "stores";
import { updateWindowPosts } from "utils";
export const loadMe = createAsyncAction( export const loadMe = createAsyncAction(
/** /**
* @param {number} userId * @param {number} userId
*/ */
async (user) => { async () => {
try { try {
const response = await fetch(`/users/me`, { const response = await fetchApi(`/users/me`, {
method: "GET", method: "GET",
expectedStatus: 200, expectedStatus: 200,
}); });
...@@ -32,7 +35,7 @@ export const loadMe = createAsyncAction( ...@@ -32,7 +35,7 @@ export const loadMe = createAsyncAction(
}); });
} }
}, },
} },
); );
export const ban = createAsyncAction( export const ban = createAsyncAction(
...@@ -41,7 +44,7 @@ export const ban = createAsyncAction( ...@@ -41,7 +44,7 @@ export const ban = createAsyncAction(
*/ */
async (user) => { async (user) => {
try { try {
await fetch(`/users/${user.id}/ban`, { await fetchApi(`/users/${user.id}/ban`, {
method: "PATCH", method: "PATCH",
expectedStatus: 204, expectedStatus: 204,
}); });
...@@ -49,7 +52,7 @@ export const ban = createAsyncAction( ...@@ -49,7 +52,7 @@ export const ban = createAsyncAction(
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
} }
} },
); );
export const unban = createAsyncAction( export const unban = createAsyncAction(
...@@ -58,13 +61,65 @@ export const unban = createAsyncAction( ...@@ -58,13 +61,65 @@ export const unban = createAsyncAction(
*/ */
async (user) => { async (user) => {
try { try {
await fetch(`/users/${user.id}/unban`, { await fetchApi(`/users/${user.id}/unban`, {
method: "PATCH",
expectedStatus: 204,
});
return successResult(user);
} catch (err) {
return errorResult([], err.toString());
}
},
);
export const inviteToJitsi = createAsyncAction(
/**
* @param {number} userId
*/
async (user) => {
try {
const body = JSON.stringify({
allowed: true,
});
await fetchApi(`/users/${user.id}/jitsi`, {
method: "PATCH", method: "PATCH",
body,
expectedStatus: 204, expectedStatus: 204,
}); });
return successResult(user); return successResult(user);
} catch (err) { } catch (err) {
return errorResult([], err.toString()); return errorResult([], err.toString());
} }
},
);
export const refreshAccessToken = async () => {
const { isAuthenticated } = AuthStore.getRawState();
if (!isAuthenticated) {
return;
} }
try {
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",
); );
Sentry.setUser(null);
AuthStore.update((state) => {
state.isAuthenticated = false;
state.user = null;
state.showJitsiInvitePopup = false;
state.jitsiPopupDimissed = false;
});
PostStore.update((state) => {
state.filters.showPendingProposals = false;
updateWindowPosts(state);
});
}
};
import baseFetch from "unfetch";
import { AuthStore } from "./stores"; import { AuthStore } from "./stores";
export const fetch = async ( export const fetchApi = async (
url, url,
{ headers = {}, expectedStatus = 200, method = "GET", body = null } = {} { headers = {}, expectedStatus = 200, method = "GET", body = null } = {},
) => { ) => {
const { isAuthenticated, user } = AuthStore.getRawState(); const { isAuthenticated, user } = AuthStore.getRawState();
...@@ -16,10 +14,11 @@ export const fetch = async ( ...@@ -16,10 +14,11 @@ export const fetch = async (
headers["Content-Type"] = "application/json"; 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, body,
method, method,
headers, headers,
redirect: "follow",
}); });
if (!!expectedStatus && response.status !== expectedStatus) { if (!!expectedStatus && response.status !== expectedStatus) {
......
import React from "react"; import React, { useState } from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import useWindowSize from "@rooks/use-window-size";
import classNames from "classnames";
const Footer = () => { const Footer = () => {
const { innerWidth } = useWindowSize();
const [showCfMenu, setShowCfMenu] = useState(false);
const [showOtherMenu, setShowOtherMenu] = useState(false);
const isLg = innerWidth >= 1024;
return ( return (
<footer className="footer bg-grey-700 text-white"> <footer className="footer bg-grey-700 text-white">
<div className="footer__main py-4 lg:py-16 container container--default"> <div className="footer__main py-4 lg:py-16 container container--default">
...@@ -12,17 +19,25 @@ const Footer = () => { ...@@ -12,17 +19,25 @@ const Footer = () => {
className="w-32 md:w-40 pb-6" className="w-32 md:w-40 pb-6"
/> />
<p className="para hidden md:block md:mb-4 lg:mb-0 text-grey-200"> <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. sdílet za stejných podmínek.
</p> </p>
</section> </section>
<section className="footer__main-links bg-grey-700 text-white lg:grid grid-cols-3 gap-4"> <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="pt-8 pb-4 lg:py-0">
<div className="footer-collapsible"> <div className="footer-collapsible">
<span className="text-xl uppercase text-white footer-collapsible__toggle"> <span
CF 2021 className={classNames(
"text-xl uppercase text-white footer-collapsible__toggle",
{
"footer-collapsible__toggle--open": showCfMenu,
}
)}
onClick={() => setShowCfMenu(!showCfMenu)}
>
CF 2024
</span>{" "} </span>{" "}
<div className=""> <div className={showCfMenu || isLg ? "" : "hidden"}>
<ul className="mt-6 space-y-2 text-grey-200"> <ul className="mt-6 space-y-2 text-grey-200">
<li> <li>
<NavLink to="/">Přímý přenos</NavLink> <NavLink to="/">Přímý přenos</NavLink>
...@@ -33,16 +48,27 @@ const Footer = () => { ...@@ -33,16 +48,27 @@ const Footer = () => {
<li> <li>
<NavLink to="/protocol">Zápis</NavLink> <NavLink to="/protocol">Zápis</NavLink>
</li> </li>
<li>
<NavLink to="/about">Co je to celostátní fórum?</NavLink>
</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div className="py-4 lg:py-0 border-t border-grey-400 lg:border-t-0"> <div className="py-4 lg:py-0 border-t border-grey-400 lg:border-t-0">
<div className="footer-collapsible"> <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 Otevřenost
</span>{" "} </span>{" "}
<div className=""> <div className={showOtherMenu || isLg ? "" : "hidden"}>
<ul className="mt-6 space-y-2 text-grey-200"> <ul className="mt-6 space-y-2 text-grey-200">
<li> <li>
<a href="https://ucet.pirati.cz">Transparentní účet</a> <a href="https://ucet.pirati.cz">Transparentní účet</a>
......
...@@ -7,7 +7,7 @@ import classNames from "classnames"; ...@@ -7,7 +7,7 @@ import classNames from "classnames";
import Button from "components/Button"; import Button from "components/Button";
import { AuthStore, GlobalInfoStore } from "stores"; import { AuthStore, GlobalInfoStore } from "stores";
const Navbar = () => { const Navbar = ({ onGetHelp }) => {
const { innerWidth } = useWindowSize(); const { innerWidth } = useWindowSize();
const [showMenu, setShowMenu] = useState(); const [showMenu, setShowMenu] = useState();
const { keycloak } = useKeycloak(); const { keycloak } = useKeycloak();
...@@ -36,10 +36,12 @@ const Navbar = () => { ...@@ -36,10 +36,12 @@ const Navbar = () => {
}; };
const connectionIndicator = ( const connectionIndicator = (
<div className="inline-flex items-center order-first md:order-last md:ml-8 lg:order-first lg:mr-8 lg:ml-0"> <div className="inline-flex items-center">
<span <span
className="relative inline-flex h-4 w-4 mr-4" className="relative inline-flex h-4 w-4 mr-4"
title={connectionStateCaption} data-tip={connectionStateCaption}
data-tip-at="left"
aria-label={connectionStateCaption}
> >
<span <span
className={classNames( className={classNames(
...@@ -75,7 +77,7 @@ const Navbar = () => { ...@@ -75,7 +77,7 @@ const Navbar = () => {
to="/" to="/"
className="pl-4 font-bold text-xl lg:border-r lg:border-grey-300 lg:pr-8 hover:no-underline" 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> </NavLink>
</div> </div>
<div className="navbar__menutoggle my-4 flex justify-end lg:hidden"> <div className="navbar__menutoggle my-4 flex justify-end lg:hidden">
...@@ -107,8 +109,10 @@ const Navbar = () => { ...@@ -107,8 +109,10 @@ const Navbar = () => {
</li> </li>
</ul> </ul>
</div> </div>
<div className="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-row items-center"> <div className="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-row items-center justify-between">
<div className="order-last lg:order-first lg:mr-8">
{connectionIndicator} {connectionIndicator}
</div>
{!isAuthenticated && ( {!isAuthenticated && (
<Button className="btn--white joyride-login" onClick={login}> <Button className="btn--white joyride-login" onClick={login}>
Přihlásit se Přihlásit se
...@@ -126,6 +130,9 @@ const Navbar = () => { ...@@ -126,6 +130,9 @@ const Navbar = () => {
<button <button
onClick={logout} onClick={logout}
className="text-grey-200 hover:text-white" className="text-grey-200 hover:text-white"
aria-label="Odhlásit se"
data-tip="Odhlásit se"
data-tip-at="bottom"
> >
<i className="ico--log-out"></i> <i className="ico--log-out"></i>
</button> </button>
......
...@@ -18,14 +18,18 @@ const AnnouncementEditModal = ({ ...@@ -18,14 +18,18 @@ const AnnouncementEditModal = ({
}) => { }) => {
const [text, setText] = useState(announcement.content); const [text, setText] = useState(announcement.content);
const [link, setLink] = useState(announcement.link); const [link, setLink] = useState(announcement.link);
const [linkValid, setLinkValid] = useState(null); const [textError, setTextError] = useState(null);
const [noTextError, setNoTextError] = useState(false); const [linkError, setLinkError] = useState(null);
const onTextInput = (newText) => { const onTextInput = (newText) => {
setText(newText); setText(newText);
if (newText !== "") { if (newText !== "") {
setNoTextError(false); if (newText.length > 1024) {
setTextError("Maximální délka příspěvku je 1024 znaků.");
} else {
setTextError(null);
}
} }
}; };
...@@ -33,7 +37,11 @@ const AnnouncementEditModal = ({ ...@@ -33,7 +37,11 @@ const AnnouncementEditModal = ({
setLink(newLink); setLink(newLink);
if (!!newLink) { if (!!newLink) {
setLinkValid(urlRegex.test(newLink)); if (newLink.length > 1024) {
setLinkError("Maximální délka URL je 256 znaků.");
} else {
setLinkError(urlRegex.test(newLink) ? null : "Zadejte platnou URL.");
}
} }
}; };
...@@ -46,12 +54,15 @@ const AnnouncementEditModal = ({ ...@@ -46,12 +54,15 @@ const AnnouncementEditModal = ({
}; };
if (!text) { if (!text) {
setNoTextError(true); setTextError("Před úpravou oznámení nezapomeňte vyplnit jeho obsah.");
preventAction = true;
} else if (!!text && text.length > 1024) {
setTextError("Maximální délka oznámení je 1024 znaků.");
preventAction = true; preventAction = true;
} }
if (announcement.type === "voting" && !link) { if (announcement.type === "voting" && !link) {
setLinkValid(false); setLinkError("Zadejte platnou URL.");
preventAction = true; preventAction = true;
} else { } else {
payload.link = link; payload.link = link;
...@@ -67,7 +78,7 @@ const AnnouncementEditModal = ({ ...@@ -67,7 +78,7 @@ const AnnouncementEditModal = ({
return ( return (
<Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}> <Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}>
<form onSubmit={confirm}> <form onSubmit={confirm}>
<Card> <Card className="elevation-21">
<CardBody> <CardBody>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit oznámení</CardHeadline> <CardHeadline>Upravit oznámení</CardHeadline>
...@@ -78,11 +89,7 @@ const AnnouncementEditModal = ({ ...@@ -78,11 +89,7 @@ const AnnouncementEditModal = ({
<MarkdownEditor <MarkdownEditor
value={text} value={text}
onChange={onTextInput} onChange={onTextInput}
error={ error={textError}
noTextError
? "Před úpravou oznámení nezapomeňte vyplnit jeho obsah."
: null
}
placeholder="Vyplňte text oznámení" placeholder="Vyplňte text oznámení"
toolbarCommands={[ toolbarCommands={[
["bold", "italic", "strikethrough"], ["bold", "italic", "strikethrough"],
...@@ -92,7 +99,7 @@ const AnnouncementEditModal = ({ ...@@ -92,7 +99,7 @@ const AnnouncementEditModal = ({
<div <div
className={classNames("form-field mt-4", { className={classNames("form-field mt-4", {
hidden: announcement.type !== "voting", hidden: announcement.type !== "voting",
"form-field--error": linkValid === false, "form-field--error": !!linkError,
})} })}
> >
<div className="form-field__wrapper form-field__wrapper--shadowed"> <div className="form-field__wrapper form-field__wrapper--shadowed">
...@@ -107,8 +114,8 @@ const AnnouncementEditModal = ({ ...@@ -107,8 +114,8 @@ const AnnouncementEditModal = ({
<i className="ico--link"></i> <i className="ico--link"></i>
</div> </div>
</div> </div>
{linkValid === false && ( {!!linkError && (
<div className="form-field__error">Zadejte platnou URL.</div> <div className="form-field__error">{linkError}</div>
)} )}
</div> </div>
{error && ( {error && (
...@@ -123,6 +130,7 @@ const AnnouncementEditModal = ({ ...@@ -123,6 +130,7 @@ const AnnouncementEditModal = ({
color="blue-300" color="blue-300"
className="text-sm" className="text-sm"
loading={confirming} loading={confirming}
disabled={textError || linkError || confirming}
type="submit" type="submit"
> >
Uložit Uložit
......
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
const Card = ({ children, elevation = 21, className }) => { const Card = ({ children, className }, ref) => {
const cls = classNames("card", `elevation-${elevation}`, className); const cls = classNames("card", className);
return <div className={cls}>{children}</div>; return (
<div className={cls} ref={ref}>
{children}
</div>
);
}; };
export default Card; export default React.forwardRef(Card);
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
const CardBody = ({ children, className }) => { const CardBody = ({ children, className, ...props }) => {
const cls = classNames("card__body", className); const cls = classNames("card__body", className);
return <div className={cls}>{children}</div>; return (
<div className={cls} {...props}>
{children}
</div>
);
}; };
export default CardBody; export default CardBody;
...@@ -9,7 +9,7 @@ const NotYetStarted = ({ startAt }) => ( ...@@ -9,7 +9,7 @@ const NotYetStarted = ({ startAt }) => (
Jejda ... Jejda ...
</div> </div>
<h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2"> <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> </h1>
<p className="text-xl leading-snug mb-8"> <p className="text-xl leading-snug mb-8">
<span>Jednání celostátního fóra ještě nezačalo. </span> <span>Jednání celostátního fóra ještě nezačalo. </span>
......
...@@ -7,13 +7,10 @@ import { markdownConverter } from "markdown"; ...@@ -7,13 +7,10 @@ import { markdownConverter } from "markdown";
import "react-mde/lib/styles/css/react-mde-toolbar.css"; import "react-mde/lib/styles/css/react-mde-toolbar.css";
import "./MarkdownEditor.css"; import "./MarkdownEditor.css";
const MarkdownEditor = ({ const MarkdownEditor = (
value, { value, onChange, error, placeholder = "", ...props },
onChange, ref
error, ) => {
placeholder = "",
...props
}) => {
const [selectedTab, setSelectedTab] = useState("write"); const [selectedTab, setSelectedTab] = useState("write");
const classes = { const classes = {
...@@ -36,6 +33,7 @@ const MarkdownEditor = ({ ...@@ -36,6 +33,7 @@ const MarkdownEditor = ({
return ( return (
<div className={classNames("form-field", { "form-field--error": !!error })}> <div className={classNames("form-field", { "form-field--error": !!error })}>
<ReactMde <ReactMde
ref={ref}
value={value} value={value}
onChange={onChange} onChange={onChange}
selectedTab={selectedTab} selectedTab={selectedTab}
...@@ -53,4 +51,4 @@ const MarkdownEditor = ({ ...@@ -53,4 +51,4 @@ const MarkdownEditor = ({
); );
}; };
export default MarkdownEditor; export default React.forwardRef(MarkdownEditor);
...@@ -21,11 +21,16 @@ const ModalConfirm = ({ ...@@ -21,11 +21,16 @@ const ModalConfirm = ({
}) => { }) => {
return ( return (
<Modal onRequestClose={onClose} {...props}> <Modal onRequestClose={onClose} {...props}>
<Card> <Card className="elevation-21">
<CardBody> <CardBody>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<CardHeadline>{title}</CardHeadline> <CardHeadline>{title}</CardHeadline>
<button onClick={onClose} type="button"> <button
onClick={onClose}
type="button"
data-tip="Zavřít"
aria-label="Zavřít"
>
<i className="ico--cross"></i> <i className="ico--cross"></i>
</button> </button>
</div> </div>
......
import React from "react"; import React from "react";
import Chip from "components/Chip"; // import Chip from "components/Chip";
export { default as Beacon } from "./Beacon"; export { default as Beacon } from "./Beacon";
...@@ -9,11 +9,12 @@ export const steps = [ ...@@ -9,11 +9,12 @@ export const steps = [
target: "body", target: "body",
content: ( 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"> <p className="leading-snug text-base">
Letošní Pirátské fórum bude online. Abychom to celé zvládli, Víme, že volebního zasedání se nemohou zúčastnit všichni.
připravili jsme tuhle aplikaci, která se snaží alespoň částečně Abychom nepřítomným umožnili zasedání lépe sledovat, připravili
nahradit fyzickou přítomnost. Nejprve si vysvětlíme, jak funguje. jsme tuhle aplikaci, která umožňuje zasáhnout do rozpravy.
Nejprve si vysvětlíme, jak funguje.
</p> </p>
</> </>
), ),
...@@ -60,24 +61,6 @@ export const steps = [ ...@@ -60,24 +61,6 @@ export const steps = [
<p> <p>
<strong>Běžné příspěvky</strong> se zobrazí ihned po přidání. <strong>Běžné příspěvky</strong> se zobrazí ihned po přidání.
</p> </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> <p>
U příspěvků se též zobrazuje celková míra podpory. Legenda barevného U příspěvků se též zobrazuje celková míra podpory. Legenda barevného
odlišení je následující: odlišení je následující:
...@@ -110,10 +93,32 @@ export const steps = [ ...@@ -110,10 +93,32 @@ export const steps = [
je označen příspěvek, který zatím není ohodnocen. je označen příspěvek, který zatím není ohodnocen.
</li> </li>
</ul> </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: "center",
},
{
target: ".joyride-filters",
content: (
<>
<h1 className="head-alt-sm mb-4">Filtrování a řazení příspěvků</h1>
<div className="leading-snug text-base space-y-2">
<p>
Příspěvky v rozpravě můžeš filtrovat <strong>podle typu</strong>{" "}
(návrhy/příspěvky), <strong>podle stavu</strong>{" "}
(aktivní/archivované) a můžeš taky přepínat jejich{" "}
<strong>řazení</strong> (podle podpory, podle času přidání).
</p>
</div> </div>
</> </>
), ),
placement: "right", placement: "bottom",
}, },
{ {
target: ".joyride-announcements", target: ".joyride-announcements",
...@@ -135,7 +140,7 @@ export const steps = [ ...@@ -135,7 +140,7 @@ export const steps = [
<> <>
<h1 className="head-alt-sm mb-4">To je vše!</h1> <h1 className="head-alt-sm mb-4">To je vše!</h1>
<p className="leading-snug text-base"> <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> </p>
</> </>
), ),
......
...@@ -23,11 +23,13 @@ const Post = ({ ...@@ -23,11 +23,13 @@ const Post = ({
currentUser, currentUser,
supportThreshold, supportThreshold,
canThumb, canThumb,
reportSeen = true,
onLike, onLike,
onDislike, onDislike,
onHide, onHide,
onBanUser, onBanUser,
onUnbanUser, onUnbanUser,
onInviteUser,
onAnnounceProcedureProposal, onAnnounceProcedureProposal,
onAcceptProcedureProposal, onAcceptProcedureProposal,
onRejectProcedureProposal, onRejectProcedureProposal,
...@@ -35,20 +37,21 @@ const Post = ({ ...@@ -35,20 +37,21 @@ const Post = ({
onEdit, onEdit,
onArchive, onArchive,
onSeen, onSeen,
...props
}) => { }) => {
const { ref, inView } = useInView({ const { ref, inView } = useInView({
threshold: 0.8, threshold: 0.8,
trackVisibility: true, trackVisibility: true,
delay: 1500, delay: 1000,
skip: seen, skip: !reportSeen,
triggerOnce: true, triggerOnce: true,
}); });
useEffect(() => { useEffect(() => {
if (!seen && inView && onSeen) { if (inView && onSeen) {
onSeen(); onSeen();
} }
}); }, [inView, onSeen]);
const wrapperClassName = classNames( const wrapperClassName = classNames(
"flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2 transition duration-500", "flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2 transition duration-500",
...@@ -88,7 +91,7 @@ const Post = ({ ...@@ -88,7 +91,7 @@ const Post = ({
key="state__pending" key="state__pending"
condensed condensed
color="grey-500" color="grey-500"
title="Návrh čekající na zpracování" aria-label="Návrh čekající na zpracování"
> >
Čeká na zpracování Čeká na zpracování
</Chip> </Chip>
...@@ -98,7 +101,7 @@ const Post = ({ ...@@ -98,7 +101,7 @@ const Post = ({
key="state__announced" key="state__announced"
condensed condensed
color="blue-300" color="blue-300"
title="Návrh k hlasování" aria-label="Návrh k hlasování"
> >
K hlasování K hlasování
</Chip> </Chip>
...@@ -108,7 +111,7 @@ const Post = ({ ...@@ -108,7 +111,7 @@ const Post = ({
key="state__accepted" key="state__accepted"
condensed condensed
color="green-400" color="green-400"
title="Schválený návrh" aria-label="Schválený návrh"
> >
Schválený Schválený
</Chip> </Chip>
...@@ -118,7 +121,7 @@ const Post = ({ ...@@ -118,7 +121,7 @@ const Post = ({
key="state__rejected" key="state__rejected"
condensed condensed
color="red-600" color="red-600"
title="Zamítnutý návrh" aria-label="Zamítnutý návrh"
> >
Zamítnutý Zamítnutý
</Chip> </Chip>
...@@ -128,7 +131,7 @@ const Post = ({ ...@@ -128,7 +131,7 @@ const Post = ({
key="state__rejected-by-chairmen" key="state__rejected-by-chairmen"
condensed condensed
color="red-600" color="red-600"
title="Návrh zamítnutý předsedajícím" aria-label="Návrh zamítnutý předsedajícím"
> >
Zamítnutý předs. Zamítnutý předs.
</Chip> </Chip>
...@@ -163,9 +166,11 @@ const Post = ({ ...@@ -163,9 +166,11 @@ const Post = ({
type === "procedure-proposal" && type === "procedure-proposal" &&
["announced", "pending"].includes(state); ["announced", "pending"].includes(state);
const showEditAction = const showEditAction =
isChairman || (currentUser && currentUser.id === author.id); isChairman ||
(currentUser && currentUser.id === author.id && !currentUser.isBanned);
const showBanAction = isChairman && !author.isBanned; const showBanAction = isChairman && !author.isBanned;
const showUnbanAction = isChairman && author.isBanned; const showUnbanAction = isChairman && author.isBanned;
const showInviteAction = isChairman;
const showHideAction = isChairman && !archived; const showHideAction = isChairman && !archived;
const showArchiveAction = isChairman && !archived; const showArchiveAction = isChairman && !archived;
...@@ -178,6 +183,7 @@ const Post = ({ ...@@ -178,6 +183,7 @@ const Post = ({
showEditAction, showEditAction,
showBanAction, showBanAction,
showUnbanAction, showUnbanAction,
showInviteAction,
showHideAction, showHideAction,
showArchiveAction, showArchiveAction,
].some((item) => !!item); ].some((item) => !!item);
...@@ -189,21 +195,23 @@ const Post = ({ ...@@ -189,21 +195,23 @@ const Post = ({
const thumbsVisible = !archived && (type === "post" || state === "announced"); const thumbsVisible = !archived && (type === "post" || state === "announced");
return ( return (
<div className={wrapperClassName} ref={ref}> <div className={wrapperClassName} ref={ref} {...props}>
<img <img
src={`https://a.pirati.cz/piratar/200/${author.username}.jpg`} src={`https://a.pirati.cz/piratar/200/${author.username}.jpg`}
className="w-8 h-8 lg:w-14 lg:h-14 rounded mr-3 object-cover" className="w-8 h-8 lg:w-14 lg:h-14 mr-3 rounded object-cover"
alt={author.name} alt={author.name}
/> />
<div className="flex-1"> <div className="flex-1 overflow-hidden">
<div className="mb-1"> <div className="mb-1">
<div className="flex justify-between items-start xl:items-center"> <div className="flex justify-between items-start xl:items-center">
<div className="flex flex-col xl:flex-row xl:items-center"> <div className="flex flex-col xl:flex-row xl:items-center">
<div className="flex flex-col xl:flex-row xl:items-center"> <div className="flex flex-col xl:flex-row xl:items-center">
<span className="font-bold">{author.name}</span> <span className="font-bold">{author.name}</span>
<div className="mt-1 xl:mt-0 xl:ml-2 leading-tight"> <div className="mt-1 xl:mt-0 xl:ml-2 leading-tight">
<span className="text-grey-200 text-sm">{author.group}</span> <span className="text-grey-200 text-xs sm:text-sm">
<span className="text-grey-200 ml-1 text-xs"> {author.group}
</span>
<span className="text-grey-200 ml-1 text-xs sm:text-sm">
@{" "} @{" "}
{format( {format(
datetime, datetime,
...@@ -234,13 +242,13 @@ const Post = ({ ...@@ -234,13 +242,13 @@ const Post = ({
)} )}
<PostScore <PostScore
className="ml-2" className="ml-2"
score={ranking.score} postType={type}
hasDislikes={ranking.dislikes > 0} ranking={ranking}
rankingReadonly={!thumbsVisible} rankingReadonly={!thumbsVisible}
supportThreshold={supportThreshold} supportThreshold={supportThreshold}
/> />
{showActions && ( {showActions && (
<DropdownMenu right className="pl-4"> <DropdownMenu right className="pl-4 static">
{showAnnounceAction && ( {showAnnounceAction && (
<DropdownMenuItem <DropdownMenuItem
onClick={onAnnounceProcedureProposal} onClick={onAnnounceProcedureProposal}
...@@ -290,6 +298,13 @@ const Post = ({ ...@@ -290,6 +298,13 @@ const Post = ({
title="Odblokovat uživatele" title="Odblokovat uživatele"
/> />
)} )}
{showInviteAction && (
<DropdownMenuItem
onClick={onInviteUser}
icon="ico--phone"
title="Pozvat uživatele do Jitsi"
/>
)}
{showHideAction && ( {showHideAction && (
<DropdownMenuItem <DropdownMenuItem
onClick={onHide} onClick={onHide}
...@@ -313,7 +328,7 @@ const Post = ({ ...@@ -313,7 +328,7 @@ const Post = ({
{labels} {labels}
</div> </div>
<div <div
className="text-sm lg:text-base text-black leading-normal content-block" className="text-sm lg:text-base text-black leading-normal content-block overflow-x-auto overflow-y-hidden mt-1"
dangerouslySetInnerHTML={htmlContent} dangerouslySetInnerHTML={htmlContent}
></div> ></div>
</div> </div>
...@@ -321,4 +336,4 @@ const Post = ({ ...@@ -321,4 +336,4 @@ const Post = ({
); );
}; };
export default Post; export default React.memo(Post);
...@@ -15,13 +15,17 @@ const PostEditModal = ({ ...@@ -15,13 +15,17 @@ const PostEditModal = ({
...props ...props
}) => { }) => {
const [text, setText] = useState(post.content); const [text, setText] = useState(post.content);
const [noTextError, setNoTextError] = useState(false); const [textError, setTextError] = useState(null);
const onTextInput = (newText) => { const onTextInput = (newText) => {
setText(newText); setText(newText);
if (newText !== "") { if (newText !== "") {
setNoTextError(false); if (newText.length >= 1024) {
setTextError("Maximální délka příspěvku je 1024 znaků.");
} else {
setTextError(null);
}
} }
}; };
...@@ -31,14 +35,14 @@ const PostEditModal = ({ ...@@ -31,14 +35,14 @@ const PostEditModal = ({
if (!!text) { if (!!text) {
onConfirm(text); onConfirm(text);
} else { } else {
setNoTextError(true); setTextError("Před upravením příspěvku nezapomeňte vyplnit jeho obsah.");
} }
}; };
return ( return (
<Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}> <Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}>
<form onSubmit={confirm}> <form onSubmit={confirm}>
<Card> <Card className="elevation-21">
<CardBody> <CardBody>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit text příspěvku</CardHeadline> <CardHeadline>Upravit text příspěvku</CardHeadline>
...@@ -49,15 +53,11 @@ const PostEditModal = ({ ...@@ -49,15 +53,11 @@ const PostEditModal = ({
<MarkdownEditor <MarkdownEditor
value={text} value={text}
onChange={onTextInput} onChange={onTextInput}
error={ error={textError}
noTextError
? "Před upravením příspěvku nezapomeňte vyplnit jeho obsah."
: null
}
placeholder="Vyplňte text příspěvku" placeholder="Vyplňte text příspěvku"
toolbarCommands={[ toolbarCommands={[
["header", "bold", "italic", "strikethrough"], ["header", "bold", "italic", "strikethrough"],
["link", "quote", "image"], ["link", "quote"],
["unordered-list", "ordered-list"], ["unordered-list", "ordered-list"],
]} ]}
/> />
...@@ -72,6 +72,7 @@ const PostEditModal = ({ ...@@ -72,6 +72,7 @@ const PostEditModal = ({
hoverActive hoverActive
color="blue-300" color="blue-300"
className="text-sm" className="text-sm"
disabled={textError || confirming}
loading={confirming} loading={confirming}
onClick={confirm} onClick={confirm}
> >
......
import React from "react"; import React, { useCallback, useMemo, useState } from "react";
import classNames from "classnames"; import classNames from "classnames";
import Post from "./Post"; import Post from "./Post";
...@@ -16,6 +16,7 @@ const PostList = ({ ...@@ -16,6 +16,7 @@ const PostList = ({
onHide, onHide,
onBanUser, onBanUser,
onUnbanUser, onUnbanUser,
onInviteUser,
onAnnounceProcedureProposal, onAnnounceProcedureProposal,
onAcceptProcedureProposal, onAcceptProcedureProposal,
onRejectProcedureProposal, onRejectProcedureProposal,
...@@ -29,12 +30,16 @@ const PostList = ({ ...@@ -29,12 +30,16 @@ const PostList = ({
responderFn(post); responderFn(post);
}; };
const windowSize = 20;
const [window, setWindow] = useState(windowSize);
const onPostLike = buildHandler(onLike); const onPostLike = buildHandler(onLike);
const onPostDislike = buildHandler(onDislike); const onPostDislike = buildHandler(onDislike);
const onPostEdit = buildHandler(onEdit); const onPostEdit = buildHandler(onEdit);
const onPostHide = buildHandler(onHide); const onPostHide = buildHandler(onHide);
const onPostBanUser = buildHandler(onBanUser); const onPostBanUser = buildHandler(onBanUser);
const onPostUnbanUser = buildHandler(onUnbanUser); const onPostUnbanUser = buildHandler(onUnbanUser);
const onPostInviteUser = buildHandler(onInviteUser);
const onPostArchive = buildHandler(onArchive); const onPostArchive = buildHandler(onArchive);
const onPostAnnounceProcedureProposal = buildHandler( const onPostAnnounceProcedureProposal = buildHandler(
onAnnounceProcedureProposal onAnnounceProcedureProposal
...@@ -45,15 +50,27 @@ const PostList = ({ ...@@ -45,15 +50,27 @@ const PostList = ({
onRejectProcedureProposalByChairman onRejectProcedureProposalByChairman
); );
const onPostSeen = (post) => () => { const onPostSeen = useCallback(
(post) => () => {
if (!post.seen) {
onSeen(post); 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 ( return (
<div className={classNames("space-y-px", className)}> <div className={classNames("space-y-px", className)}>
{items {windowItems.map((item, idx) => (
.filter((item) => !item.hidden)
.map((item) => (
<Post <Post
key={item.id} key={item.id}
datetime={item.datetime} datetime={item.datetime}
...@@ -62,9 +79,9 @@ const PostList = ({ ...@@ -62,9 +79,9 @@ const PostList = ({
state={item.state} state={item.state}
content={item.contentHtml} content={item.contentHtml}
ranking={item.ranking} ranking={item.ranking}
historyLog={item.historyLog}
modified={item.modified} modified={item.modified}
seen={item.seen} seen={item.seen}
reportSeen={!item.seen || idx === window - 1}
archived={item.archived} archived={item.archived}
dimIfArchived={dimArchived} dimIfArchived={dimArchived}
currentUser={currentUser} currentUser={currentUser}
...@@ -75,6 +92,7 @@ const PostList = ({ ...@@ -75,6 +92,7 @@ const PostList = ({
onHide={onPostHide(item)} onHide={onPostHide(item)}
onBanUser={onPostBanUser(item)} onBanUser={onPostBanUser(item)}
onUnbanUser={onPostUnbanUser(item)} onUnbanUser={onPostUnbanUser(item)}
onInviteUser={onPostInviteUser(item)}
onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)} onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)}
onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)} onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)}
onRejectProcedureProposal={onPostRejectProcedureProposal(item)} onRejectProcedureProposal={onPostRejectProcedureProposal(item)}
......
...@@ -2,22 +2,44 @@ import React from "react"; ...@@ -2,22 +2,44 @@ import React from "react";
import classNames from "classnames"; import classNames from "classnames";
const PostScore = ({ const PostScore = ({
score, postType,
hasDislikes, ranking,
supportThreshold, supportThreshold,
rankingReadonly, rankingReadonly,
className, className,
}) => { }) => {
const coloring = rankingReadonly const { score, dislikes } = ranking;
? "bg-grey-125 text-grey-200" const highlight = postType === "procedure-proposal" && !rankingReadonly;
: { const coloring = highlight
? {
"bg-red-600 text-white": score < 0, "bg-red-600 text-white": score < 0,
"bg-grey-125 text-grey-200": score === 0, "bg-grey-125 text-grey-200": score === 0 && score < supportThreshold,
"bg-yellow-400 text-grey-300": "bg-yellow-400 text-grey-300":
score > 0 && hasDislikes && score < supportThreshold, score > 0 && dislikes > 0 && score < supportThreshold,
"bg-green-400 text-white": "bg-green-400 text-white":
score >= supportThreshold || (score > 0 && !hasDislikes), score >= supportThreshold || (score > 0 && dislikes <= 0),
}; }
: "bg-grey-125 text-grey-200";
let title;
if (postType === "procedure-proposal") {
if (rankingReadonly) {
title = `Návrh postupu získal podporu ${score} hlasů.`;
} else if (dislikes > 0) {
if (score < supportThreshold) {
title = `Aktuální podpora je ${score} hlasů, pro získání podpory skupiny členů chybí ještě ${
supportThreshold - score
}.`;
} else {
title = `Aktuální podpora je ${score} hlasů, což je dostatek pro získání podpory skupiny členů (vyžaduje alespoň ${supportThreshold} hlasů).`;
}
} else {
title = `Příspěvek získal ${score} hlasů bez jakýchkoliv hlasů proti a má tedy konkludentní podporu.`;
}
} else {
title = `Příspěvek získal podporu ${score} hlasů.`;
}
return ( return (
<span <span
...@@ -26,7 +48,11 @@ const PostScore = ({ ...@@ -26,7 +48,11 @@ const PostScore = ({
coloring, coloring,
className className
)} )}
title={`Míra podpory je ${score}.`} style={{ cursor: "help" }}
aria-label={title}
data-tip={title}
data-type="dark"
data-place="top"
> >
<i className="ico--power" /> <i className="ico--power" />
<span className="font-bold">{score}</span> <span className="font-bold">{score}</span>
......
...@@ -27,7 +27,7 @@ const RejectPostModalConfirm = ({ ...@@ -27,7 +27,7 @@ const RejectPostModalConfirm = ({
}) => { }) => {
return ( return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card> <Card className="elevation-21">
<CardBody> <CardBody>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<CardHeadline>{title}</CardHeadline> <CardHeadline>{title}</CardHeadline>
......
...@@ -24,7 +24,7 @@ const ProgramEntryEditModal = ({ ...@@ -24,7 +24,7 @@ const ProgramEntryEditModal = ({
return ( return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card> <Card className="elevation-21">
<CardBody> <CardBody>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit název programového bodu</CardHeadline> <CardHeadline>Upravit název programového bodu</CardHeadline>
......