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 (
+
+ );
+}
+
+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,