From 75153c75bda1f28cee59994f4575615f257a1489 Mon Sep 17 00:00:00 2001 From: Ismael Dosil Date: Wed, 25 Mar 2026 17:14:07 -0300 Subject: [PATCH] feat(help): implement help request button with email support Users can now submit help requests from the homepage, leader dashboard, and burger menu. Messages are sent via SendGrid to contact@chalkcoaching.com with user info (name, email, role). Shows confirmation after submission. Closes CHALK-097 --- .gitignore | 1 + functions/funcSendHelpRequest/index.js | 61 +++++++ functions/index.js | 1 + src/components/BurgerMenu.tsx | 19 ++- src/components/Firebase/Firebase.tsx | 15 ++ src/components/Shared/HelpRequestDialog.tsx | 159 ++++++++++++++++++ src/views/protected/HomeViews/HomePage.tsx | 15 +- .../LeadersViews/LeadersDashboard.tsx | 8 + 8 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 functions/funcSendHelpRequest/index.js create mode 100644 src/components/Shared/HelpRequestDialog.tsx diff --git a/.gitignore b/.gitignore index 37474144b..224532edc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ yarn-error.log* .idea/ src/SPREADSHEET_SECRETS.js .firebase/ +functions/.runtimeconfig.json public/precache-manifest.* # Project management (internal) diff --git a/functions/funcSendHelpRequest/index.js b/functions/funcSendHelpRequest/index.js new file mode 100644 index 000000000..328d79a13 --- /dev/null +++ b/functions/funcSendHelpRequest/index.js @@ -0,0 +1,61 @@ +const sgMail = require("@sendgrid/mail"); +const functions = require("firebase-functions"); +const {getUser} = require("../common/accessUtils"); + +sgMail.setApiKey(functions.config().sendgrid ? functions.config().sendgrid.key : ""); + +const HELP_RECIPIENT = "contact@chalkcoaching.com"; +const SENDER_ADDRESS = "chalkcoaching@gmail.com"; + +exports.funcSendHelpRequest = functions.https.onCall(async (data, context) => { + if (!context.auth) { + throw new functions.https.HttpsError( + "unauthenticated", + "User must be logged in to submit a help request." + ); + } + + const {message} = data; + if (!message || message.trim().length === 0) { + throw new functions.https.HttpsError( + "invalid-argument", + "Message is required." + ); + } + + const userData = await getUser(context.auth.uid); + const userName = `${userData.firstName} ${userData.lastName}`; + const userEmail = userData.email; + const userRole = userData.role; + + const emailMessage = { + to: HELP_RECIPIENT, + replyTo: userEmail, + from: SENDER_ADDRESS, + subject: `CHALK Help Request from ${userName}`, + text: [ + `Help request from: ${userName}`, + `Email: ${userEmail}`, + `Role: ${userRole}`, + ``, + `Message:`, + message, + ``, + `---`, + `Sent from CHALK Coaching` + ].join("\n") + }; + + return sgMail.send(emailMessage) + .then(() => { + console.log("Help request email sent"); + return {success: true}; + }) + .catch((err) => { + console.error("Error sending help request:", JSON.stringify(err)); + throw new functions.https.HttpsError( + "internal", + "Failed to send help request. Please try again." + ); + }); +}); diff --git a/functions/index.js b/functions/index.js index 2d90aca12..53602ec88 100644 --- a/functions/index.js +++ b/functions/index.js @@ -79,6 +79,7 @@ exports.funcLiteracyTrendLanguage = require('./funcLiteracyTrendLanguage').funcL exports.funcMathDetails = require('./funcMathDetails').funcMathDetails; exports.funcRecentObservations = require('./funcRecentObservations').funcRecentObservations; exports.funcSendEmail = require('./funcSendEmail').funcSendEmail; +exports.funcSendHelpRequest = require('./funcSendHelpRequest').funcSendHelpRequest; exports.funcSendMLE = require('./funcSendMLE').funcSendMLE; exports.funcSeqDetails = require('./funcSeqDetails').funcSeqDetails; exports.funcSessionDates = require('./funcSessionDates').funcSessionDates; diff --git a/src/components/BurgerMenu.tsx b/src/components/BurgerMenu.tsx index cd06495af..4593d9f0c 100644 --- a/src/components/BurgerMenu.tsx +++ b/src/components/BurgerMenu.tsx @@ -36,6 +36,7 @@ import ReactRouterPropTypes from 'react-router-prop-types'; import * as Types from '../constants/Types'; import * as H from 'history'; import Firebase from './Firebase'; +import HelpRequestDialog from './Shared/HelpRequestDialog'; import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'; const styles: object = { @@ -75,7 +76,8 @@ interface State { chalkOpen: boolean, teacherModal: boolean, practiceOpen: boolean, - type: string + type: string, + helpDialogOpen: boolean } /** @@ -91,7 +93,8 @@ class BurgerMenu extends React.Component{ chalkOpen: false, teacherModal: false, practiceOpen: false, - type: "" + type: "", + helpDialogOpen: false }; handleDrawerOpen = (): void => { this.setState({ open: true }); @@ -459,11 +462,9 @@ class BurgerMenu extends React.Component{ this.props.handleNavigation( (): void => { - this.setState({ menu: 13, chalkOpen: false }); - this.props.history.push("/help"); - })} + onClick={(): void => { + this.setState({ helpDialogOpen: true }); + }} > @@ -536,6 +537,10 @@ class BurgerMenu extends React.Component{ ) : (
)} + this.setState({ helpDialogOpen: false })} + />
); } diff --git a/src/components/Firebase/Firebase.tsx b/src/components/Firebase/Firebase.tsx index db6072984..9e6357dcf 100644 --- a/src/components/Firebase/Firebase.tsx +++ b/src/components/Firebase/Firebase.tsx @@ -413,6 +413,21 @@ class Firebase { .catch(error => error) } + sendHelpRequest = async (message: string): Promise<{success: boolean}> => { + const sendHelpRequestFunction = this.functions.httpsCallable( + 'funcSendHelpRequest' + ) + return sendHelpRequestFunction({message}) + .then(result => { + console.log('Help request sent:', result) + return {success: true} + }) + .catch(error => { + console.error('Error sending help request:', error) + throw error + }) + } + /** * gets list of all teachers linked to current user's account */ diff --git a/src/components/Shared/HelpRequestDialog.tsx b/src/components/Shared/HelpRequestDialog.tsx new file mode 100644 index 000000000..88ab2ea35 --- /dev/null +++ b/src/components/Shared/HelpRequestDialog.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { TextField, CircularProgress } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import CheckCircleIcon from '@material-ui/icons/CheckCircle'; +import Firebase, { FirebaseContext } from '../Firebase'; + +interface Props { + open: boolean, + handleClose(): void, +} + +interface State { + message: string, + sending: boolean, + sent: boolean, + error: string +} + +class HelpRequestDialog extends React.Component { + static contextType = FirebaseContext; + context!: Firebase; + + constructor(props: Props) { + super(props); + this.state = { + message: '', + sending: false, + sent: false, + error: '' + }; + } + + static propTypes = { + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired + } + + handleSubmit = async (): Promise => { + const { message } = this.state; + if (!message.trim()) { + this.setState({ error: 'Please enter a message.' }); + return; + } + + this.setState({ sending: true, error: '' }); + + try { + await this.context.sendHelpRequest(message.trim()); + this.setState({ sending: false, sent: true }); + } catch (error) { + console.error('Error sending help request:', error); + this.setState({ + sending: false, + error: 'There was a problem sending your request. Please try again.' + }); + } + } + + handleClose = (): void => { + this.setState({ + message: '', + sending: false, + sent: false, + error: '' + }); + this.props.handleClose(); + } + + render(): React.ReactNode { + const { open } = this.props; + const { message, sending, sent, error } = this.state; + + return ( + + {sent ? ( + <> + + + + Message Received! + + + Thank you for reaching out. A member of our team will respond within 2 business days. + + + + + + + ) : ( + <> + + Help & Support + + + + How can we help? Describe your question or issue below and our team will get back to you within 2 business days. + + this.setState({ message: e.target.value, error: '' })} + error={!!error} + helperText={error} + disabled={sending} + inputProps={{ style: { fontFamily: 'Arimo' } }} + InputLabelProps={{ style: { fontFamily: 'Arimo' } }} + /> + + + + + + + )} + + ); + } +} + +export default HelpRequestDialog; diff --git a/src/views/protected/HomeViews/HomePage.tsx b/src/views/protected/HomeViews/HomePage.tsx index 106f9186f..867905342 100644 --- a/src/views/protected/HomeViews/HomePage.tsx +++ b/src/views/protected/HomeViews/HomePage.tsx @@ -23,6 +23,7 @@ import * as Types from '../../../constants/Types'; import ReactRouterPropTypes from 'react-router-prop-types'; import * as H from 'history'; import Firebase from '../../../components/Firebase' +import HelpRequestDialog from '../../../components/Shared/HelpRequestDialog' const styles: object = { root: { @@ -99,7 +100,8 @@ interface Props { interface State { teacherModal: boolean, type: string, - coachName: string + coachName: string, + helpDialogOpen: boolean } /** @@ -116,7 +118,8 @@ class HomePage extends React.Component { this.state = { teacherModal: false, type: "", - coachName: "" + coachName: "", + helpDialogOpen: false } } @@ -546,7 +549,9 @@ class HomePage extends React.Component { - @@ -581,6 +586,10 @@ class HomePage extends React.Component { ) : (
)} + this.setState({ helpDialogOpen: false })} + />
); } diff --git a/src/views/protected/LeadersViews/LeadersDashboard.tsx b/src/views/protected/LeadersViews/LeadersDashboard.tsx index abded544b..750499cf3 100644 --- a/src/views/protected/LeadersViews/LeadersDashboard.tsx +++ b/src/views/protected/LeadersViews/LeadersDashboard.tsx @@ -24,6 +24,7 @@ import * as Types from '../../../constants/Types' import ReactRouterPropTypes from 'react-router-prop-types' import * as H from 'history' import Firebase from '../../../components/Firebase' +import HelpRequestDialog from '../../../components/Shared/HelpRequestDialog' import UsersIcon from '../../../assets/icons/UsersIcon.png' import ReportsIcon from '../../../assets/icons/ReportsIcon.png' @@ -112,6 +113,7 @@ interface State { teacherModal: boolean type: string coachName: string + helpDialogOpen: boolean } /** @@ -129,6 +131,7 @@ class LeadersDashboard extends React.Component { teacherModal: false, type: '', coachName: '', + helpDialogOpen: false, } } @@ -704,6 +707,7 @@ class LeadersDashboard extends React.Component { color="primary" className={classes.helpButtons} style={{ paddingLeft: '2em' }} + onClick={(): void => this.setState({ helpDialogOpen: true })} > HELP @@ -739,6 +743,10 @@ class LeadersDashboard extends React.Component { ) : (
)} + this.setState({ helpDialogOpen: false })} + />
) }