Skip to content

Commit 782ff6c

Browse files
committed
edit announcements
1 parent 6ca6b25 commit 782ff6c

7 files changed

Lines changed: 198 additions & 23 deletions

File tree

api/src/controllers/announcement.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,34 @@ export const pinAnnouncement = async (req, res) => {
208208
res.status(500).send(e.message)
209209
}
210210
}
211+
212+
export const editAnnouncement = async (req, res) => {
213+
if (req.body.title && req.body.body) {
214+
try {
215+
if (req.userRole !== 'admin') {
216+
res
217+
.status(401)
218+
.send('You do not have permission to edit an announcement')
219+
return
220+
}
221+
222+
const announcement = await Announcement.findOneAndUpdate(
223+
{ _id: req.params.announcementId },
224+
{
225+
$set: {
226+
title: req.body.title,
227+
body: req.body.body,
228+
pinned: req.body.pinned,
229+
updated: Date.now(),
230+
},
231+
}
232+
)
233+
234+
res.send(announcement.slug)
235+
} catch (e) {
236+
res.status(500).send(e.message)
237+
}
238+
} else {
239+
res.status(400).send('Request must include title and body')
240+
}
241+
}

api/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
getPinnedAnnouncements,
4242
deleteAnnouncement,
4343
pinAnnouncement,
44+
editAnnouncement,
4445
} from './controllers/announcement'
4546
import {
4647
createReport,
@@ -172,6 +173,7 @@ app.get('/announcements/:slug', fetchAnnouncement)
172173
app.get('/announcements/page/:page', getAnnouncements)
173174
app.delete('/announcements/:slug', deleteAnnouncement)
174175
app.post('/announcements/pin/:announcementId/:action', pinAnnouncement)
176+
app.post('/announcements/edit/:announcementId', editAnnouncement)
175177

176178
// moderation routes
177179
app.get('/reports/page/:page', getReports)

api/src/schema/announcement.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const Announcement = new mongoose.Schema({
77
createdBy: mongoose.Schema.ObjectId,
88
pinned: Boolean,
99
created: Number,
10+
updated: Number,
1011
})
1112

1213
export default mongoose.model('announcement', Announcement)

api/src/utils/validateConfig.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as yup from 'yup'
22
import config from '../../../config'
33

4+
const httpRegex = /http(s)?:\/\/.*/
5+
const mongoRegex = /mongodb:\/\/.*/
6+
47
const configSchema = yup
58
.object({
69
envs: yup
@@ -23,10 +26,10 @@ const configSchema = yup
2326
)
2427
)
2528
.required(),
26-
SQ_BASE_URL: yup.string().required(),
27-
SQ_API_URL: yup.string().required(),
28-
SQ_TRACKER_URL: yup.string().required(),
29-
SQ_MONGO_URL: yup.string().required(),
29+
SQ_BASE_URL: yup.string().matches(httpRegex).required(),
30+
SQ_API_URL: yup.string().matches(httpRegex).required(),
31+
SQ_TRACKER_URL: yup.string().matches(httpRegex).required(),
32+
SQ_MONGO_URL: yup.string().matches(mongoRegex).required(),
3033
SQ_MAIL_FROM_ADDRESS: yup.string().email().required(),
3134
SQ_SMTP_HOST: yup.string().required(),
3235
SQ_SMTP_PORT: yup.number().integer().min(1).max(65535).required(),

client/components/Checkbox.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ const Container = styled.label(
5151
space
5252
)
5353

54-
const Checkbox = ({ label, name, ...rest }) => (
54+
const Checkbox = ({ label, name, inputProps, ...rest }) => (
5555
<Container {...rest}>
56-
<input type="checkbox" name={name} />
56+
<input type="checkbox" name={name} {...inputProps} />
5757
<Box alignItems="center" justifyContent="center" className="check">
5858
<Box className="inner" />
5959
</Box>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useContext } from 'react'
2+
import getConfig from 'next/config'
3+
import { useRouter } from 'next/router'
4+
import jwt from 'jsonwebtoken'
5+
import SEO from '../../../components/SEO'
6+
import Text from '../../../components/Text'
7+
import Input from '../../../components/Input'
8+
import Checkbox from '../../../components/Checkbox'
9+
import Button from '../../../components/Button'
10+
import { withAuthServerSideProps } from '../../../utils/withAuth'
11+
import { NotificationContext } from '../../../components/Notifications'
12+
13+
const EditAnnouncement = ({ announcement, token, userRole }) => {
14+
if (userRole !== 'admin') {
15+
return <Text>You do not have permission to do that.</Text>
16+
}
17+
18+
const { addNotification } = useContext(NotificationContext)
19+
20+
const router = useRouter()
21+
22+
const {
23+
publicRuntimeConfig: { SQ_API_URL },
24+
} = getConfig()
25+
26+
const handleCreate = async (e) => {
27+
e.preventDefault()
28+
const form = new FormData(e.target)
29+
30+
try {
31+
const updateAnnouncementRes = await fetch(
32+
`${SQ_API_URL}/announcements/edit/${announcement._id}`,
33+
{
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
Authorization: `Bearer ${token}`,
38+
},
39+
body: JSON.stringify({
40+
title: form.get('title'),
41+
body: form.get('body'),
42+
pinned: !!form.get('pinned'),
43+
}),
44+
}
45+
)
46+
47+
if (updateAnnouncementRes.status !== 200) {
48+
const reason = await updateAnnouncementRes.text()
49+
throw new Error(reason)
50+
}
51+
52+
addNotification('success', 'Announcement updated successfully')
53+
54+
const slug = await updateAnnouncementRes.text()
55+
router.push(`/announcements/${slug}`)
56+
} catch (e) {
57+
addNotification('error', `Could not update announcement: ${e.message}`)
58+
console.error(e)
59+
}
60+
}
61+
62+
return (
63+
<>
64+
<SEO title="Edit announcement" />
65+
<Text as="h1" mb={5}>
66+
Edit announcement
67+
</Text>
68+
<form onSubmit={handleCreate}>
69+
<Input
70+
name="title"
71+
label="Title"
72+
defaultValue={announcement.title}
73+
mb={4}
74+
required
75+
/>
76+
<Input
77+
name="body"
78+
label="Body"
79+
placeholder="Markdown supported"
80+
defaultValue={announcement.body}
81+
rows={10}
82+
mb={4}
83+
required
84+
/>
85+
<Checkbox
86+
label="Pin this announcement?"
87+
name="pinned"
88+
inputProps={{ defaultChecked: announcement.pinned }}
89+
mb={4}
90+
/>
91+
<Button display="block" ml="auto">
92+
Update announcement
93+
</Button>
94+
</form>
95+
</>
96+
)
97+
}
98+
99+
export const getServerSideProps = withAuthServerSideProps(
100+
async ({ token, query: { slug } }) => {
101+
if (!token) return { props: {} }
102+
103+
const {
104+
publicRuntimeConfig: { SQ_API_URL },
105+
serverRuntimeConfig: { SQ_JWT_SECRET },
106+
} = getConfig()
107+
108+
const { role } = jwt.verify(token, SQ_JWT_SECRET)
109+
110+
const announcementRes = await fetch(`${SQ_API_URL}/announcements/${slug}`, {
111+
headers: {
112+
'Content-Type': 'application/json',
113+
Authorization: `Bearer ${token}`,
114+
},
115+
})
116+
const announcement = await announcementRes.json()
117+
return { props: { announcement, token, userRole: role } }
118+
}
119+
)
120+
121+
export default EditAnnouncement

client/pages/announcements/[slug].js renamed to client/pages/announcements/[slug]/index.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import jwt from 'jsonwebtoken'
77
import ReactMarkdown from 'react-markdown'
88
import remarkGfm from 'remark-gfm'
99
import { Pin } from '@styled-icons/boxicons-regular'
10-
import SEO from '../../components/SEO'
11-
import Box from '../../components/Box'
12-
import Text from '../../components/Text'
13-
import Button from '../../components/Button'
14-
import MarkdownBody from '../../components/MarkdownBody'
15-
import { withAuthServerSideProps } from '../../utils/withAuth'
16-
import { NotificationContext } from '../../components/Notifications'
10+
import SEO from '../../../components/SEO'
11+
import Box from '../../../components/Box'
12+
import Text from '../../../components/Text'
13+
import Button from '../../../components/Button'
14+
import MarkdownBody from '../../../components/MarkdownBody'
15+
import { withAuthServerSideProps } from '../../../utils/withAuth'
16+
import { NotificationContext } from '../../../components/Notifications'
1717

1818
const Announcement = ({ announcement, token, userRole }) => {
1919
const [pinned, setPinned] = useState(announcement.pinned)
@@ -108,20 +108,37 @@ const Announcement = ({ announcement, token, userRole }) => {
108108
<Button onClick={handlePin} variant="secondary" mr={3}>
109109
{pinned ? 'Unpin' : 'Pin'}
110110
</Button>
111-
<Button onClick={handleDelete}>Delete</Button>
111+
<Link href={`${router.asPath}/edit`} passHref>
112+
<a>
113+
<Button variant="secondary" mr={3}>
114+
Edit
115+
</Button>
116+
</a>
117+
</Link>
118+
<Button onClick={handleDelete} variant="secondary">
119+
Delete
120+
</Button>
112121
</Box>
113122
)}
114123
</Box>
115-
<Text color="grey" mb={5}>
116-
Posted {moment(announcement.created).format('HH:mm Do MMM YYYY')} by{' '}
117-
{announcement.createdBy?.username ? (
118-
<Link href={`/user/${announcement.createdBy.username}`} passHref>
119-
<a>{announcement.createdBy.username}</a>
120-
</Link>
121-
) : (
122-
'deleted user'
124+
<Box mb={5}>
125+
<Text color="grey">
126+
Posted {moment(announcement.created).format('HH:mm Do MMM YYYY')} by{' '}
127+
{announcement.createdBy?.username ? (
128+
<Link href={`/user/${announcement.createdBy.username}`} passHref>
129+
<a>{announcement.createdBy.username}</a>
130+
</Link>
131+
) : (
132+
'deleted user'
133+
)}
134+
</Text>
135+
{announcement.updated && (
136+
<Text color="grey" mt={3}>
137+
Last updated{' '}
138+
{moment(announcement.updated).format('HH:mm Do MMM YYYY')}
139+
</Text>
123140
)}
124-
</Text>
141+
</Box>
125142
<MarkdownBody>
126143
<ReactMarkdown remarkPlugins={[remarkGfm]}>
127144
{announcement.body}

0 commit comments

Comments
 (0)