diff --git a/package-lock.json b/package-lock.json index 5732850..d3141ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "textsender", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "textsender", - "version": "0.0.5", + "version": "0.0.6", "dependencies": { "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/package.json b/package.json index 05a64f1..bd1e976 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "textsender", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "scripts": { "dev": "vite", diff --git a/src/api/scheduleApi.js b/src/api/scheduleApi.js new file mode 100644 index 0000000..dd6630e --- /dev/null +++ b/src/api/scheduleApi.js @@ -0,0 +1,44 @@ +import { API_BASE_URL } from '../constants/api'; + +export const scheduleApi = { + scheduleMessage: async (reqBody, authBearerToken) => { + console.log('Scheduling message'); + + const response = await fetch(`${API_BASE_URL}/api/v1/schedule/message`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: authBearerToken, + }, + body: JSON.stringify(reqBody), + }); + + if (!response.ok) { + throw new Error('Error scheduling message'); + } else { + return response.json(); + } + }, + + prepareMessage: async (reqBody, authBearerToken) => { + console.log('Preparing Scheduled message'); + + const response = await fetch( + `${API_BASE_URL}/api/v1/schedule/message/status/update`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: authBearerToken, + }, + body: JSON.stringify(reqBody), + } + ); + + if (!response.ok) { + throw new Error('Error updating status of scheduled message'); + } else { + return response.json(); + } + }, +}; diff --git a/src/api/scheduleEventApi.js b/src/api/scheduleEventApi.js new file mode 100644 index 0000000..662f6b0 --- /dev/null +++ b/src/api/scheduleEventApi.js @@ -0,0 +1,25 @@ +import { API_BASE_URL } from '../constants/api'; + +export const scheduleEventApi = { + scheduleMessageEvent: async (reqBody, authBearerToken) => { + console.log('Scheduling message Event'); + + const response = await fetch( + `${API_BASE_URL}/api/v1/schedule/message/event`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: authBearerToken, + }, + body: JSON.stringify(reqBody), + } + ); + + if (!response.ok) { + throw new Error('Error scheduling message event'); + } else { + return response.json(); + } + }, +}; diff --git a/src/components/SendMessageWizard/ConfirmationStep.jsx b/src/components/SendMessageWizard/ConfirmationStep.jsx index 5991d7b..6b6873b 100644 --- a/src/components/SendMessageWizard/ConfirmationStep.jsx +++ b/src/components/SendMessageWizard/ConfirmationStep.jsx @@ -5,6 +5,7 @@ function ConfirmationStep({ selectedContacts, createdMessage, message, + scheduledTime, isSubmitting, }) { // In a real app, you might fetch full contact details here @@ -88,6 +89,12 @@ function ConfirmationStep({ + {/* Scheduled time Section */} +
+ Scheduled time:{' '} + {scheduledTime ? scheduledTime.toISOString() : 'No schedule date'} +
+ {/* Summary Section */}
diff --git a/src/components/SendMessageWizard/FutureTimeForm.jsx b/src/components/SendMessageWizard/FutureTimeForm.jsx new file mode 100644 index 0000000..914ec90 --- /dev/null +++ b/src/components/SendMessageWizard/FutureTimeForm.jsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; + +function FutureTimeForm({ scheduledTime, setScheduledTime }) { + const [selectedTime, setSelectedTime] = useState(new Date()); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const handleTimeChange = (e) => { + setSelectedTime(e.target.value); + setScheduledTime(new Date(e.target.value)); + setError(''); + setSuccess(''); + }; + + const validateAndSubmit = (e) => { + e.preventDefault(); + + if (!selectedTime) { + setError('Please select a date and time.'); + return; + } + + const now = new Date(); + const inputDate = new Date(selectedTime); + + // Calculate the minimum allowed time (Now + 10 minutes) + const minAllowedTime = new Date(now.getTime() + 10 * 60000); + + if (inputDate < minAllowedTime) { + setError('The time must be at least 10 minutes in the future.'); + setSuccess(''); + setScheduledTime(null); + } else { + setError(''); + setSuccess('Time validated successfully!'); + setScheduledTime(inputDate); + } + }; + + return ( +
+

Schedule Event

+
+
+ + +
+ + {error &&

{error}

} + {success && ( +

{success}

+ )} + + +
+
+ ); +} + +export default FutureTimeForm; diff --git a/src/components/SendMessageWizard/MessageForm.jsx b/src/components/SendMessageWizard/MessageForm.jsx index 8e81591..da69864 100644 --- a/src/components/SendMessageWizard/MessageForm.jsx +++ b/src/components/SendMessageWizard/MessageForm.jsx @@ -3,9 +3,15 @@ import styles from './SendMessageWizard.module.css'; import { tokenService } from '../../services/TokenService'; import { messageApi, CreateMessageRequest } from '../../api/messageApi'; -function MessageForm({ message, onMessageChange, onMessageCreated }) { +function MessageForm({ + message, + onMessageChange, + onMessageCreated, + onToggleScheduled, +}) { const [characterCount, setCharacterCount] = useState(message.length); const [isCreatingMessage, setIsCreatingMessage] = useState(false); + const [isScheduledChecked, setIsScheduledChecked] = useState(false); const [createdMessage, setCreatedMessage] = useState(null); const handleMessageChange = (e) => { @@ -14,6 +20,11 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) { setCharacterCount(newMessage.length); }; + const handleIsScheduledCheckbox = (event) => { + setIsScheduledChecked(event.target.checked); + onToggleScheduled(event.target.checked); + }; + const createMessage = async () => { if (!message.trim()) { return; @@ -38,10 +49,8 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) { const createdMessage = response.data[0]; setCreatedMessage(createdMessage); if (onMessageCreated) { - console.log('Setting createdMessage'); onMessageCreated(createdMessage); } - console.log('Created message: ', createdMessage); } } catch (error) { console.error(error); @@ -97,6 +106,17 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
  • Check spelling and grammar
  • +
    + +
    ); } diff --git a/src/components/SendMessageWizard/SendMessageWizardModal.jsx b/src/components/SendMessageWizard/SendMessageWizardModal.jsx index 01551e1..2a0bddb 100644 --- a/src/components/SendMessageWizard/SendMessageWizardModal.jsx +++ b/src/components/SendMessageWizard/SendMessageWizardModal.jsx @@ -1,5 +1,6 @@ import React from 'react'; import ContactSelection from './ContactSelection'; +import FutureTimeForm from './FutureTimeForm'; import MessageForm from './MessageForm'; import ConfirmationStep from './ConfirmationStep'; import StepNavigation from './StepNavigation'; @@ -26,6 +27,14 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) { message={wizard.message} onMessageChange={wizard.setMessage} onMessageCreated={wizard.setCreatedMessage} + onToggleScheduled={wizard.toggleScheduled} + /> + ); + case 'schedule': + return ( + ); case 'confirm': @@ -34,6 +43,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) { selectedContacts={wizard.selectedContacts} createdMessage={wizard.createdMessage} message={wizard.message} + scheduledTime={wizard.scheduledTime} isSubmitting={wizard.isSubmitting} /> ); @@ -74,6 +84,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) { const stepTitles = { select: 'Select Contacts', message: 'Compose Message', + schedule: 'Schedule message', confirm: 'Review & Send', result: 'Result', }; diff --git a/src/components/SendMessageWizard/StepNavigation.jsx b/src/components/SendMessageWizard/StepNavigation.jsx index c02efa4..d81a148 100644 --- a/src/components/SendMessageWizard/StepNavigation.jsx +++ b/src/components/SendMessageWizard/StepNavigation.jsx @@ -34,6 +34,25 @@ function StepNavigation({ ); + case 'schedule': + return ( + <> + + + + ); case 'confirm': return ( <> diff --git a/src/components/SendMessageWizard/useSendMessageWizard.js b/src/components/SendMessageWizard/useSendMessageWizard.js index a717f38..abc137a 100644 --- a/src/components/SendMessageWizard/useSendMessageWizard.js +++ b/src/components/SendMessageWizard/useSendMessageWizard.js @@ -5,14 +5,18 @@ import { sendMessageApi, SendInstantMessageRequest, } from '../../api/sendMessageApi'; +import { scheduleApi } from '../../api/scheduleApi.js'; +import { scheduleEventApi } from '../../api/scheduleEventApi.js'; const useSendMessageWizard = () => { const [state, setState] = useState({ step: 'select', selectedContacts: [], + scheduledTime: null, message: '', createdMessage: null, isSubmitting: false, + isScheduled: false, submissionError: null, }); @@ -25,12 +29,28 @@ const useSendMessageWizard = () => { return state.selectedContacts.length > 0; case 'message': return state.message.trim().length > 0; + case 'schedule': + console.log('Checking to see if schedule can go to the next step'); + if (state.scheduledTime == null) { + return false; + } + console.log('Scheduled time is not null'); + const result = + state.scheduledTime instanceof Date && + isNaN(state.scheduledTime.getTime()); + + return !result; case 'confirm': return true; default: return false; } - }, [state.step, state.selectedContacts.length, state.message]); + }, [ + state.step, + state.selectedContacts.length, + state.message, + state.scheduledTime, + ]); const canGoBack = state.step !== 'select'; @@ -44,6 +64,14 @@ const useSendMessageWizard = () => { })); }; + const toggleScheduled = (scheduled) => { + console.log('Yup'); + setState((prev) => ({ + ...prev, + isScheduled: scheduled, + })); + }; + const setMessage = (message) => { setState((prev) => ({ ...prev, message })); }; @@ -53,29 +81,67 @@ const useSendMessageWizard = () => { setState((prev) => ({ ...prev, createdMessage: message })); }; + const setScheduledTime = (selectedTime) => { + console.log('Setting scheduled time'); + setState((prev) => ({ ...prev, scheduledTime: selectedTime })); + }; + const nextStep = () => { if (!canProceedToNextStep()) return; - const stepOrder = ['select', 'message', 'confirm', 'result']; + const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result']; const currentIndex = stepOrder.indexOf(state.step); if (currentIndex < stepOrder.length - 1) { - setState((prev) => ({ - ...prev, - step: stepOrder[currentIndex + 1], - submissionError: null, - })); + if (state.step === 'message') { + if (state.isScheduled) { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex + 1], + submissionError: null, + })); + } else { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex + 2], + submissionError: null, + })); + } + } else { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex + 1], + submissionError: null, + })); + } } }; const prevStep = () => { - const stepOrder = ['select', 'message', 'confirm', 'result']; + const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result']; const currentIndex = stepOrder.indexOf(state.step); + if (currentIndex > 0) { - setState((prev) => ({ - ...prev, - step: stepOrder[currentIndex - 1], - submissionError: null, - })); + if (state.step === 'confirm') { + if (state.isScheduled) { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex - 1], + submissionError: null, + })); + } else { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex - 2], + submissionError: null, + })); + } + } else { + setState((prev) => ({ + ...prev, + step: stepOrder[currentIndex - 1], + submissionError: null, + })); + } } }; @@ -91,25 +157,77 @@ const useSendMessageWizard = () => { })); try { - console.log('Sending message'); - let reqBod = new SendInstantMessageRequest(); - reqBod.user_id = tokenService.getUserId(); - reqBod.message_id = state.createdMessage.id; - state.selectedContacts.forEach((selectedContact) => { - reqBod.contact_ids.push(selectedContact); - }); - const response = await sendMessageApi.sendInstantMessage( - reqBod, - tokenService.bearerToken() - ); + if (state.scheduledTime != null) { + console.log('Going to schedule message'); - if (response) { - // Success - move to result step - setState((prev) => ({ - ...prev, - step: 'result', - isSubmitting: false, - })); + const schMsgReq = { + scheduled: state.scheduledTime.toISOString(), + status: 'PENDING', + user_id: tokenService.getUserId(), + }; + + const token = tokenService.bearerToken(); + const schMsgResponse = await scheduleApi.scheduleMessage( + schMsgReq, + token + ); + + if (schMsgResponse) { + console.log('Message scheduled'); + + const scheduledMessageId = schMsgResponse.data[0].id; + for (let contact of state.selectedContacts) { + let schMsgEvtReq = { + contact_id: contact, + message_id: state.createdMessage.id, + scheduled_message_id: scheduledMessageId, + }; + + const schMsgEvtResponse = + await scheduleEventApi.scheduleMessageEvent(schMsgEvtReq, token); + } + + console.log('Updating message status'); + const req = { + scheduled_message_id: scheduledMessageId, + status: 'READY', + }; + console.log(req); + const response = await scheduleApi.prepareMessage(req, token); + if (response) { + console.log('Message scheduled'); + setState((prev) => ({ + ...prev, + step: 'result', + isSubmitting: false, + isScheduled: false, + scheduledTime: null, + })); + } + } + } else { + console.log('Sending message'); + let reqBod = new SendInstantMessageRequest(); + reqBod.user_id = tokenService.getUserId(); + reqBod.message_id = state.createdMessage.id; + state.selectedContacts.forEach((selectedContact) => { + reqBod.contact_ids.push(selectedContact); + }); + const response = await sendMessageApi.sendInstantMessage( + reqBod, + tokenService.bearerToken() + ); + + if (response) { + // Success - move to result step + setState((prev) => ({ + ...prev, + step: 'result', + isSubmitting: false, + isScheduled: false, + scheduledTime: null, + })); + } } } catch (error) { if (error.name === 'AbortError') { @@ -121,6 +239,8 @@ const useSendMessageWizard = () => { ...prev, submissionError: error.message || 'Failed to send messages', isSubmitting: false, + isScheduled: false, + scheduledTime: null, })); } }; @@ -130,9 +250,11 @@ const useSendMessageWizard = () => { setState({ step: 'select', selectedContacts: [], + scheduledTime: null, message: '', createdMessage: null, isSubmitting: false, + isScheduled: false, submissionError: null, }); }; @@ -146,17 +268,21 @@ const useSendMessageWizard = () => { // Getters currentStep: state.step, selectedContacts: state.selectedContacts, + scheduledTime: state.scheduledTime, message: state.message, createdMessage: state.createdMessage, isSubmitting: state.isSubmitting, + isScheduled: state.isScheduled, submissionError: state.submissionError, canProceedToNextStep: canProceedToNextStep(), canGoBack, // Actions toggleContact, + toggleScheduled, setMessage, setCreatedMessage, + setScheduledTime, nextStep, prevStep, submit,