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

Target

Select target project
  • to/cf-online-ui
  • vpfafrin/cf2021
2 results
Select Git revision
Show changes
Showing
with 642 additions and 12 deletions
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";
const Announcement = ({
className,
......@@ -12,9 +14,27 @@ const Announcement = ({
link,
relatedPostId,
seen,
canRunActions,
onDelete,
onEdit,
onSeen,
}) => {
const { ref, inView } = useInView({
threshold: 0.8,
trackVisibility: true,
delay: 1500,
skip: seen,
triggerOnce: true,
});
useEffect(() => {
if (!seen && inView && onSeen) {
onSeen();
}
});
const wrapperClassName = classNames(
"bg-opacity-50 border-l-2 px-4 py-2 lg:px-8 lg:py-3",
"bg-opacity-50 border-l-2 px-4 py-2 lg:px-8 lg:py-3 transition duration-500",
{
"bg-grey-50": !!seen,
"bg-yellow-100": !seen,
......@@ -39,7 +59,7 @@ const Announcement = ({
const chipLabel = {
"rejected-procedure-proposal": "Zamítnutý návrh postupu",
"suggested-procedure-proposal": "Přijatelný návrh postupu",
"suggested-procedure-proposal": "Návrh postupu k hlasování",
"accepted-procedure-proposal": "Schválený návrh postupu",
voting: "Rozhodující hlasování",
announcement: "Oznámení předsedajícího",
......@@ -47,20 +67,64 @@ const Announcement = ({
}[type];
const linkLabel =
type === "voting" ? "Hlasovat v heliosu" : "Zobrazit související příspěvek";
type === "voting" ? "Hlasovat" : "Zobrazit související příspěvek";
const showEdit = [
"suggested-procedure-proposal",
"voting",
"announcement",
].includes(type);
const htmlContent = {
__html: content,
};
return (
<div className={wrapperClassName}>
<div className={wrapperClassName} ref={ref}>
<div className="flex items-center justify-between mb-2">
<div className="space-x-2 flex items-center">
<div className="font-bold text-sm">{format(datetime, "H:mm")}</div>
<div className="font-bold text-sm">
{format(datetime, isToday(datetime) ? "H:mm" : "dd. MM. H:mm")}
</div>
<Chip color={chipColor} condensed>
{chipLabel}
</Chip>
{link && <a href={link}>{linkLabel + "»"}</a>}
{link && (
<a
href={link}
className={classNames("text-xs font-bold text-" + chipColor)}
target="_blank"
rel="noopener noreferrer"
>
{linkLabel + " »"}
</a>
)}
</div>
{canRunActions && (
<DropdownMenu
right
className="pl-4"
triggerIconClass="ico--dots-three-horizontal"
>
{showEdit && (
<DropdownMenuItem
onClick={onEdit}
icon="ico--pencil"
title="Upravit"
/>
)}
<DropdownMenuItem
onClick={onDelete}
icon="ico--bin"
title="Smazat"
/>
</DropdownMenu>
)}
</div>
<span className="leading-tight text-sm lg:text-base">{content}</span>
<div
className="leading-tight text-sm lg:text-base content-block"
dangerouslySetInnerHTML={htmlContent}
></div>
</div>
);
};
......
import React, { useState } from "react";
import classNames from "classnames";
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";
import { urlRegex } from "utils";
const AnnouncementEditModal = ({
announcement,
onCancel,
onConfirm,
confirming,
error,
...props
}) => {
const [text, setText] = useState(announcement.content);
const [link, setLink] = useState(announcement.link);
const [textError, setTextError] = useState(null);
const [linkError, setLinkError] = 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 onLinkInput = (newLink) => {
setLink(newLink);
if (!!newLink) {
if (newLink.length > 1024) {
setLinkError("Maximální délka URL je 256 znaků.");
} else {
setLinkError(urlRegex.test(newLink) ? null : "Zadejte platnou URL.");
}
}
};
const confirm = (evt) => {
evt.preventDefault();
let preventAction = false;
const payload = {
content: text,
};
if (!text) {
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;
}
if (announcement.type === "voting" && !link) {
setLinkError("Zadejte platnou URL.");
preventAction = true;
} else {
payload.link = link;
}
if (preventAction) {
return;
}
onConfirm(payload);
};
return (
<Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}>
<form onSubmit={confirm}>
<Card className="elevation-21">
<CardBody>
<div className="flex items-center justify-between mb-4">
<CardHeadline>Upravit oznámení</CardHeadline>
<button onClick={onCancel} type="button">
<i className="ico--cross"></i>
</button>
</div>
<MarkdownEditor
value={text}
onChange={onTextInput}
error={textError}
placeholder="Vyplňte text oznámení"
toolbarCommands={[
["bold", "italic", "strikethrough"],
["link", "unordered-list", "ordered-list"],
]}
/>
<div
className={classNames("form-field mt-4", {
hidden: announcement.type !== "voting",
"form-field--error": !!linkError,
})}
>
<div className="form-field__wrapper form-field__wrapper--shadowed">
<input
type="text"
className="text-input text-sm text-input--has-addon-l form-field__control"
value={link}
placeholder="URL hlasování"
onChange={(evt) => onLinkInput(evt.target.value)}
/>
<div className="text-input-addon text-input-addon--l order-first">
<i className="ico--link"></i>
</div>
</div>
{!!linkError && (
<div className="form-field__error">{linkError}</div>
)}
</div>
{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"
loading={confirming}
disabled={textError || linkError || confirming}
type="submit"
>
Uložit
</Button>
<Button
hoverActive
color="red-600"
className="text-sm"
onClick={onCancel}
type="button"
>
Zrušit
</Button>
</CardActions>
</Card>
</form>
</Modal>
);
};
export default AnnouncementEditModal;
......@@ -3,19 +3,60 @@ import classNames from "classnames";
import Announcement from "./Announcement";
const AnnouncementList = ({ items, className }) => {
const AnnouncementList = ({
items,
className,
canRunActions,
onDelete,
onEdit,
onSeen,
}) => {
const buildHandler = (responderFn) => (announcement) => (evt) => {
evt.preventDefault();
responderFn(announcement);
};
const onAnnouncementEdit = buildHandler(onEdit);
const onAnnouncementDelete = buildHandler(onDelete);
const onAnnouncementSeen = (announcement) => () => {
onSeen(announcement);
};
const getClassName = (idx) => {
if (idx === 0) {
return "pt-4 lg:pt-8";
}
if (idx === items.length - 1) {
return "pb-4 lg:pb-8";
}
return "";
};
return (
<div className={classNames("space-y-px", className)}>
{items.map((item) => (
{items.map((item, idx) => (
<Announcement
className={getClassName(idx)}
key={item.id}
datetime={item.datetime}
type={item.type}
content={item.content}
content={item.contentHtml}
link={item.link}
seen={item.seen}
canRunActions={canRunActions}
onEdit={onAnnouncementEdit(item)}
onDelete={onAnnouncementDelete(item)}
onSeen={onAnnouncementSeen(item)}
/>
))}
{!items.length && (
<p className="px-8 py-4 leading-snug text-sm md:text-base">
Zatím žádná oznámení.
</p>
)}
</div>
);
};
......
import React from "react";
import classNames from "classnames";
const Card = ({ children, className }, ref) => {
const cls = classNames("card", className);
return (
<div className={cls} ref={ref}>
{children}
</div>
);
};
export default React.forwardRef(Card);
import React from "react";
import classNames from "classnames";
const CardActions = ({ children, right, className }) => {
const cls = classNames(
"card-actions",
{ "card-actions--right": !!right },
className
);
return <div className={cls}>{children}</div>;
};
export default CardActions;
import React from "react";
import classNames from "classnames";
const CardBody = ({ children, className, ...props }) => {
const cls = classNames("card__body", className);
return (
<div className={cls} {...props}>
{children}
</div>
);
};
export default CardBody;
import React from "react";
const CardBodyText = ({ children }) => {
return <div className="card-body-text">{children}</div>;
};
export default CardBodyText;
import React from "react";
const CardHeadline = ({ children }) => {
return <h1 className="card-headline">{children}</h1>;
};
export default CardHeadline;
export { default as Card } from "./Card";
export { default as CardActions } from "./CardActions";
export { default as CardBody } from "./CardBody";
export { default as CardBodyText } from "./CardBodyText";
export { default as CardHeadline } from "./CardHeadline";
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";
import classNames from "classnames";
const DropdownMenu = ({
children,
className,
right,
triggerSize = "sm",
triggerIconClass = "ico--dots-three-vertical",
}) => {
const wrapperCls = classNames(
"dropdown",
{
"dropdown--right": !!right,
},
className
);
const triggerCls = classNames(
"cursor-pointer ml-auto text-grey-200 hover:text-black",
`text-${triggerSize}`,
triggerIconClass
);
return (
<div className={wrapperCls}>
<i className={triggerCls}></i>
<ul className="dropdown__content whitespace-no-wrap">{children}</ul>
</div>
);
};
export default React.memo(DropdownMenu);
import React from "react";
import classNames from "classnames";
const DropdownMenuItem = ({
icon,
className,
onClick,
title,
titleClass,
iconSize = "2xs",
titleSize = "xs",
}) => {
const iconCls = classNames("text-2xs mr-2", `text-${iconSize}`, icon);
const titleCls = classNames(`text-${titleSize}`, titleClass);
const cls = classNames(
"dropdown__content-item bg-white hover:bg-grey-125 cursor-pointer",
className
);
return (
<li className={cls} onClick={onClick}>
<span className="block px-3 py-3">
<i className={iconCls}></i>
<span className={titleCls}>{title}</span>
</span>
</li>
);
};
export default React.memo(DropdownMenuItem);
export { default as DropdownMenu } from "./DropdownMenu";
export { default as DropdownMenuItem } from "./DropdownMenuItem";
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;
}
}