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()} + + )} + + + + + + + + ); +}; + +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()} + + + + + 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"