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 1131 additions and 118 deletions
import React from "react";
import classNames from "classnames";
const DropdownButton = ({
className,
color = "black",
hoverActive = true,
fullwidth = false,
loading = false,
disabled = false,
items,
children,
onClick,
...props
}) => {
const btnClass = classNames(
"btn btn--icon",
`btn--${color}`,
{
"btn--hoveractive": hoverActive,
"btn--fullwidth md:btn--autowidth": fullwidth,
"btn--loading": loading,
},
className
);
const inner = (
<div className="btn__body-wrap">
<button className="btn__body" onClick={onClick} disabled={disabled}>
{children}
</button>
<button className="btn__icon dropdown-button">
<i className="ico--chevron-down"></i>
<ul className="dropdown-button__choices bg-white text-black whitespace-no-wrap">
{items}
</ul>
</button>
</div>
);
return (
<div className={btnClass} {...props}>
{inner}
</div>
);
};
export default DropdownButton;
import React from "react";
const DropdownButtonItem = ({ onClick, children }) => {
return (
<li className="dropdown-button__choice hover:bg-grey-125" onClick={onClick}>
<span className="block px-4 py-3">{children}</span>
</li>
);
};
export default DropdownButtonItem;
export { default as DropdownButton } from "./DropdownButton";
export { default as DropdownButtonItem } from "./DropdownButtonItem";
import React from "react";
const AlreadyFinished = () => (
<article className="container container--wide py-8 md:py-16 lg:py-32">
<div className="flex">
<div>
<i className="ico--anchor text-2xl md:text-6xl lg:text-9xl mr-4 lg:mr-8"></i>
</div>
<div>
<h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
Jednání už skočilo!
</h1>
<p className="text-xl leading-snug">
Oficiální program již skončil. Těšíme se na viděnou zase příště.
</p>
</div>
</div>
</article>
);
export default AlreadyFinished;
import React from "react";
import Button from "components/Button";
const BreakInProgress = () => (
<article className="container container--wide py-8 md:py-16 lg:py-32">
<div className="flex">
<div>
<i className="ico--clock text-2xl md:text-6xl lg:text-9xl mr-4 lg:mr-8"></i>
</div>
<div>
<h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
Probíhá přestávka ...
</h1>
<p className="text-xl leading-snug mb-8">
Jednání celostátního fóra je momentálně přerušeno. Můžete si ale
zobrazit program.
</p>
<Button
routerTo="/program"
className="md:text-lg lg:text-xl"
hoverActive
>
Zobrazit program
</Button>
</div>
</div>
</article>
);
export default BreakInProgress;
import React from "react";
import { format } from "date-fns";
import Button from "components/Button";
const NotYetStarted = ({ startAt }) => (
<article className="container container--wide py-8 md:py-16 lg:py-32">
<div className="hidden md:inline-block flag bg-violet-400 text-white head-alt-base mb-4 py-4 px-5">
Jejda ...
</div>
<h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
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>
{startAt && (
<span>
Mělo by být zahájeno <strong>{format(startAt, "d. M. Y")}</strong> v{" "}
<strong>{format(startAt, "H:mm")}</strong>.{" "}
</span>
)}
<span>Můžete si ale zobrazit program.</span>
</p>
<Button routerTo="/program" className="md:text-lg lg:text-xl" hoverActive>
Zobrazit program
</Button>
</article>
);
export default NotYetStarted;
export { default as AlreadyFinished } from "./AlreadyFinished";
export { default as BreakInProgress } from "./BreakInProgress";
export { default as NotYetStarted } from "./NotYetStarted";
.mde-header {
background: transparent;
}
.react-mde .invisible {
display: none;
}
.mde-header {
border: 0;
align-items: center;
}
.mde-header .mde-tabs {
display: inline-flex;
background-color: #000;
background-color: rgba(0,0,0,1);
padding: .25rem;
}
.mde-header .mde-tabs button {
padding: .25rem 0.25rem;
font-family: Bebas Neue,Helvetica,Arial,sans-serif;
font-weight: 400;
font-size: 1.1rem;
--text-opacity: 1;
color: #fff;
color: rgba(255,255,255,var(--text-opacity));
text-align: center;
cursor: pointer;
border: 0;
border-radius: 0;
margin: 0 !important;
outline: 0;
}
.mde-header {
/* grey-50 */
background-color: #f7f7f7;
}
.mde-header .mde-tabs button.selected {
/* blue-300 */
background: #027da8;
color: #fff;
border: 0;
border-radius: 0;
margin: 0;
outline: 0;
}
.mde-header .mde-tabs button:not(.selected):hover {
/* grey-500 */
background: #303132;
}
.mde-header .mde-header-item {
border: 1px transparent solid;
transition: border-color 100ms ease-in-out;
}
.mde-header ul.mde-header-group li.mde-header-item {
margin: 0;
}
.mde-header .mde-header-item:hover {
/* grey-200 */
border: 1px #adadad solid;
}
.mde-text {
font-family: monospace;
}
.mde-header ul.mde-header-group {
padding: 0;
}
.mde-header ul.mde-header-group:first-of-type {
padding-left: .5rem;
}
.mde-header ul.mde-header-group + .mde-header-group {
margin-left: 0;
}
@media (min-width: 992px) {
.mde-header .mde-tabs button {
padding: .5rem 1rem;
}
}
@media (min-width: 1200px) {
.mde-header ul.mde-header-group {
padding: 0 0.5rem;
}
.mde-header ul.mde-header-group + .mde-header-group {
margin-left: .5rem;
}
}
import React, { useState } from "react";
import ReactMde from "react-mde";
import classNames from "classnames";
import { markdownConverter } from "markdown";
import "react-mde/lib/styles/css/react-mde-toolbar.css";
import "./MarkdownEditor.css";
const MarkdownEditor = (
{ value, onChange, error, placeholder = "", ...props },
ref
) => {
const [selectedTab, setSelectedTab] = useState("write");
const classes = {
preview: "p-2 content-block text-input text-sm md:text-base",
textArea: "p-2 text-input text-sm md:text-base",
};
const l18n = {
write: "Psaní",
preview: "Náhled",
uploadingImage: "Nahrávám obrázek",
};
const childProps = {
textArea: {
placeholder,
},
};
return (
<div className={classNames("form-field", { "form-field--error": !!error })}>
<ReactMde
ref={ref}
value={value}
onChange={onChange}
selectedTab={selectedTab}
onTabChange={setSelectedTab}
generateMarkdownPreview={(markdown) =>
Promise.resolve(markdownConverter.makeHtml(markdown))
}
classes={classes}
l18n={l18n}
childProps={childProps}
{...props}
/>
{error && <div className="form-field__error">{error}</div>}
</div>
);
};
export default React.forwardRef(MarkdownEditor);
......@@ -9,8 +9,13 @@ const CustomModal = ({ children, containerClassName, ...props }) => (
className="modal__content"
{...props}
>
<div className={classNames("modal__container w-full", containerClassName)}>
<div className="modal__container-body">{children}</div>
<div
className={classNames(
"modal__container w-full flex items-center justify-center",
containerClassName
)}
>
<div className="modal__container-body w-full">{children}</div>
</div>
</Modal>
);
......
import React from "react";
import Button from "components/Button";
import {
Card,
CardActions,
CardBody,
CardBodyText,
CardHeadline,
} from "components/cards";
import Modal from "./Modal";
import ModalWithActions from "./ModalWithActions";
const ModalConfirm = ({
title,
......@@ -18,41 +11,44 @@ const ModalConfirm = ({
cancelActionLabel = "Zrušit",
onCancel,
onConfirm,
confirming,
error,
...props
}) => {
return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card>
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>{title}</CardHeadline>
<button onClick={onCancel}>
<i className="ico--close"></i>
</button>
</div>
<CardBodyText>{children}</CardBodyText>
</CardBody>
<CardActions right className="space-x-1">
const actions = (
<>
<Button
hoverActive
color="blue-300"
className="text-sm"
onClick={onConfirm}
loading={confirming}
>
{yesActionLabel}
</Button>
<Button
hoverActive
color="red-600"
color="grey-125"
className="text-sm"
onClick={onCancel}
>
{cancelActionLabel}
</Button>
</CardActions>
</Card>
</Modal>
</>
);
return (
<ModalWithActions
onClose={onCancel}
title={title}
error={error}
actions={actions}
containerClassName="max-w-md"
{...props}
>
{children}
</ModalWithActions>
);
};
export default ModalConfirm;
export default React.memo(ModalConfirm);
import React from "react";
import {
Card,
CardActions,
CardBody,
CardBodyText,
CardHeadline,
} from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import Modal from "./Modal";
const ModalConfirm = ({
title,
children,
actions,
error,
onClose,
...props
}) => {
return (
<Modal onRequestClose={onClose} {...props}>
<Card className="elevation-21">
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>{title}</CardHeadline>
<button
onClick={onClose}
type="button"
data-tip="Zavřít"
aria-label="Zavřít"
>
<i className="ico--cross"></i>
</button>
</div>
<CardBodyText>{children}</CardBodyText>
{error && (
<ErrorMessage className="mt-2">
Při provádění akce došlo k problému: {error}
</ErrorMessage>
)}
</CardBody>
<CardActions right className="space-x-1">
{actions}
</CardActions>
</Card>
</Modal>
);
};
export default React.memo(ModalConfirm);
import React from "react";
const Beacon = React.forwardRef(({ onClick }, ref) => (
<span
className="relative inline-flex h-8 w-8"
title="Jsi online"
ref={ref}
onClick={onClick}
>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-red-600"></span>
<span className="inline-flex rounded-full w-8 h-8 bg-red-600"></span>
</span>
));
export default Beacon;
import React from "react";
// import Chip from "components/Chip";
export { default as Beacon } from "./Beacon";
export const steps = [
{
target: "body",
content: (
<>
<h1 className="head-alt-sm mb-4">Vítej na celostátním fóru 2024</h1>
<p className="leading-snug text-base">
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>
</>
),
placement: "center",
disableBeacon: true,
},
{
target: ".joyride-login",
content: (
<>
<h1 className="head-alt-sm mb-4">Jsi člen či příznivec? Přihlaš se</h1>
<p className="leading-snug text-base">
Pokud jsi člen strany nebo registrovaný příznivec, je rozhodně dobrý
nápad se přihlásit. Budeš tak moci přidávat příspěvky v rozpravě a
palcovat je.
</p>
</>
),
},
{
target: ".joyride-player",
content: (
<>
<h1 className="head-alt-sm mb-4">Video stream</h1>
<p className="leading-snug text-base">
Zde můžeš sledovat přímý přenos z jednání. Přenos má drobné zpoždění,
tak s tím počítej.
</p>
</>
),
placement: "bottom",
},
{
target: ".joyride-posts",
content: (
<>
<h1 className="head-alt-sm mb-4">Příspěvky v rozpravě</h1>
<div className="leading-snug text-base space-y-2">
<p>
Předsedající pro každý bod programu může otevřít či uzavřít
rozpravu. V rámci rozpravy je možné přidávat běžné diskusní
příspěvky, nebo návrhy postupu.
</p>
<p>
<strong>Běžné příspěvky</strong> se zobrazí ihned po přidání.
</p>
<p>
U příspěvků se též zobrazuje celková míra podpory. Legenda barevného
odlišení je následující:
</p>
<ul className="unordered-list unordered-list--dense">
<li>
<div className="px-1 text-sm font-bold inline-block bg-green-400 text-white">
Zelenou
</div>{" "}
je označen příspěvek, na kterém je konsensus, nebo takový, který
získal podporu skupiny členů.
</li>
<li>
<div className="px-1 text-sm font-bold inline-block bg-yellow-400 text-grey-300">
Žlutou
</div>{" "}
je označen příspěvek, který podporu teprve sbírá.
</li>
<li>
<div className="px-1 text-sm font-bold inline-block bg-red-600 text-white">
Červeně
</div>{" "}
je označen příspěvek, který má spíše negativní odezvu.
</li>
<li>
<div className="px-1 text-sm font-bold inline-block bg-grey-125 text-grey-200">
Šedivě
</div>{" "}
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: "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>
</>
),
placement: "bottom",
},
{
target: ".joyride-announcements",
content: (
<>
<h1 className="head-alt-sm mb-4">Oblast pro oznámení</h1>
<p className="leading-snug text-base">
V této oblasti se zobrazují oznámení podstatných událostí v rámci
jednání, jako například nové rozhodující hlasování, nebo třeba nový
hlasovatelný návrh postupu.
</p>
</>
),
placement: "left",
},
{
target: "body",
content: (
<>
<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í.
</p>
</>
),
placement: "center",
},
];
import React from "react";
import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import classNames from "classnames";
import { format } from "date-fns";
import { format, isToday } from "date-fns";
import Chip from "components/Chip";
import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu";
import PostScore from "components/posts/PostScore";
import Thumbs from "components/Thumbs";
const Post = ({
......@@ -13,26 +15,49 @@ const Post = ({
type,
ranking,
content,
modified,
seen,
archived,
state,
historyLog,
dimIfArchived = true,
displayActions = false,
currentUser,
supportThreshold,
canThumb,
reportSeen = true,
onLike,
onDislike,
onHide,
onBanUser,
onUnbanUser,
onInviteUser,
onAnnounceProcedureProposal,
onAcceptProcedureProposal,
onRejectProcedureProposal,
onRejectProcedureProposalByChairman,
onEdit,
onArchive,
onSeen,
...props
}) => {
const { ref, inView } = useInView({
threshold: 0.8,
trackVisibility: true,
delay: 1000,
skip: !reportSeen,
triggerOnce: true,
});
useEffect(() => {
if (inView && onSeen) {
onSeen();
}
}, [inView, onSeen]);
const wrapperClassName = classNames(
"flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2",
"flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2 transition duration-500",
{
"bg-yellow-100 bg-opacity-50": !seen,
"opacity-25 hover:opacity-100 transition-opacity duration-200":
dimIfArchived && !!archived,
"opacity-25 hover:opacity-100": dimIfArchived && !!archived,
},
className
);
......@@ -62,28 +87,53 @@ const Post = ({
labels.push(
{
pending: (
<Chip key="state__pending" condensed color="grey-500">
<Chip
key="state__pending"
condensed
color="grey-500"
aria-label="Návrh čekající na zpracování"
>
Čeká na zpracování
</Chip>
),
announced: (
<Chip key="state__announced" condensed color="blue-300">
Vyhlášený
<Chip
key="state__announced"
condensed
color="blue-300"
aria-label="Návrh k hlasování"
>
K hlasování
</Chip>
),
accepted: (
<Chip key="state__accepted" condensed color="green-400">
<Chip
key="state__accepted"
condensed
color="green-400"
aria-label="Schválený návrh"
>
Schválený
</Chip>
),
rejected: (
<Chip key="state__rejected" condensed color="red-600">
<Chip
key="state__rejected"
condensed
color="red-600"
aria-label="Zamítnutý návrh"
>
Zamítnutý
</Chip>
),
"rejected-by-chairman": (
<Chip key="state__rejected-by-chairmen" condensed color="red-600">
Zamítnutý předsedajícím
<Chip
key="state__rejected-by-chairmen"
condensed
color="red-600"
aria-label="Návrh zamítnutý předsedajícím"
>
Zamítnutý předs.
</Chip>
),
}[state]
......@@ -103,60 +153,102 @@ const Post = ({
);
}
const isModified =
(historyLog || []).filter(
(logRecord) =>
logRecord.attribute === "content" && logRecord.originator === "chairman"
).length > 0;
const isChairman = currentUser && currentUser.role === "chairman";
const showAnnounceAction =
type === "procedure-proposal" && state === "pending";
isChairman && type === "procedure-proposal" && state === "pending";
const showAcceptAction =
type === "procedure-proposal" && state === "announced";
isChairman && type === "procedure-proposal" && state === "announced";
const showRejectAction =
type === "procedure-proposal" && state === "announced";
const showBanAction = true;
const showHideAction = !archived;
isChairman && type === "procedure-proposal" && state === "announced";
const showRejectByChairmanAction =
isChairman &&
type === "procedure-proposal" &&
["announced", "pending"].includes(state);
const showEditAction =
isChairman ||
(currentUser && currentUser.id === author.id && !currentUser.isBanned);
const showBanAction = isChairman && !author.isBanned;
const showUnbanAction = isChairman && author.isBanned;
const showInviteAction = isChairman;
const showHideAction = isChairman && !archived;
const showArchiveAction = isChairman && !archived;
// Show actions dropdown if any of actions is available.
const showActions = [
showAnnounceAction,
showAcceptAction,
showRejectAction,
showRejectByChairmanAction,
showEditAction,
showBanAction,
showUnbanAction,
showInviteAction,
showHideAction,
showArchiveAction,
].some((item) => !!item);
const htmlContent = {
__html: content,
};
const thumbsVisible = !archived && (type === "post" || state === "announced");
return (
<div className={wrapperClassName}>
<div className={wrapperClassName} ref={ref} {...props}>
<img
src="http://placeimg.com/100/100/people"
className="w-8 h-8 lg:w-14 lg:h-14 rounded mr-3"
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">
<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">
<div className="flex flex-col xl:flex-row xl:items-center">
<span className="font-bold">{author.name}</span>
<div className="mt-1 lg:mt-0 lg:ml-2">
<span className="text-grey-200 text-sm">{author.group}</span>
<span className="text-grey-200 ml-1 text-sm">
@ {format(datetime, "H:mm")}
{isModified && (
<span className="text-grey-200 text-xs ml-2 underline">
(Upraveno přesdedajícím)
<div className="mt-1 xl:mt-0 xl:ml-2 leading-tight">
<span className="text-grey-200 text-xs sm:text-sm">
{author.group}
</span>
<span className="text-grey-200 ml-1 text-xs sm:text-sm">
@{" "}
{format(
datetime,
isToday(datetime) ? "H:mm" : "dd. MM. H:mm"
)}
{modified && (
<span className="text-grey-200 text-xs block md:inline md:ml-2">
(upraveno)
</span>
)}
</span>
</div>
</div>
<div className="flex flex-row flex-wrap lg:flex-no-wrap lg:items-center mt-1 xl:mt-0 xl:ml-2 space-x-2">
<div className="hidden lg:flex flex-row flex-wrap lg:flex-no-wrap lg:items-center mt-1 xl:mt-0 xl:ml-2 space-x-2">
{labels}
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center">
{thumbsVisible && (
<Thumbs
likes={ranking.likes}
dislikes={ranking.dislikes}
readOnly={!canThumb}
onLike={onLike}
onDislike={onDislike}
myVote={ranking.myVote}
/>
{displayActions && (
<DropdownMenu right>
)}
<PostScore
className="ml-2"
postType={type}
ranking={ranking}
rankingReadonly={!thumbsVisible}
supportThreshold={supportThreshold}
/>
{showActions && (
<DropdownMenu right className="pl-4 static">
{showAnnounceAction && (
<DropdownMenuItem
onClick={onAnnounceProcedureProposal}
......@@ -178,6 +270,20 @@ const Post = ({
title="Zamítnout procedurální návrh"
/>
)}
{showRejectByChairmanAction && (
<DropdownMenuItem
onClick={onRejectProcedureProposalByChairman}
icon="ico--thumbs-down"
title="Zamítnout procedurální návrh předsedajícím"
/>
)}
{showEditAction && (
<DropdownMenuItem
onClick={onEdit}
icon="ico--pencil"
title="Upravit příspěvek"
/>
)}
{showBanAction && (
<DropdownMenuItem
onClick={onBanUser}
......@@ -185,6 +291,20 @@ const Post = ({
title="Zablokovat uživatele"
/>
)}
{showUnbanAction && (
<DropdownMenuItem
onClick={onUnbanUser}
icon="ico--lock-open"
title="Odblokovat uživatele"
/>
)}
{showInviteAction && (
<DropdownMenuItem
onClick={onInviteUser}
icon="ico--phone"
title="Pozvat uživatele do Jitsi"
/>
)}
{showHideAction && (
<DropdownMenuItem
onClick={onHide}
......@@ -192,17 +312,28 @@ const Post = ({
title="Skrýt příspěvek"
/>
)}
{showArchiveAction && (
<DropdownMenuItem
onClick={onArchive}
icon="ico--drawer"
title="Archivovat příspěvek"
/>
)}
</DropdownMenu>
)}
</div>
</div>
</div>
<p className="text-sm lg:text-base text-black leading-normal">
{content}
</p>
<div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
{labels}
</div>
<div
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>
);
};
export default Post;
export default React.memo(Post);
import React, { useState } from "react";
import Button from "components/Button";
import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import MarkdownEditor from "components/mde/MarkdownEditor";
import Modal from "components/modals/Modal";
const PostEditModal = ({
post,
onCancel,
onConfirm,
confirming,
error,
...props
}) => {
const [text, setText] = useState(post.content);
const [textError, setTextError] = useState(null);
const onTextInput = (newText) => {
setText(newText);
if (newText !== "") {
if (newText.length >= 1024) {
setTextError("Maximální délka příspěvku je 1024 znaků.");
} else {
setTextError(null);
}
}
};
const confirm = (evt) => {
evt.preventDefault();
if (!!text) {
onConfirm(text);
} else {
setTextError("Před upravením příspěvku nezapomeňte vyplnit jeho obsah.");
}
};
return (
<Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}>
<form onSubmit={confirm}>
<Card className="elevation-21">
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit text příspěvku</CardHeadline>
<button onClick={onCancel} type="button">
<i className="ico--cross"></i>
</button>
</div>
<MarkdownEditor
value={text}
onChange={onTextInput}
error={textError}
placeholder="Vyplňte text příspěvku"
toolbarCommands={[
["header", "bold", "italic", "strikethrough"],
["link", "quote"],
["unordered-list", "ordered-list"],
]}
/>
{error && (
<ErrorMessage className="mt-2">
Při editaci došlo k problému: {error}
</ErrorMessage>
)}
</CardBody>
<CardActions right className="space-x-1">
<Button
hoverActive
color="blue-300"
className="text-sm"
disabled={textError || confirming}
loading={confirming}
onClick={confirm}
>
Uložit
</Button>
<Button
hoverActive
color="red-600"
className="text-sm"
onClick={onCancel}
>
Zrušit
</Button>
</CardActions>
</Card>
</form>
</Modal>
);
};
export default PostEditModal;
import React from "react";
import React, { useCallback, useMemo, useState } from "react";
import classNames from "classnames";
import Post from "./Post";
......@@ -6,57 +6,110 @@ import Post from "./Post";
const PostList = ({
className,
items,
showAddPostCta,
currentUser,
supportThreshold,
canThumb,
dimArchived,
onLike,
onDislike,
onHide,
onBanUser,
onUnbanUser,
onInviteUser,
onAnnounceProcedureProposal,
onAcceptProcedureProposal,
onRejectProcedureProposal,
dimArchived,
onRejectProcedureProposalByChairman,
onEdit,
onArchive,
onSeen,
}) => {
const buildHandler = (responderFn) => (post) => (evt) => {
evt.preventDefault();
responderFn(post);
};
const windowSize = 20;
const [window, setWindow] = useState(windowSize);
const onPostLike = buildHandler(onLike);
const onPostDislike = buildHandler(onDislike);
const onPostEdit = buildHandler(onEdit);
const onPostHide = buildHandler(onHide);
const onPostBanUser = buildHandler(onBanUser);
const onPostUnbanUser = buildHandler(onUnbanUser);
const onPostInviteUser = buildHandler(onInviteUser);
const onPostArchive = buildHandler(onArchive);
const onPostAnnounceProcedureProposal = buildHandler(
onAnnounceProcedureProposal
);
const onPostAcceptProcedureProposal = buildHandler(onAcceptProcedureProposal);
const onPostRejectProcedureProposal = buildHandler(onRejectProcedureProposal);
const onPostRejectProcedureProposalByChairman = buildHandler(
onRejectProcedureProposalByChairman
);
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}
author={item.author}
type={item.type}
state={item.state}
content={item.content}
content={item.contentHtml}
ranking={item.ranking}
historyLog={item.historyLog}
modified={item.modified}
seen={item.seen}
reportSeen={!item.seen || idx === window - 1}
archived={item.archived}
displayActions={true}
dimIfArchived={dimArchived}
currentUser={currentUser}
supportThreshold={supportThreshold}
canThumb={canThumb}
onLike={onPostLike(item)}
onDislike={onPostDislike(item)}
onHide={onPostHide(item)}
onBanUser={onPostBanUser(item)}
onUnbanUser={onPostUnbanUser(item)}
onInviteUser={onPostInviteUser(item)}
onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)}
onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)}
onRejectProcedureProposal={onPostRejectProcedureProposal(item)}
onRejectProcedureProposalByChairman={onPostRejectProcedureProposalByChairman(
item
)}
onEdit={onPostEdit(item)}
onArchive={onPostArchive(item)}
onSeen={onPostSeen(item)}
/>
))}
{showAddPostCta && !items.length && (
<p className="p-4 lg:p-0 lg:py-3 leading-snug text-sm md:text-base">
Nikdo zatím žádný odpovídající příspěvek do rozpravy nepřidal. Budeš
první?
</p>
)}
</div>
);
};
......
import React from "react";
import classNames from "classnames";
const PostScore = ({
postType,
ranking,
supportThreshold,
rankingReadonly,
className,
}) => {
const { score, dislikes } = ranking;
const highlight = postType === "procedure-proposal" && !rankingReadonly;
const coloring = highlight
? {
"bg-red-600 text-white": score < 0,
"bg-grey-125 text-grey-200": score === 0 && score < supportThreshold,
"bg-yellow-400 text-grey-300":
score > 0 && dislikes > 0 && score < supportThreshold,
"bg-green-400 text-white":
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 (
<span
className={classNames(
"p-1 text-sm flex items-center space-x-1",
coloring,
className
)}
style={{ cursor: "help" }}
aria-label={title}
data-tip={title}
data-type="dark"
data-place="top"
>
<i className="ico--power" />
<span className="font-bold">{score}</span>
</span>
);
};
export default React.memo(PostScore);
import React from "react";
import Button from "components/Button";
import {
Card,
CardActions,
CardBody,
CardBodyText,
CardHeadline,
} from "components/cards";
import ErrorMessage from "components/ErrorMessage";
import Modal from "./Modal";
const RejectPostModalConfirm = ({
title,
children,
yesActionLabel = "OK",
yesAndArchiveActionLabel = "OK",
cancelActionLabel = "Zrušit",
onCancel,
onConfirm,
onConfirmAndArchive,
confirming,
error,
...props
}) => {
return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card className="elevation-21">
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>{title}</CardHeadline>
<button onClick={onCancel} type="button">
<i className="ico--cross"></i>
</button>
</div>
<CardBodyText>{children}</CardBodyText>
{error && (
<ErrorMessage className="mt-2">
Při provádění akce došlo k problému: {error}
</ErrorMessage>
)}
</CardBody>
<CardActions right className="space-x-1">
<Button
hoverActive
color="blue-300"
className="text-sm"
onClick={onConfirm}
loading={confirming}
>
{yesActionLabel}
</Button>
<Button
hoverActive
color="blue-300"
className="text-sm"
onClick={onConfirm}
loading={confirming}
>
{yesAndArchiveActionLabel}
</Button>
<Button
hoverActive
color="red-600"
className="text-sm"
onClick={onCancel}
>
{cancelActionLabel}
</Button>
</CardActions>
</Card>
</Modal>
);
};
export default React.memo(RejectPostModalConfirm);
import React, { useState } from "react";
import Button from "components/Button";
import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
import Modal from "components/modals/Modal";
const ProgramEntryEditModal = ({
programEntry,
onCancel,
onConfirm,
...props
}) => {
const [text, setText] = useState(programEntry.title);
const onTextInput = (evt) => {
setText(evt.target.value);
};
const confirm = (evt) => {
if (!!text) {
onConfirm(text);
}
};
return (
<Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
<Card className="elevation-21">
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit název programového bodu</CardHeadline>
<button onClick={onCancel}>
<i className="ico--cross"></i>
</button>
</div>
<div className="form-field">
<label className="form-field__label" htmlFor="field">
Nový název
</label>
<div className="form-field__wrapper form-field__wrapper--shadowed">
<input
type="text"
className="text-input form-field__control"
value={text}
onChange={onTextInput}
placeholder="Vyplňte nový název"
/>
</div>
</div>
</CardBody>
<CardActions right className="space-x-1">
<Button
hoverActive
color="blue-300"
className="text-sm"
onClick={confirm}
>
Uložit
</Button>
<Button
hoverActive
color="red-600"
className="text-sm"
onClick={onCancel}
>
Zrušit
</Button>
</CardActions>
</Card>
</Modal>
);
};
export default ProgramEntryEditModal;