From 781382f0e8ad388522a8fd1a0a13f6d0eaa36fab Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 14:21:22 +0100 Subject: [PATCH 01/12] Pull questions from /jam API and basic edit page --- pages/api/jam.js | 46 +++++++++++---- pages/moderator/[jam].js | 124 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 pages/moderator/[jam].js diff --git a/pages/api/jam.js b/pages/api/jam.js index 2226367..6c18935 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, includeQuestions) { 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 (!includeQuestions) { + return finalJam; + } + + finalJam.questions = await jamsRef + .doc(finalJam.key) + .collection('statements') + .get() + .then((query) => { + const questions = []; + query.forEach((doc) => { + const question = doc.data(); + question.key = doc.id; + questions.push(question); + }); + return questions; + }); + + return finalJam; } function createJam({ name, description, statements }) { @@ -61,7 +81,7 @@ function createJam({ name, description, statements }) { export default async function handler(req, res) { const { - query: { jamUrlPath }, + query: { jamUrlPath, includeQuestions }, method, } = req; @@ -99,14 +119,16 @@ 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, includeQuestions).then( + (jam) => { + if (jam) { + res.status(200); + res.setHeader('Content-Type', 'application/json'); + res.json(jam); + } else { + res.status(404).end(); + } + }, + ); } } diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js new file mode 100644 index 0000000..47a46ad --- /dev/null +++ b/pages/moderator/[jam].js @@ -0,0 +1,124 @@ +import { + Badge, + Box, + Button, + GridItem, + Stack, + Switch, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, +} from '@chakra-ui/react'; +import Layout from 'components/Layout'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; + +const Jam = () => { + const router = useRouter(); + const { jam: jamUrlPath } = router.query; + const [jam, setJam] = useState({}); + const [published, setPublished] = useState(); + + const [location, setLocation] = useState(); + + useEffect(() => { + setLocation(window.location.origin); + }, []); + + useEffect(() => { + if (router.isReady) { + loadJam(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router.isReady]); + + const loadJam = () => { + fetch( + `/api/jam?jamUrlPath=${encodeURIComponent( + jamUrlPath, + )}&includeQuestions=true`, + ) + .then((response) => response.json()) + .then((jam) => { + setJam(jam); + setPublished(jam.isOpen); + }); + }; + + return ( + + + + + {jam.name} + + {`${location}/jams/${jam.urlPath}`} + + + + setPublished((published) => { + // SET JAM TO OPEN/CLOSED HERE + return !published; + }) + } + > + {published ? ( + + Open + + ) : ( + + Closed + + )} + + {jam.description} + + + + Approved + Rejected + New + + + a + b + c + + + + + + + ); +}; + +export default Jam; From 9817af86bf1efa3b947100cf9d9b0bc3fc49eed2 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 14:26:27 +0100 Subject: [PATCH 02/12] Filter questions in appropriate tabs --- pages/moderator/[jam].js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index 47a46ad..5a0081a 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -109,9 +109,27 @@ const Jam = () => { New - a - b - c + + {jam.questions + .filter((question) => question.state === 1) + .map((question) => ( +

{question.text}

+ ))} +
+ + {jam.questions + .filter((question) => question.state === -1) + .map((question) => ( +

{question.text}

+ ))} +
+ + {jam.questions + .filter((question) => question.state === 0) + .map((question) => ( +

{question.text}

+ ))} +
From fe68b15277d1db0423c5532dda04bb6b8edc0352 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 14:36:58 +0100 Subject: [PATCH 03/12] Split question types into their own states --- pages/moderator/[jam].js | 50 +++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index 5a0081a..6d9a789 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -22,8 +22,10 @@ const Jam = () => { const { jam: jamUrlPath } = router.query; const [jam, setJam] = useState({}); const [published, setPublished] = useState(); - const [location, setLocation] = useState(); + const [approvedQuestions, setApprovedQuestions] = useState([]); + const [rejectedQuestions, setRejectedQuestions] = useState([]); + const [newQuestions, setNewQuestions] = useState([]); useEffect(() => { setLocation(window.location.origin); @@ -36,6 +38,22 @@ const Jam = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady]); + useEffect(() => { + if (!jam.questions) { + return; + } + + setApprovedQuestions( + jam.questions.filter((question) => question.state === 1), + ); + setRejectedQuestions( + jam.questions.filter((question) => question.state === -1), + ); + setNewQuestions( + jam.questions.filter((question) => question.state === 0), + ); + }, [jam]); + const loadJam = () => { fetch( `/api/jam?jamUrlPath=${encodeURIComponent( @@ -104,31 +122,25 @@ const Jam = () => { - Approved - Rejected - New + Approved {approvedQuestions.length} + Rejected {rejectedQuestions.length} + New {newQuestions.length} - {jam.questions - .filter((question) => question.state === 1) - .map((question) => ( -

{question.text}

- ))} + {approvedQuestions.map((question) => ( + {question.text} + ))}
- {jam.questions - .filter((question) => question.state === -1) - .map((question) => ( -

{question.text}

- ))} + {rejectedQuestions.map((question) => ( + {question.text} + ))}
- {jam.questions - .filter((question) => question.state === 0) - .map((question) => ( -

{question.text}

- ))} + {newQuestions.map((question) => ( + {question.text} + ))}
From c6e6d27fea7019712ff07cc1b94fd23d8bcab5a4 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 15:53:02 +0100 Subject: [PATCH 04/12] Add basic QuestionCard and back link with icons --- package.json | 1 + pages/moderator/[jam].js | 66 +++++++++++++++++++++++++++++++++++++--- yarn.lock | 17 +++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) 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/moderator/[jam].js b/pages/moderator/[jam].js index 6d9a789..3be291a 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -3,6 +3,7 @@ import { Box, Button, GridItem, + Heading, Stack, Switch, Tab, @@ -12,11 +13,49 @@ import { Tabs, Text, } from '@chakra-ui/react'; +import { ArrowBackIcon, ChatIcon, LockIcon } from '@chakra-ui/icons'; import Layout from 'components/Layout'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Link from 'next/link'; +const QuestionCard = ({ question, buttonText }) => { + return ( + + {question.text} + + {question.isUserSubmitted ? ( + + Participant submitted{' '} + {new Date( + question.createdAt?._seconds * 1000, + ).toUTCString()} + + ) : ( + + Moderator submitted{' '} + {new Date( + question.createdAt?._seconds * 1000, + ).toUTCString()} + + )} + + + + + + ); +}; + const Jam = () => { const router = useRouter(); const { jam: jamUrlPath } = router.query; @@ -69,10 +108,17 @@ const Jam = () => { return ( + + + Back to overview + + - {jam.name} + + {jam.name} + {`${location}/jams/${jam.urlPath}`} @@ -117,6 +163,9 @@ const Jam = () => { )} {jam.description} + + {new Date(jam.createdAt?._seconds * 1000).toUTCString()} + @@ -129,17 +178,26 @@ const Jam = () => { {approvedQuestions.map((question) => ( - {question.text} + ))} {rejectedQuestions.map((question) => ( - {question.text} + ))} {newQuestions.map((question) => ( - {question.text} + ))} 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" From 86720bfaf3318e56533d37485e365af746a8a252 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 17:20:25 +0100 Subject: [PATCH 05/12] Add cards for questions --- pages/moderator/[jam].js | 114 ++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 49 deletions(-) diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index 3be291a..7d2a06d 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -2,8 +2,10 @@ import { Badge, Box, Button, + Flex, GridItem, Heading, + Spacer, Stack, Switch, Tab, @@ -19,7 +21,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Link from 'next/link'; -const QuestionCard = ({ question, buttonText }) => { +const LiveQuestionCard = ({ question, buttonText }) => { return ( { > {question.text} - {question.isUserSubmitted ? ( - - Participant submitted{' '} - {new Date( - question.createdAt?._seconds * 1000, - ).toUTCString()} - - ) : ( - - Moderator submitted{' '} - {new Date( - question.createdAt?._seconds * 1000, - ).toUTCString()} - - )} + + + {question.isUserSubmitted ? ( + + Participant submitted{' '} + {new Date( + question.createdAt?._seconds * 1000, + ).toUTCString()} + + ) : ( + + Moderator submitted{' '} + {new Date( + question.createdAt?._seconds * 1000, + ).toUTCString()} + + )} + + + + + + + + ); +}; + +const NewQuestionCard = ({ question }) => { + return ( + + {question.text} + + + + + Participant submitted{' '} + {new Date( + question.createdAt?._seconds * 1000, + ).toUTCString()} + + + + - - - + + + + + + ); }; @@ -124,7 +162,7 @@ const Jam = () => { @@ -135,31 +173,9 @@ const Jam = () => { } > {published ? ( - - Open - + Open ) : ( - - Closed - + Closed )} {jam.description} @@ -178,7 +194,7 @@ const Jam = () => { {approvedQuestions.map((question) => ( - @@ -186,7 +202,7 @@ const Jam = () => { {rejectedQuestions.map((question) => ( - @@ -194,7 +210,7 @@ const Jam = () => { {newQuestions.map((question) => ( - From d6b176f658bee59609478541f8f27d64acff48d7 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 18:46:39 +0100 Subject: [PATCH 06/12] Add PATCH to /statement and add approve/reject --- pages/api/statement.js | 41 +++++++++++++++++++++++---- pages/moderator/[jam].js | 61 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 12 deletions(-) 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 index 7d2a06d..2a9e358 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -20,8 +20,10 @@ import Layout from 'components/Layout'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Link from 'next/link'; +import cloneDeep from 'lodash.clonedeep'; +import merge from 'lodash.merge'; -const LiveQuestionCard = ({ question, buttonText }) => { +const LiveQuestionCard = ({ question, buttonText, onClick }) => { return ( { - + ); @@ -103,6 +107,7 @@ const Jam = () => { const [approvedQuestions, setApprovedQuestions] = useState([]); const [rejectedQuestions, setRejectedQuestions] = useState([]); const [newQuestions, setNewQuestions] = useState([]); + const [patchSuccess, setPatchTrigger] = useState(); useEffect(() => { setLocation(window.location.origin); @@ -129,7 +134,7 @@ const Jam = () => { setNewQuestions( jam.questions.filter((question) => question.state === 0), ); - }, [jam]); + }, [jam, patchSuccess]); const loadJam = () => { fetch( @@ -144,6 +149,33 @@ const Jam = () => { }); }; + const patchQuestion = (body) => { + const { jamId, statementId, ...updateFields } = body; + fetch('/api/statement', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + .then(() => { + setJam((jam) => { + var questionIndex = jam.questions.findIndex( + (q) => q.key == statementId, + ); + + jam.questions[questionIndex] = merge( + jam.questions[questionIndex], + updateFields, + ); + + return jam; + }); + setPatchTrigger((patchTrigger) => !patchTrigger); + }) + .catch(() => console.error('Bad request')); + }; + return ( @@ -193,24 +225,41 @@ const Jam = () => { - {approvedQuestions.map((question) => ( + {approvedQuestions.map((question, index) => ( + patchQuestion({ + jamId: jam.key, + statementId: question.key, + state: -1, + }) + } /> ))} - {rejectedQuestions.map((question) => ( + {rejectedQuestions.map((question, index) => ( + patchQuestion({ + jamId: jam.key, + statementId: question.key, + state: 1, + }) + } /> ))} - {newQuestions.map((question) => ( + {newQuestions.map((question, index) => ( From f9a6519345f44b341dd4200d050e3e9198aeedb8 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 19:04:47 +0100 Subject: [PATCH 07/12] Rename to use statements not questions --- pages/api/jam.js | 20 ++--- pages/moderator/[jam].js | 161 ++++++++++++++++++++++++--------------- 2 files changed, 110 insertions(+), 71 deletions(-) diff --git a/pages/api/jam.js b/pages/api/jam.js index 6c18935..cb5ad06 100644 --- a/pages/api/jam.js +++ b/pages/api/jam.js @@ -1,7 +1,7 @@ import fire from '../../config/firebaseAdminConfig'; import ensureAdmin from 'utils/admin-auth-middleware'; -async function getJamByUrlPath(jamUrlPath, includeQuestions) { +async function getJamByUrlPath(jamUrlPath, includeStatements) { const db = fire.firestore(); const jamsRef = db.collection('jams'); @@ -18,22 +18,22 @@ async function getJamByUrlPath(jamUrlPath, includeQuestions) { return jams[0]; }); - if (!includeQuestions) { + if (!includeStatements) { return finalJam; } - finalJam.questions = await jamsRef + finalJam.statements = await jamsRef .doc(finalJam.key) .collection('statements') .get() .then((query) => { - const questions = []; + const statements = []; query.forEach((doc) => { - const question = doc.data(); - question.key = doc.id; - questions.push(question); + const statement = doc.data(); + statement.key = doc.id; + statements.push(statement); }); - return questions; + return statements; }); return finalJam; @@ -81,7 +81,7 @@ function createJam({ name, description, statements }) { export default async function handler(req, res) { const { - query: { jamUrlPath, includeQuestions }, + query: { jamUrlPath, includeStatements }, method, } = req; @@ -119,7 +119,7 @@ export default async function handler(req, res) { res.status(500).json({ error: error }); } } else if (method === 'GET') { - return getJamByUrlPath(jamUrlPath, includeQuestions).then( + return getJamByUrlPath(jamUrlPath, includeStatements).then( (jam) => { if (jam) { res.status(200); diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index 2a9e358..389f3bb 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -20,10 +20,9 @@ import Layout from 'components/Layout'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Link from 'next/link'; -import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; -const LiveQuestionCard = ({ question, buttonText, onClick }) => { +const LiveStatementCard = ({ statement, buttonText, onClick }) => { return ( { my={4} backgroundColor="white" > - {question.text} + {statement.text} - {question.isUserSubmitted ? ( + {statement.isUserSubmitted ? ( Participant submitted{' '} {new Date( - question.createdAt?._seconds * 1000, + statement.createdAt?._seconds * 1000, ).toUTCString()} ) : ( Moderator submitted{' '} {new Date( - question.createdAt?._seconds * 1000, + statement.createdAt?._seconds * 1000, ).toUTCString()} )} @@ -64,8 +63,14 @@ const LiveQuestionCard = ({ question, buttonText, onClick }) => { ); }; -const NewQuestionCard = ({ question }) => { - return ( +const NewStatementCard = ({ statement, jamId, patchStatement }) => { + const [isEditable, setIsEditable] = useState(false); + + const invertEditable = () => { + setIsEditable((isEditable) => !isEditable); + }; + + if (isEditable) { { borderColor="gray.200" my={4} backgroundColor="white" - > - {question.text} + >; + } else { + return ( + + {statement.text} - - - - Participant submitted{' '} - {new Date( - question.createdAt?._seconds * 1000, - ).toUTCString()} - - + + + + Participant submitted{' '} + {new Date( + statement.createdAt?._seconds * 1000, + ).toUTCString()} + + - + - - - - - - - - ); + + + + + + + + ); + } }; const Jam = () => { @@ -104,9 +142,9 @@ const Jam = () => { const [jam, setJam] = useState({}); const [published, setPublished] = useState(); const [location, setLocation] = useState(); - const [approvedQuestions, setApprovedQuestions] = useState([]); - const [rejectedQuestions, setRejectedQuestions] = useState([]); - const [newQuestions, setNewQuestions] = useState([]); + const [approvedStatements, setApprovedStatements] = useState([]); + const [rejectedStatements, setRejectedStatements] = useState([]); + const [newStatements, setNewStatements] = useState([]); const [patchSuccess, setPatchTrigger] = useState(); useEffect(() => { @@ -121,18 +159,18 @@ const Jam = () => { }, [router.isReady]); useEffect(() => { - if (!jam.questions) { + if (!jam.statements) { return; } - setApprovedQuestions( - jam.questions.filter((question) => question.state === 1), + setApprovedStatements( + jam.statements.filter((statement) => statement.state === 1), ); - setRejectedQuestions( - jam.questions.filter((question) => question.state === -1), + setRejectedStatements( + jam.statements.filter((statement) => statement.state === -1), ); - setNewQuestions( - jam.questions.filter((question) => question.state === 0), + setNewStatements( + jam.statements.filter((statement) => statement.state === 0), ); }, [jam, patchSuccess]); @@ -140,7 +178,7 @@ const Jam = () => { fetch( `/api/jam?jamUrlPath=${encodeURIComponent( jamUrlPath, - )}&includeQuestions=true`, + )}&includeStatements=true`, ) .then((response) => response.json()) .then((jam) => { @@ -149,7 +187,7 @@ const Jam = () => { }); }; - const patchQuestion = (body) => { + const patchStatement = (body) => { const { jamId, statementId, ...updateFields } = body; fetch('/api/statement', { method: 'PATCH', @@ -160,12 +198,12 @@ const Jam = () => { }) .then(() => { setJam((jam) => { - var questionIndex = jam.questions.findIndex( - (q) => q.key == statementId, + var statementIndex = jam.statements.findIndex( + (s) => s.key == statementId, ); - jam.questions[questionIndex] = merge( - jam.questions[questionIndex], + jam.statements[statementIndex] = merge( + jam.statements[statementIndex], updateFields, ); @@ -219,21 +257,21 @@ const Jam = () => { - Approved {approvedQuestions.length} - Rejected {rejectedQuestions.length} - New {newQuestions.length} + Approved {approvedStatements.length} + Rejected {rejectedStatements.length} + New {newStatements.length} - {approvedQuestions.map((question, index) => ( - ( + - patchQuestion({ + patchStatement({ jamId: jam.key, - statementId: question.key, + statementId: statement.key, state: -1, }) } @@ -241,15 +279,15 @@ const Jam = () => { ))} - {rejectedQuestions.map((question, index) => ( - ( + - patchQuestion({ + patchStatement({ jamId: jam.key, - statementId: question.key, + statementId: statement.key, state: 1, }) } @@ -257,11 +295,12 @@ const Jam = () => { ))} - {newQuestions.map((question, index) => ( - ( + ))} From 9f5d458afc154255abdf3c4ca9f668d8973b63a9 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 19:21:54 +0100 Subject: [PATCH 08/12] Add edit functionality for new statements --- components/ModeratorNewStatementCard.js | 160 ++++++++++++++++++++++++ pages/moderator/[jam].js | 84 +------------ 2 files changed, 166 insertions(+), 78 deletions(-) create mode 100644 components/ModeratorNewStatementCard.js diff --git a/components/ModeratorNewStatementCard.js b/components/ModeratorNewStatementCard.js new file mode 100644 index 0000000..8def96c --- /dev/null +++ b/components/ModeratorNewStatementCard.js @@ -0,0 +1,160 @@ +import { + Box, + Button, + Flex, + Spacer, + Stack, + Text, + Textarea, +} from '@chakra-ui/react'; +import { ChatIcon } from '@chakra-ui/icons'; +import { useState } from 'react'; + +const ModeratorEditStatementCard = ({ + statement, + jamId, + patchRequest, + invertEditable, +}) => { + const [editedStatement, setEditedStatement] = useState( + statement.text, + ); + + const handleStatementChange = (e) => { + let statementValue = e.target.value; + setEditedStatement(statementValue); + }; + + return ( + + + + + + + + ); +}; + +const ModeratorDecisionStatementCard = ({ + statement, + jamId, + patchRequest, + invertEditable, +}) => { + return ( + + {statement.text} + + + + + Participant submitted{' '} + {new Date( + statement.createdAt?._seconds * 1000, + ).toUTCString()} + + + + + + + + + + + + + ); +}; + +const ModeratorNewStatementCard = ({ + statement, + jamId, + patchRequest, +}) => { + const [isEditable, setIsEditable] = useState(false); + + const invertEditable = () => { + setIsEditable((isEditable) => !isEditable); + }; + + if (isEditable) { + return ( + + ); + } else { + return ( + + ); + } +}; + +export default ModeratorNewStatementCard; diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index 389f3bb..e6f0736 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -21,6 +21,7 @@ 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 ( @@ -63,79 +64,6 @@ const LiveStatementCard = ({ statement, buttonText, onClick }) => { ); }; -const NewStatementCard = ({ statement, jamId, patchStatement }) => { - const [isEditable, setIsEditable] = useState(false); - - const invertEditable = () => { - setIsEditable((isEditable) => !isEditable); - }; - - if (isEditable) { - ; - } else { - return ( - - {statement.text} - - - - - Participant submitted{' '} - {new Date( - statement.createdAt?._seconds * 1000, - ).toUTCString()} - - - - - - - - - - - - - ); - } -}; - const Jam = () => { const router = useRouter(); const { jam: jamUrlPath } = router.query; @@ -187,7 +115,7 @@ const Jam = () => { }); }; - const patchStatement = (body) => { + const patchRequest = (body) => { const { jamId, statementId, ...updateFields } = body; fetch('/api/statement', { method: 'PATCH', @@ -269,7 +197,7 @@ const Jam = () => { statement={statement} buttonText="Reject" onClick={() => - patchStatement({ + patchRequest({ jamId: jam.key, statementId: statement.key, state: -1, @@ -285,7 +213,7 @@ const Jam = () => { statement={statement} buttonText="Approve" onClick={() => - patchStatement({ + patchRequest({ jamId: jam.key, statementId: statement.key, state: 1, @@ -296,10 +224,10 @@ const Jam = () => { {newStatements.map((statement, index) => ( - ))} From fedfd7719b02ea053fbf66615cb82bed9caa71d7 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 19:34:11 +0100 Subject: [PATCH 09/12] Add patch to close Jams --- pages/api/jam.js | 20 ++++++++++++++++++++ pages/moderator/[jam].js | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/pages/api/jam.js b/pages/api/jam.js index cb5ad06..0081b8b 100644 --- a/pages/api/jam.js +++ b/pages/api/jam.js @@ -79,6 +79,24 @@ 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, includeStatements }, @@ -130,5 +148,7 @@ export default async function handler(req, res) { } }, ); + } else if (method === 'PATCH') { + return patchJam(req, res); } } diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index e6f0736..b192905 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -115,7 +115,7 @@ const Jam = () => { }); }; - const patchRequest = (body) => { + const patchStatementRequest = (body) => { const { jamId, statementId, ...updateFields } = body; fetch('/api/statement', { method: 'PATCH', @@ -142,6 +142,25 @@ const Jam = () => { .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 ( @@ -163,12 +182,13 @@ const Jam = () => { size="md" colorScheme="green" isChecked={published} - onChange={() => - setPublished((published) => { - // SET JAM TO OPEN/CLOSED HERE - return !published; - }) - } + onChange={(e) => { + patchJamRequest({ + isOpen: e.target.checked, + jamId: jam.key, + }); + setPublished(e.target.checked); + }} > {published ? ( Open @@ -197,7 +217,7 @@ const Jam = () => { statement={statement} buttonText="Reject" onClick={() => - patchRequest({ + patchStatementRequest({ jamId: jam.key, statementId: statement.key, state: -1, @@ -213,7 +233,7 @@ const Jam = () => { statement={statement} buttonText="Approve" onClick={() => - patchRequest({ + patchStatementRequest({ jamId: jam.key, statementId: statement.key, state: 1, @@ -227,7 +247,7 @@ const Jam = () => { ))} From 91f4867c62a007dd82b07a6c33b40e2ec968c255 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Fri, 27 Aug 2021 19:39:45 +0100 Subject: [PATCH 10/12] Add admin header and change link --- components/OverviewJamCard.jsx | 2 +- pages/moderator/[jam].js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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" > - + { return ( + Back to overview From 3650d3cf85c0f5cd7986a75235e145ae0f06b2a8 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Sat, 28 Aug 2021 18:55:41 +0100 Subject: [PATCH 11/12] Add ComponentSwitcher In both the create jam page and the edit jam page we have the same behaviour of allowing the admin to modify "new" statements. This consists of the admin triggering an event which causes the statement to become editable, and from that state triggering some event to go back to the read-only state. I added the ComponentSwitcher component so that we can re-use the code to switch between components without making any assumptions to which event triggers the switching. --- components/ComponentSwitcher.js | 25 +++++++++++ components/ModeratorNewStatementCard.js | 54 +++++++++++------------- components/ProposedStatementCard.js | 55 +++++++++++-------------- 3 files changed, 72 insertions(+), 62 deletions(-) create mode 100644 components/ComponentSwitcher.js 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 index 8def96c..4a97ce0 100644 --- a/components/ModeratorNewStatementCard.js +++ b/components/ModeratorNewStatementCard.js @@ -9,12 +9,13 @@ import { } from '@chakra-ui/react'; import { ChatIcon } from '@chakra-ui/icons'; import { useState } from 'react'; +import ComponentSwitcher from './ComponentSwitcher'; const ModeratorEditStatementCard = ({ statement, jamId, patchRequest, - invertEditable, + invertComponent, }) => { const [editedStatement, setEditedStatement] = useState( statement.text, @@ -41,7 +42,7 @@ const ModeratorEditStatementCard = ({ mb={4} > - - @@ -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; From d7df41a1e61f31a06fcfa96b3843c55b78c8fd32 Mon Sep 17 00:00:00 2001 From: Siame Rafiq Date: Tue, 31 Aug 2021 10:04:12 +0100 Subject: [PATCH 12/12] PR fixes --- components/ModeratorNewStatementCard.js | 5 +++-- pages/moderator/[jam].js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/components/ModeratorNewStatementCard.js b/components/ModeratorNewStatementCard.js index 4a97ce0..d49950e 100644 --- a/components/ModeratorNewStatementCard.js +++ b/components/ModeratorNewStatementCard.js @@ -2,6 +2,7 @@ import { Box, Button, Flex, + HStack, Spacer, Stack, Text, @@ -92,7 +93,7 @@ const ModeratorDecisionStatementCard = ({ - + @@ -120,7 +121,7 @@ const ModeratorDecisionStatementCard = ({ > Approve - + ); diff --git a/pages/moderator/[jam].js b/pages/moderator/[jam].js index fc35653..6be821c 100644 --- a/pages/moderator/[jam].js +++ b/pages/moderator/[jam].js @@ -40,14 +40,14 @@ const LiveStatementCard = ({ statement, buttonText, onClick }) => { {statement.isUserSubmitted ? ( - Participant submitted{' '} + Participant submitted{' '} {new Date( statement.createdAt?._seconds * 1000, ).toUTCString()} ) : ( - Moderator submitted{' '} + Moderator submitted{' '} {new Date( statement.createdAt?._seconds * 1000, ).toUTCString()}