diff --git a/backend/modules/alert/graphql.js b/backend/modules/alert/graphql.js index 5915495ad..0b3c1f1d1 100644 --- a/backend/modules/alert/graphql.js +++ b/backend/modules/alert/graphql.js @@ -1,5 +1,5 @@ const { withFilter } = require('graphql-subscriptions') -// const { RedisPubSub } = require('graphql-redis-subscriptions') +const { RedisPubSub } = require('graphql-redis-subscriptions') const { GraphQLString, GraphQLObjectType, @@ -11,12 +11,12 @@ const { const { GraphQLDate } = require('graphql-iso-date') const RegistrationController = require('../registration/controller') const Event = require('../event/model') -// const Redis = require('ioredis') +const Redis = require('ioredis') -// const pubsub = new RedisPubSub({ -// publisher: new Redis(process.env.REDISCLOUD_URL), -// subscriber: new Redis(process.env.REDISCLOUD_URL), -// }) +const pubsub = new RedisPubSub({ + publisher: new Redis(process.env.REDISCLOUD_URL), + subscriber: new Redis(process.env.REDISCLOUD_URL), +}) const AlertInput = new GraphQLInputObjectType({ name: 'AlertInput', @@ -116,13 +116,13 @@ const Resolvers = { throw new Error('You are not an organiser of this event') } - // pubsub.publish('ALERT_SENT', { - // newAlert: { - // ...args.alert, - // sentAt: new Date(), - // sender: userId, - // }, - // }) + pubsub.publish('ALERT_SENT', { + newAlert: { + ...args.alert, + sentAt: new Date(), + sender: userId, + }, + }) return context.controller('Alert').send(args.alert, userId) }, @@ -131,7 +131,7 @@ const Resolvers = { newAlert: { subscribe: withFilter( () => { - // return pubsub.asyncIterator('ALERT_SENT') + return pubsub.asyncIterator('ALERT_SENT') }, async ({ newAlert }, { eventId, slug }, { user }) => { // Check authentication from context diff --git a/backend/modules/event/graphql.js b/backend/modules/event/graphql.js index f4c6bcf2f..007927043 100644 --- a/backend/modules/event/graphql.js +++ b/backend/modules/event/graphql.js @@ -52,6 +52,8 @@ const { EventPageScriptInput, ScoreCriteriaSettings, ScoreCriteriaSettingsInput, + Certificate, + CertificateInput, } = require('../graphql-shared-types') const Organization = require('../organization/model') @@ -318,6 +320,9 @@ const EventInput = new GraphQLInputObjectType({ experimental: { type: GraphQLBoolean, }, + certificate: { + type: CertificateInput, + }, }, }) @@ -542,6 +547,9 @@ const EventType = new GraphQLObjectType({ experimental: { type: GraphQLBoolean, }, + Certificate: { + type: Certificate, + }, } }, }) diff --git a/backend/modules/message/graphql.js b/backend/modules/message/graphql.js index 78a9b4399..07103c25a 100644 --- a/backend/modules/message/graphql.js +++ b/backend/modules/message/graphql.js @@ -1,6 +1,6 @@ const { GraphQLBoolean } = require('graphql') const { withFilter } = require('graphql-subscriptions') -// const { RedisPubSub } = require('graphql-redis-subscriptions') +const { RedisPubSub } = require('graphql-redis-subscriptions') const { GraphQLString, GraphQLObjectType, @@ -10,12 +10,12 @@ const { GraphQLInputObjectType, } = require('graphql') const { GraphQLDate } = require('graphql-iso-date') -// const Redis = require('ioredis') +const Redis = require('ioredis') -// const pubsub = new RedisPubSub({ -// publisher: new Redis(process.env.REDISCLOUD_URL), -// subscriber: new Redis(process.env.REDISCLOUD_URL) -// }) +const pubsub = new RedisPubSub({ + publisher: new Redis(process.env.REDISCLOUD_URL), + subscriber: new Redis(process.env.REDISCLOUD_URL), +}) const MessageInput = new GraphQLInputObjectType({ name: 'MessageInput', fields: { @@ -118,13 +118,13 @@ const Resolvers = { const userId = context.req.user ? context.req.user.sub : null if (!userId) return null - // pubsub.publish('MESSAGE_SENT', { - // newMessage: { - // ...args.message, - // sentAt: new Date(), - // sender: userId, - // }, - // }) + pubsub.publish('MESSAGE_SENT', { + newMessage: { + ...args.message, + sentAt: new Date(), + sender: userId, + }, + }) return context.controller('Message').send(args.message, userId) }, @@ -145,7 +145,7 @@ const Resolvers = { newMessage: { subscribe: withFilter( () => { - // return pubsub.asyncIterator('MESSAGE_SENT') + return pubsub.asyncIterator('MESSAGE_SENT') }, ({ newMessage }, _, { user }) => { // Check authentication from context diff --git a/backend/modules/team/controller.js b/backend/modules/team/controller.js index 025dde271..40c2f4304 100644 --- a/backend/modules/team/controller.js +++ b/backend/modules/team/controller.js @@ -8,6 +8,8 @@ const { NotFoundError, } = require('../../common/errors/errors') +const maxTeamSize = 5 + const controller = {} controller.getRoles = (eventId, code) => { @@ -51,6 +53,7 @@ controller.createNewTeam = (data, eventId, userId) => { email, telegram, discord, + slack, } = data const team = new Team({ event: eventId, @@ -64,6 +67,7 @@ controller.createNewTeam = (data, eventId, userId) => { email, telegram, discord, + slack, }) if (teamRoles && teamRoles.length > 0) { team.teamRoles = teamRoles.map(role => ({ @@ -173,8 +177,10 @@ controller.joinTeam = (eventId, userId, code) => { return controller.getTeamByCode(eventId, code).then(team => { // TODO HIGH PRIORITY team size defined in event - if (team.members.length >= 4) { - throw new ForbiddenError('Teams can have at most 5 members') + if (team.members.length + 1 >= maxTeamSize) { + throw new ForbiddenError( + `Teams can have at most ${maxTeamSize} members`, + ) } return controller .getTeamsForEvent(eventId) @@ -223,13 +229,14 @@ controller.joinTeam = (eventId, userId, code) => { } //TODO: optimize this process, slow with over 200 teams controller.acceptCandidateToTeam = (eventId, userId, code, candidateId) => { - let teamToReturn return controller .getTeamByCode(eventId, code) .then(team => { - if (team.members.length >= 4) { - throw new ForbiddenError('Teams can have at most 5 members') + if (team.members.length + 1 >= maxTeamSize) { + throw new ForbiddenError( + `Teams can have at most ${maxTeamSize} members`, + ) } if (!_.includes([team.owner].concat(team.members), userId)) { throw new InsufficientPrivilegesError( @@ -493,7 +500,7 @@ controller.attachUserApplicant = (teams, userId) => { controller.getTeamsForEvent = async (eventId, userId, page, size, filter) => { if (page && size) { - console.log("filter", filter) + console.log('filter', filter) if (filter) { const found = await Team.find({ event: eventId, @@ -507,8 +514,11 @@ controller.getTeamsForEvent = async (eventId, userId, page, size, filter) => { return controller.attachUserApplicant(teams, userId) } }) - const count = await Team.find({ event: eventId, challenge: filter }).countDocuments() - console.log("with filter", { data: found, count: count }) + const count = await Team.find({ + event: eventId, + challenge: filter, + }).countDocuments() + console.log('with filter', { data: found, count: count }) return { data: found, count: count } } else { const found = await Team.find({ @@ -537,14 +547,13 @@ controller.getTeamsForEvent = async (eventId, userId, page, size, filter) => { return teams }) const count = await Team.find({ event: eventId }).countDocuments() - console.log("getting all teams", count) + console.log('getting all teams', count) return { data: found, count: count } } // TODO make the code not visible to participants on Redux store } controller.getAllTeamsForEvent = async (eventId, userId, page, size) => { - return await Team.find({ event: eventId, }) @@ -590,26 +599,31 @@ controller.convertToFlatExportData = teamWithMeta => { } } -controller.organiserRemoveMemberFromTeam = (eventId, teamCode, userToRemove) => { - console.log("removing ", eventId, teamCode, userToRemove) +controller.organiserRemoveMemberFromTeam = ( + eventId, + teamCode, + userToRemove, +) => { + console.log('removing ', eventId, teamCode, userToRemove) return controller.getTeamByCode(eventId, teamCode).then(team => { - if (team.members.length === 0 && team.owner === userToRemove) { - console.log("deleting team") + console.log('deleting team') controller.deleteTeamByCode(eventId, teamCode) } else { if (team.owner === userToRemove) { - console.log("new owner", team.members[0]) + console.log('new owner', team.members[0]) team.owner = team.members[0] team.members = team.members.slice(1) } else { - console.log("removing member") - team.members = team.members.filter(member => member !== userToRemove) + console.log('removing member') + team.members = team.members.filter( + member => member !== userToRemove, + ) } - console.log("deleted ", team.members) + console.log('deleted ', team.members) return team.save() } - console.log("deleted team", team.members) + console.log('deleted team', team.members) return team.save() }) } diff --git a/backend/modules/team/model.js b/backend/modules/team/model.js index 7bbbc4f55..2a13103eb 100644 --- a/backend/modules/team/model.js +++ b/backend/modules/team/model.js @@ -73,6 +73,9 @@ const TeamSchema = new mongoose.Schema({ discord: { type: String, }, + slack: { + type: String, + }, }) TeamSchema.pre('save', async function (next) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f3a7f408..4a915b1b3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4196,16 +4196,6 @@ "requires": { "legacy-swc-helpers": "npm:@swc/helpers@=0.4.14", "tslib": "^2.4.0" - }, - "dependencies": { - "legacy-swc-helpers": { - "version": "npm:@swc/helpers@0.4.14", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", - "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", - "requires": { - "tslib": "^2.4.0" - } - } } }, "@types/babel__core": { @@ -11960,6 +11950,14 @@ "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" }, + "legacy-swc-helpers": { + "version": "npm:@swc/helpers@0.4.14", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", + "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", + "requires": { + "tslib": "^2.4.0" + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", diff --git a/frontend/src/components/Team/TeamCreateEditForm/index.js b/frontend/src/components/Team/TeamCreateEditForm/index.js index 9e1b1dc32..71b988458 100644 --- a/frontend/src/components/Team/TeamCreateEditForm/index.js +++ b/frontend/src/components/Team/TeamCreateEditForm/index.js @@ -405,7 +405,7 @@ export default ({ {({ field, form }) => ( 0 @@ -429,6 +429,34 @@ export default ({ )} +
+ + {({ field, form }) => ( + 0 + } + error={form.errors[field.name]} + > + + form.setFieldValue( + field.name, + value, + ) + } + onBlur={() => + form.setFieldTouched(field.name) + } + placeholder="Your team's Slack" + /> + + )} + +
{({ field, form }) => ( diff --git a/frontend/src/components/Team/TeamProfile/index.js b/frontend/src/components/Team/TeamProfile/index.js index ebe6b78fb..2ec151eeb 100644 --- a/frontend/src/components/Team/TeamProfile/index.js +++ b/frontend/src/components/Team/TeamProfile/index.js @@ -18,9 +18,9 @@ export default ({ enableActions = true, teamData = {}, onClickLeave, - onClickDelete = () => { }, + onClickDelete = () => {}, onClickEdit, - onRoleClick = () => { }, + onRoleClick = () => {}, loading = false, }) => { const teamMembersArr = [...objToArr(teamData.meta)] @@ -97,6 +97,19 @@ export default ({ )} + {teamData?.slack && ( + + popupCenter({ + url: teamData.slack, + title: 'Slack', + }) + } + className={classes.socialIcon} + size="2x" + /> + )}
{/* TODO add socialLinks component from Damilare (@mrprotocoll) */} {enableActions && ( diff --git a/frontend/src/components/cards/CandidateCard.js b/frontend/src/components/cards/CandidateCard.js index d91f0a341..ec21b7542 100644 --- a/frontend/src/components/cards/CandidateCard.js +++ b/frontend/src/components/cards/CandidateCard.js @@ -125,8 +125,9 @@ function CandidateCard({ candidateData = {}, onViewApplication = () => {} }) { onChange={formik.handleChange} className="tw-flex tw-flex-col tw-gap-4" > - {rolesToRender.map(role => ( + {rolesToRender.map((role, index) => ( { if (!EventHelpers.isEventOver(event, moment)) return null if (registration?.status !== RegistrationStatuses.asObject.checkedIn.id) return null - if (event.slug == "junction-2023" || ('certificate' in event && event.certificate.url !== '')) {//TODO: fix certificate upload + if ('certificate' in event && event.certificate.url !== '') { + //TODO: fix certificate upload return ( @@ -44,11 +45,17 @@ export default () => { ) } diff --git a/frontend/src/pages/_dashboard/renderDashboard/organiser/edit/other/index.js b/frontend/src/pages/_dashboard/renderDashboard/organiser/edit/other/index.js index 1c180f03f..68fd34163 100644 --- a/frontend/src/pages/_dashboard/renderDashboard/organiser/edit/other/index.js +++ b/frontend/src/pages/_dashboard/renderDashboard/organiser/edit/other/index.js @@ -10,13 +10,12 @@ import MetaTagsForm from './MetaTagsForm' import CertificateForm from './CertificateForm' import PageScriptsForm from './PageScriptsForm' import FileInput from '../submission/components/inputs/FileInput' -import PdfUpload from 'components/inputs/PdfUpload' +// import PdfUpload from 'components/inputs/PdfUpload' import Switch from 'components/generic/Switch' import { hasSuperAdmin } from 'redux/auth/selectors' export default () => { const isSuperAdmin = useSelector(hasSuperAdmin) - console.log('isSuperAdmin', isSuperAdmin) return ( diff --git a/frontend/src/pages/_dashboard/renderDashboard/organiser/index.js b/frontend/src/pages/_dashboard/renderDashboard/organiser/index.js index f3e0b4ec5..9abaee777 100644 --- a/frontend/src/pages/_dashboard/renderDashboard/organiser/index.js +++ b/frontend/src/pages/_dashboard/renderDashboard/organiser/index.js @@ -28,7 +28,7 @@ import ManagePage from './manage' import ParticipantsPage from './participants' import ProjectsPage from './projects' import ResultsPage from './results' -import StatsPage from './stats' +// import StatsPage from './stats' import TravelGrantsPage from './travel-grants' import AlertsPage from './alerts' import { QuestionAnswerSharp } from '@material-ui/icons' @@ -128,14 +128,14 @@ export default () => { label: 'Edit', component: EditPage, }, - { - key: 'stats', - path: '/stats', - exact: true, - icon: , - label: 'Stats', - component: StatsPage, - }, + // { + // key: 'stats', + // path: '/stats', + // exact: true, + // icon: , + // label: 'Stats', + // component: StatsPage, + // }, { key: 'participants', path: '/participants', diff --git a/frontend/src/pages/_dashboard/renderDashboard/organiser/participants/teams/index.js b/frontend/src/pages/_dashboard/renderDashboard/organiser/participants/teams/index.js index 0b989e9e9..9f0e7c53a 100644 --- a/frontend/src/pages/_dashboard/renderDashboard/organiser/participants/teams/index.js +++ b/frontend/src/pages/_dashboard/renderDashboard/organiser/participants/teams/index.js @@ -6,16 +6,59 @@ import * as OrganiserSelectors from 'redux/organiser/selectors' import PageWrapper from 'components/layouts/PageWrapper' import TeamsTable from 'components/tables/TeamsTable' +import { List, ListItem, ListItemText, Typography } from '@material-ui/core' export default () => { + const event = useSelector(OrganiserSelectors.event) const teams = useSelector(OrganiserSelectors.teams) const registrationsLoading = useSelector( OrganiserSelectors.registrationsLoading, ) const teamsLoading = useSelector(OrganiserSelectors.teamsLoading) + const challengeList = event?.challenges || [] + + if (challengeList.length > 0) { + challengeList.forEach(challenge => { + challenge.teamCount = 0 + }) + } + + if (teams.length > 0) { + teams.map(team => { + challengeList.find(challenge => { + if (challenge._id === team.challenge) { + challenge.teamCount += 1 + } + }) + }) + } return ( + {challengeList.length > 0 && ( + <> + + Breakdown of teams per challenge + + + {challengeList.map(challenge => ( + + 1 + ? 'teams' + : 'team' + }`} + /> + + ))} + + + )} { const match = useRouteMatch() const location = useLocation() + const hasTeam = useSelector(DashboardSelectors.hasTeam) + const enabledTabs = [ + { + label: 'All teams', + key: 'teams', + path: '', + component: TeamsPage, + }, + { + label: 'My team', + key: 'profile', + path: '/profile', + component: ProfilePage, + }, + ] + if (hasTeam) { + enabledTabs.push({ + label: 'Team candidates', + key: 'candidates', + path: '/candidates', + component: CandidatesPage, + }) + } // const hasTeam = useSelector(DashboardSelectors.hasTeam) // TODO make tab "my team" and "Team candidates" visible only if user has a team return ( @@ -18,26 +43,7 @@ export default () => { diff --git a/frontend/src/utils/modifyPdf.js b/frontend/src/utils/modifyPdf.js index a1603c5bc..114a5fe7d 100644 --- a/frontend/src/utils/modifyPdf.js +++ b/frontend/src/utils/modifyPdf.js @@ -2,8 +2,46 @@ import { PDFDocument, rgb, StandardFonts } from 'pdf-lib' import download from 'downloadjs' // TODO: This is hardcoded at the moment for Junction2021 certificate. Make this modular for all certificates +const colorList = [ + { white: rgb(0.95, 0.95, 0.95) }, + { black: rgb(0.01, 0.01, 0.01) }, + { red: rgb(1, 0, 0) }, + { green: rgb(0, 1, 0) }, + { blue: rgb(0, 0, 1) }, +] + +const modifyPdf = async ( + url, + x, + y, + name, + slug, + color = 'white', + enableRegistrationId, + registrationIdX, + registrationIdY, + registrationIdColor, + registrationId, +) => { + const colorConvertToRgb = color => { + switch (color) { + case 'white': + return rgb(0.95, 0.95, 0.95) + case 'black': + return rgb(0.01, 0.01, 0.01) + case 'red': + return rgb(1, 0, 0) + case 'green': + return rgb(0, 1, 0) + case 'blue': + return rgb(0, 0, 1) + default: + return rgb(0.95, 0.95, 0.95) + } + } + + const participantNameColor = colorConvertToRgb(color) -const modifyPdf = async (url, x, y, name, slug, color) => { const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer()) const pdfDoc = await PDFDocument.load(existingPdfBytes) @@ -12,36 +50,35 @@ const modifyPdf = async (url, x, y, name, slug, color) => { const page = pages[0] const text = name - const textSize = 20 - //const textHeight = font.heightAtSize(textSize) + const participantNamesize = 20 + const participantNameTextWidth = font.widthOfTextAtSize( + text, + participantNamesize, + ) - const textWidth = font.widthOfTextAtSize(text, textSize) - // align text center page.drawText(text, { - x: 100 + 200 - textWidth / 2, - y: 400, - size: textSize, + x: x - participantNameTextWidth / 2, + y: y, + size: participantNamesize, font: font, - align: 'center', - color: rgb(0.95, 0.95, 0.95), + color: participantNameColor, }) - // page.drawRectangle({ - // x: 100, - // y: 475, - // width: 400, - // height: textHeight, - - // borderWidth: 1.5, - // }) - - // firstPage.drawText(name, { - // x: boxX + boxWidth - textWidth, - // y: boxY, - // size: textSize, - // font: helveticaFont, - - // color: rgb(0.95, 0.95, 0.95), - // }) + + if (enableRegistrationId) { + const registrationIdTextSize = 10 + const registrationIdColorRgb = colorConvertToRgb(registrationIdColor) + const registrationIdTextWidth = font.widthOfTextAtSize( + registrationId, + registrationIdTextSize, + ) + page.drawText(registrationId, { + x: registrationIdX - registrationIdTextWidth / 2, + y: registrationIdY, + size: registrationIdTextSize, + font: font, + color: registrationIdColorRgb, + }) + } const pdfBytes = await pdfDoc.save() download(pdfBytes, `${name}-${slug}-certificate`, 'application/pdf') diff --git a/shared/schemas/Certificate.js b/shared/schemas/Certificate.js index f762b0870..519f37cc3 100644 --- a/shared/schemas/Certificate.js +++ b/shared/schemas/Certificate.js @@ -1,3 +1,11 @@ +const { + GraphQLInt, + GraphQLNonNull, + GraphQLString, + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLBoolean, +} = require('graphql') const mongoose = require('mongoose') const CertificateSchema = new mongoose.Schema({ @@ -13,8 +21,94 @@ const CertificateSchema = new mongoose.Schema({ y: { type: Number, }, + color: { + type: String, + }, + enableRegistrationId: { + type: Boolean, + }, + registrationIdX: { + type: Number, + }, + registrationIdY: { + type: Number, + }, + registrationIdColor: { + type: String, + }, +}) + +const CertificateType = new GraphQLObjectType({ + name: 'Certificate', + fields: { + url: { + type: GraphQLNonNull(GraphQLString), + }, + publicId: { + type: GraphQLNonNull(GraphQLString), + }, + x: { + type: GraphQLInt, + }, + y: { + type: GraphQLInt, + }, + color: { + type: GraphQLString, + }, + enableRegistrationId: { + type: GraphQLBoolean, + }, + registrationIdX: { + type: GraphQLInt, + }, + registrationIdY: { + type: GraphQLInt, + }, + registrationIdColor: { + type: GraphQLString, + }, + }, +}) + +const CertificateInput = new GraphQLInputObjectType({ + name: 'CertificateInput', + fields: { + _id: { + type: GraphQLString, + }, + url: { + type: GraphQLNonNull(GraphQLString), + }, + publicId: { + type: GraphQLNonNull(GraphQLString), + }, + x: { + type: GraphQLInt, + }, + y: { + type: GraphQLInt, + }, + color: { + type: GraphQLString, + }, + enableRegistrationId: { + type: GraphQLBoolean, + }, + registrationIdX: { + type: GraphQLInt, + }, + registrationIdY: { + type: GraphQLInt, + }, + registrationIdColor: { + type: GraphQLString, + }, + }, }) module.exports = { mongoose: CertificateSchema, + graphql: CertificateType, + graphqlInput: CertificateInput, } diff --git a/shared/schemas/index.js b/shared/schemas/index.js index 4b5b1c69f..8b722dfbe 100644 --- a/shared/schemas/index.js +++ b/shared/schemas/index.js @@ -35,6 +35,7 @@ const MeetingRoom = require('./MeetingRoom') const EventPageScript = require('./EventPageScript') const SubmissionDefaultFields = require('./SubmissionDefaultFields') const ScoreCriteriaSettings = require('./ScoreCriteriaSettings') +const Certificate = require('./Certificate') // const GraphQLSchema = makeExecutableSchema const SharedSchema = new GraphQLSchema({ @@ -96,6 +97,8 @@ const SharedSchema = new GraphQLSchema({ EventPageScript.graphqlInput, ScoreCriteriaSettings.graphql, ScoreCriteriaSettings.graphqlInput, + Certificate.graphql, + Certificate.graphqlInput, ], })