diff --git a/components/ComponentSwitcher.js b/components/ComponentSwitcher.js
new file mode 100644
index 0000000..c05d5fa
--- /dev/null
+++ b/components/ComponentSwitcher.js
@@ -0,0 +1,25 @@
+import { cloneElement } from 'react';
+import { useState } from 'react';
+
+const ComponentSwitcher = ({
+ primaryComponent,
+ secondaryComponent,
+}) => {
+ const [isSwitched, setIsSwitched] = useState(false);
+
+ const invertComponent = () => {
+ setIsSwitched((isSwitched) => !isSwitched);
+ };
+
+ if (isSwitched) {
+ return cloneElement(secondaryComponent, {
+ invertComponent: invertComponent,
+ });
+ } else {
+ return cloneElement(primaryComponent, {
+ invertComponent: invertComponent,
+ });
+ }
+};
+
+export default ComponentSwitcher;
diff --git a/components/ModeratorNewStatementCard.js b/components/ModeratorNewStatementCard.js
new file mode 100644
index 0000000..d49950e
--- /dev/null
+++ b/components/ModeratorNewStatementCard.js
@@ -0,0 +1,155 @@
+import {
+ Box,
+ Button,
+ Flex,
+ HStack,
+ Spacer,
+ Stack,
+ Text,
+ Textarea,
+} from '@chakra-ui/react';
+import { ChatIcon } from '@chakra-ui/icons';
+import { useState } from 'react';
+import ComponentSwitcher from './ComponentSwitcher';
+
+const ModeratorEditStatementCard = ({
+ statement,
+ jamId,
+ patchRequest,
+ invertComponent,
+}) => {
+ const [editedStatement, setEditedStatement] = useState(
+ statement.text,
+ );
+
+ const handleStatementChange = (e) => {
+ let statementValue = e.target.value;
+ setEditedStatement(statementValue);
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const ModeratorDecisionStatementCard = ({
+ statement,
+ jamId,
+ patchRequest,
+ invertComponent,
+}) => {
+ return (
+
+ {statement.text}
+
+
+
+
+ Participant submitted{' '}
+ {new Date(
+ statement.createdAt?._seconds * 1000,
+ ).toUTCString()}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ModeratorNewStatementCard = ({
+ statement,
+ jamId,
+ patchRequest,
+}) => {
+ return (
+
+ }
+ secondaryComponent={
+
+ }
+ />
+ );
+};
+
+export default ModeratorNewStatementCard;
diff --git a/components/OverviewJamCard.jsx b/components/OverviewJamCard.jsx
index b1ec2fe..b206b44 100644
--- a/components/OverviewJamCard.jsx
+++ b/components/OverviewJamCard.jsx
@@ -25,7 +25,7 @@ const overviewCard = ({
borderColor="#8D8D8D"
>
-
+
{
const [statement, setStatement] = useState(children);
@@ -24,13 +25,13 @@ const EditableStatement = ({
borderRadius="none"
/>
-
@@ -64,33 +65,23 @@ const VisibleOnlyStatement = ({
};
function ProposedStatementCard(props) {
- const [isEditable, setIsEditable] = useState(false);
-
- const invertEditable = () => {
- setIsEditable((isEditable) => !isEditable);
- };
-
- if (isEditable) {
- return (
-
- {props.children}
-
- );
- } else {
- return (
-
- {props.children}
-
- );
- }
+ return (
+
+ {props.children}
+
+ }
+ secondaryComponent={
+
+ {props.children}
+
+ }
+ />
+ );
}
export default ProposedStatementCard;
diff --git a/package.json b/package.json
index 28f15fc..43095c2 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"test": "jest"
},
"dependencies": {
+ "@chakra-ui/icons": "^1.0.15",
"@chakra-ui/react": "^1.6.6",
"@emotion/react": "^11",
"@emotion/styled": "^11",
diff --git a/pages/api/jam.js b/pages/api/jam.js
index 2226367..0081b8b 100644
--- a/pages/api/jam.js
+++ b/pages/api/jam.js
@@ -1,11 +1,11 @@
import fire from '../../config/firebaseAdminConfig';
import ensureAdmin from 'utils/admin-auth-middleware';
-function getJamByUrlPath(jamUrlPath) {
+async function getJamByUrlPath(jamUrlPath, includeStatements) {
const db = fire.firestore();
const jamsRef = db.collection('jams');
- return jamsRef
+ const finalJam = await jamsRef
.where('urlPath', '==', jamUrlPath)
.get()
.then((querySnapshot) => {
@@ -17,6 +17,26 @@ function getJamByUrlPath(jamUrlPath) {
});
return jams[0];
});
+
+ if (!includeStatements) {
+ return finalJam;
+ }
+
+ finalJam.statements = await jamsRef
+ .doc(finalJam.key)
+ .collection('statements')
+ .get()
+ .then((query) => {
+ const statements = [];
+ query.forEach((doc) => {
+ const statement = doc.data();
+ statement.key = doc.id;
+ statements.push(statement);
+ });
+ return statements;
+ });
+
+ return finalJam;
}
function createJam({ name, description, statements }) {
@@ -59,9 +79,27 @@ function createJam({ name, description, statements }) {
});
}
+function patchJam(req, res) {
+ const { jamId, ...body } = req.body;
+ const db = fire.firestore();
+ const jamsRef = db.collection('jams');
+
+ return new Promise(() => {
+ jamsRef
+ .doc(jamId)
+ .update(body)
+ .then(() => {
+ res.status(200).end();
+ })
+ .catch((error) => {
+ console.error('Error writing document: ', error);
+ });
+ });
+}
+
export default async function handler(req, res) {
const {
- query: { jamUrlPath },
+ query: { jamUrlPath, includeStatements },
method,
} = req;
@@ -99,14 +137,18 @@ export default async function handler(req, res) {
res.status(500).json({ error: error });
}
} else if (method === 'GET') {
- return getJamByUrlPath(jamUrlPath).then((jam) => {
- if (jam) {
- res.status(200);
- res.setHeader('Content-Type', 'application/json');
- res.json(jam);
- } else {
- res.status(404).end();
- }
- });
+ return getJamByUrlPath(jamUrlPath, includeStatements).then(
+ (jam) => {
+ if (jam) {
+ res.status(200);
+ res.setHeader('Content-Type', 'application/json');
+ res.json(jam);
+ } else {
+ res.status(404).end();
+ }
+ },
+ );
+ } else if (method === 'PATCH') {
+ return patchJam(req, res);
}
}
diff --git a/pages/api/statement.js b/pages/api/statement.js
index 0abcbeb..b9a222e 100644
--- a/pages/api/statement.js
+++ b/pages/api/statement.js
@@ -1,11 +1,6 @@
import fire from '../../config/firebaseAdminConfig';
-export default function handler(req, res) {
- if (req.method !== 'POST') {
- res.setHeader('Allow', ['POST']);
- res.status(405).end(`Method ${method} Not Allowed`);
- }
-
+const handlePost = (req, res) => {
const { jamId, statement } = req.body;
const db = fire.firestore();
const jamsRef = db.collection('jams');
@@ -30,4 +25,38 @@ export default function handler(req, res) {
console.error('Error writing document: ', error);
});
});
+};
+
+const handlePatch = (req, res) => {
+ const { jamId, statementId, ...body } = req.body;
+ const db = fire.firestore();
+ const jamsRef = db.collection('jams');
+
+ return new Promise(() => {
+ jamsRef
+ .doc(jamId)
+ .collection('statements')
+ .doc(statementId)
+ .update(body)
+ .then(() => {
+ res.status(200).end();
+ })
+ .catch((error) => {
+ console.error('Error writing document: ', error);
+ });
+ });
+};
+
+export default function handler(req, res) {
+ if (!['POST', 'PATCH'].includes(req.method)) {
+ res.setHeader('Allow', ['POST', 'PATCH']);
+ res.status(405).end(`Method ${req.method} Not Allowed`);
+ return;
+ }
+
+ if (req.method == 'POST') {
+ return handlePost(req, res);
+ } else if (req.method == 'PATCH') {
+ return handlePatch(req, res);
+ }
}
diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js
new file mode 100644
index 0000000..6be821c
--- /dev/null
+++ b/pages/moderator/[jam].js
@@ -0,0 +1,266 @@
+import {
+ Badge,
+ Box,
+ Button,
+ Flex,
+ GridItem,
+ Heading,
+ Spacer,
+ Stack,
+ Switch,
+ Tab,
+ TabList,
+ TabPanel,
+ TabPanels,
+ Tabs,
+ Text,
+} from '@chakra-ui/react';
+import { ArrowBackIcon, ChatIcon, LockIcon } from '@chakra-ui/icons';
+import Layout from 'components/Layout';
+import AdminHeader from 'components/AdminHeader';
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import Link from 'next/link';
+import merge from 'lodash.merge';
+import ModeratorNewStatementCard from '../../components/ModeratorNewStatementCard';
+
+const LiveStatementCard = ({ statement, buttonText, onClick }) => {
+ return (
+
+ {statement.text}
+
+
+
+ {statement.isUserSubmitted ? (
+
+ Participant submitted{' '}
+ {new Date(
+ statement.createdAt?._seconds * 1000,
+ ).toUTCString()}
+
+ ) : (
+
+ Moderator submitted{' '}
+ {new Date(
+ statement.createdAt?._seconds * 1000,
+ ).toUTCString()}
+
+ )}
+
+
+
+
+
+ {buttonText}
+
+
+
+ );
+};
+
+const Jam = () => {
+ const router = useRouter();
+ const { jam: jamUrlPath } = router.query;
+ const [jam, setJam] = useState({});
+ const [published, setPublished] = useState();
+ const [location, setLocation] = useState();
+ const [approvedStatements, setApprovedStatements] = useState([]);
+ const [rejectedStatements, setRejectedStatements] = useState([]);
+ const [newStatements, setNewStatements] = useState([]);
+ const [patchSuccess, setPatchTrigger] = useState();
+
+ useEffect(() => {
+ setLocation(window.location.origin);
+ }, []);
+
+ useEffect(() => {
+ if (router.isReady) {
+ loadJam();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [router.isReady]);
+
+ useEffect(() => {
+ if (!jam.statements) {
+ return;
+ }
+
+ setApprovedStatements(
+ jam.statements.filter((statement) => statement.state === 1),
+ );
+ setRejectedStatements(
+ jam.statements.filter((statement) => statement.state === -1),
+ );
+ setNewStatements(
+ jam.statements.filter((statement) => statement.state === 0),
+ );
+ }, [jam, patchSuccess]);
+
+ const loadJam = () => {
+ fetch(
+ `/api/jam?jamUrlPath=${encodeURIComponent(
+ jamUrlPath,
+ )}&includeStatements=true`,
+ )
+ .then((response) => response.json())
+ .then((jam) => {
+ setJam(jam);
+ setPublished(jam.isOpen);
+ });
+ };
+
+ const patchStatementRequest = (body) => {
+ const { jamId, statementId, ...updateFields } = body;
+ fetch('/api/statement', {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ })
+ .then(() => {
+ setJam((jam) => {
+ var statementIndex = jam.statements.findIndex(
+ (s) => s.key == statementId,
+ );
+
+ jam.statements[statementIndex] = merge(
+ jam.statements[statementIndex],
+ updateFields,
+ );
+
+ return jam;
+ });
+ setPatchTrigger((patchTrigger) => !patchTrigger);
+ })
+ .catch(() => console.error('Bad request'));
+ };
+
+ const patchJamRequest = (body) => {
+ const { jamId, ...updateFields } = body;
+ fetch('/api/jam', {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ })
+ .then(() => {
+ setJam((jam) => {
+ jam = merge(jam, updateFields);
+ return jam;
+ });
+ setPatchTrigger((patchTrigger) => !patchTrigger);
+ })
+ .catch(() => console.error('Bad request'));
+ };
+
+ return (
+
+
+
+
+ Back to overview
+
+
+
+
+
+
+ {jam.name}
+
+
+ {`${location}/jams/${jam.urlPath}`}
+
+
+ {
+ patchJamRequest({
+ isOpen: e.target.checked,
+ jamId: jam.key,
+ });
+ setPublished(e.target.checked);
+ }}
+ >
+ {published ? (
+ Open
+ ) : (
+ Closed
+ )}
+
+ {jam.description}
+
+ {new Date(jam.createdAt?._seconds * 1000).toUTCString()}
+
+
+ Download CSV
+
+
+
+ Approved {approvedStatements.length}
+ Rejected {rejectedStatements.length}
+ New {newStatements.length}
+
+
+
+ {approvedStatements.map((statement, index) => (
+
+ patchStatementRequest({
+ jamId: jam.key,
+ statementId: statement.key,
+ state: -1,
+ })
+ }
+ />
+ ))}
+
+
+ {rejectedStatements.map((statement, index) => (
+
+ patchStatementRequest({
+ jamId: jam.key,
+ statementId: statement.key,
+ state: 1,
+ })
+ }
+ />
+ ))}
+
+
+ {newStatements.map((statement, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default Jam;
diff --git a/yarn.lock b/yarn.lock
index ca049f9..680e0ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1771,6 +1771,14 @@
dependencies:
"@chakra-ui/utils" "1.8.2"
+"@chakra-ui/icons@^1.0.15":
+ version "1.0.15"
+ resolved "https://registry.yarnpkg.com/@chakra-ui/icons/-/icons-1.0.15.tgz#90b0e3c2c161c5a100d6b83a277941b22945f880"
+ integrity sha512-MMuPwmeCil9vAXceIN/Fxn6CNHbhkLofFQaKUfs+UaBsviiU2tvS0nqGaxm/9FNzLr5ithPVWpbz3uV7DXc77g==
+ dependencies:
+ "@chakra-ui/icon" "1.1.11"
+ "@types/react" "^17.0.0"
+
"@chakra-ui/image@1.0.18":
version "1.0.18"
resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-1.0.18.tgz#9b3b8ee5a9fac5f05699b4fc1705b30aea2073f3"
@@ -4135,6 +4143,15 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/react@^17.0.0":
+ version "17.0.19"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.19.tgz#8f2a85e8180a43b57966b237d26a29481dacc991"
+ integrity sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/resolve@1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"