Skip to content
Snippets Groups Projects
Commit 2fa6c73e authored by xaralis's avatar xaralis
Browse files

feat: announcements with link, better thumb style, fix modals on mobile,...

feat: announcements with link, better thumb style, fix modals on mobile, break/finished/not yet started screens
parent 2c8a70f6
Branches
No related tags found
No related merge requests found
Pipeline #1849 passed
......@@ -37,11 +37,13 @@ export const loadAnnouncements = createAsyncAction(
/**
* Add new announcement.
*/
export const addAnnouncement = createAsyncAction(async ({ content }) => {
export const addAnnouncement = createAsyncAction(
async ({ content, link, type }) => {
try {
const body = JSON.stringify({
content,
type: announcementTypeMappingRev["announcement"],
link,
type: announcementTypeMappingRev[type],
});
const resp = await fetch("/announcements", { method: "POST", body });
const data = await resp.json();
......@@ -49,7 +51,8 @@ export const addAnnouncement = createAsyncAction(async ({ content }) => {
} catch (err) {
return errorResult([], err.toString());
}
});
}
);
/**
* Delete existing announcement.
......
......@@ -35,7 +35,7 @@ export const loadPosts = createAsyncAction(
items: filteredPosts.map(property("id")),
itemCount: filteredPosts.length,
page: 1,
perPage: 5,
perPage: 20,
};
});
}
......
import React from "react";
import classNames from "classnames";
const Thumbs = ({ likes, dislikes, onLike, onDislike, readOnly }) => {
const Thumbs = ({ likes, dislikes, myVote, onLike, onDislike, readOnly }) => {
return (
<div>
<div className="space-x-2 text-sm flex items-center">
<button
className={classNames("text-blue-300 flex items-center space-x-1", {
className={classNames("flex items-center space-x-1", {
"cursor-pointer": !readOnly,
"cursor-default": readOnly,
"text-blue-300": myVote === "like",
"text-grey-200 hover:text-blue-300": myVote !== "like",
})}
disabled={readOnly}
onClick={onLike}
......@@ -17,9 +19,11 @@ const Thumbs = ({ likes, dislikes, onLike, onDislike, readOnly }) => {
<i className="ico--thumbs-up"></i>
</button>
<button
className={classNames("text-red-600 flex items-center space-x-1", {
className={classNames("flex items-center space-x-1", {
"cursor-pointer": !readOnly,
"cursor-default": readOnly,
"text-red-600": myVote === "dislike",
"text-grey-200 hover:text-red-600": myVote !== "dislike",
})}
disabled={readOnly}
onClick={onDislike}
......
......@@ -67,7 +67,7 @@ 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",
......@@ -83,7 +83,16 @@ const Announcement = ({
<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 triggerIconClass="ico--dots-three-horizontal">
......
......@@ -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>
);
......
......@@ -144,19 +144,19 @@ const Post = ({
<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">
<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 ml-1 text-sm">
@ {format(datetime, "H:mm")}
{modified && (
<span className="text-grey-200 text-xs ml-2 underline">
(Upraveno přesdedajícím)
<span className="text-grey-200 text-sm block md:inline md:ml-2 underline">
(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>
......@@ -218,6 +218,9 @@ const Post = ({
</div>
</div>
</div>
<div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
{labels}
</div>
<p className="text-sm lg:text-base text-black leading-normal">
{content}
</p>
......
import React, { useState } from "react";
import classNames from "classnames";
import { addAnnouncement } from "actions/announcements";
import Button from "components/Button";
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const AddAnnouncementForm = ({ className }) => {
const [text, setText] = useState("");
const [link, setLink] = useState("");
const [linkValid, setLinkValid] = useState(false);
const [type, setType] = useState("announcement");
const onTextInput = (evt) => {
setText(evt.target.value);
};
const onLinkInput = (evt) => {
setLink(evt.target.value);
if (!!evt.target.value) {
setLinkValid(evt.target.value.match(urlRegex));
}
};
const onAdd = (evt) => {
if (!!text) {
addAnnouncement.run({ content: text });
addAnnouncement.run({ content: text, link, type });
setText("");
setLink("");
}
};
return (
<div className={className}>
<div className="grid grid-cols-1 gap-4">
<div
className="form-field"
onChange={(evt) => setType(evt.target.value)}
>
<div className="form-field__wrapper text-sm">
<div className="radio form-field__control">
<label>
<input
type="radio"
name="type"
value="announcement"
defaultChecked
/>
<span>Oznámení</span>
</label>
</div>
<div className="radio form-field__control">
<label>
<input type="radio" name="type" value="voting" />
<span>Rozhodující hlasování</span>
</label>
</div>
</div>
</div>
<div className="form-field">
<div className="form-field__wrapper form-field__wrapper--shadowed">
<textarea
className="text-input form-field__control "
className="text-input text-sm form-field__control "
value={text}
rows="3"
cols="40"
......@@ -32,11 +74,35 @@ const AddAnnouncementForm = ({ className }) => {
</div>
</div>
<div
className={classNames("form-field", {
hidden: type !== "voting",
"form-field--error": !!link && !linkValid,
})}
>
<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={onLinkInput}
/>
<div className="text-input-addon text-input-addon--l order-first">
<i className="ico--link1"></i>
</div>
</div>
{!!link && !linkValid && (
<div className="form-field__error">Zadejte platnou URL.</div>
)}
</div>
</div>
<Button
onClick={onAdd}
className="text-sm mt-2"
className="text-sm mt-4"
hoverActive
disabled={!text}
disabled={!text || (type === "voting" && !linkValid)}
>
Přidat oznámení
</Button>
......
import React, { useState } from "react";
import { format } from "date-fns";
import {
closeDiscussion,
......@@ -18,8 +19,8 @@ import PostsContainer from "containers/PostsContainer";
import { useActionConfirm } from "hooks";
import { AuthStore, ProgramStore } from "stores";
const noprogramEntryDiscussion = (
<article className="container container--wide pt-8 py-8 lg:py-32">
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>
......@@ -27,8 +28,14 @@ const noprogramEntryDiscussion = (
Jednání ještě nebylo zahájeno :(
</h1>
<p className="text-xl leading-snug mb-8">
Jednání celostátního fóra v tuto chvíli neprobíhá. Můžete si ale zobrazit
program.
<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
......@@ -36,10 +43,58 @@ const noprogramEntryDiscussion = (
</article>
);
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>
);
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>
);
const Home = () => {
const { currentId, items } = ProgramStore.useState();
const {
currentId,
items: programEntries,
scheduleIds,
} = ProgramStore.useState();
const { isAuthenticated, user } = AuthStore.useState();
const programEntry = currentId ? items[currentId] : null;
const programEntry = currentId ? programEntries[currentId] : null;
const [showProgramEditModal, setShowProgramEditModal] = useState(false);
const [
showCloseDiscussion,
......@@ -68,8 +123,24 @@ const Home = () => {
setShowProgramEditModal(false);
};
const firstProgramEntry = scheduleIds.length
? programEntries[scheduleIds[0]]
: null;
const lastProgramEntry = scheduleIds.length
? programEntries[scheduleIds[0]]
: null;
if (!programEntry && new Date() < firstProgramEntry.expectedStartAt) {
return <NotYetStarted startAt={firstProgramEntry.expectedStartAt} />;
}
if (!programEntry && new Date() > lastProgramEntry.expectedStartAt) {
return <AlreadyFinished />;
}
if (!programEntry) {
return noprogramEntryDiscussion;
return <BreakInProgress />;
}
return (
......@@ -144,7 +215,19 @@ const Home = () => {
<section className="cf2021__posts">
<div className="flex flex-col xl:flex-row xl:justify-between xl:items-center mb-4">
<h2 className="head-heavy-sm whitespace-no-wrap">
Příspěvky v rozpravě
<span>Příspěvky v rozpravě</span>
{!programEntry.discussionOpened && (
<i
className="ico--lock text-black ml-1 opacity-50 hover:opacity-100 transition duration-500 text-xl"
title="Rozprava je uzavřena"
/>
)}
{programEntry.discussionOpened && (
<i
className="ico--lock-open text-black ml-1 opacity-50 hover:opacity-100 transition duration-500 text-xl"
title="Probíhá rozprava"
/>
)}
</h2>
<PostFilters />
</div>
......
......@@ -49,7 +49,7 @@ const Schedule = () => {
<p className="head-heavy-xs md:head-heavy-base">
{format(entry.expectedStartAt, "H:mm")}
</p>
<p className="ml-auto md:ml-0 head-heavy-xs md:head-heavy-xs md:text-grey-200">
<p className="ml-auto md:ml-0 head-heavy-xs md:head-heavy-xs md:text-grey-200 whitespace-no-wrap">
{format(entry.expectedStartAt, "d. M. Y")}
</p>
</div>
......
......@@ -33,7 +33,7 @@ const postStoreInitial = {
items: [],
itemCount: 0,
page: 1,
perPage: 5,
perPage: 20,
},
filters: {
flags: "all",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment