8 Commits
Author SHA1 Message Date
phoenix 5b979c2890 Adding license (#20)
Reviewed-on: #20
2026-07-14 15:40:32 -04:00
phoenix 5c498310fb tsk-18: Updating name (#19)
Closes #18

Reviewed-on: phoenix/textsender#19
2026-07-13 18:17:09 -04:00
phoenix 291cd3c2b1 tsk-10: View scheduled messages (#17)
Closes #10

Reviewed-on: phoenix/textsender#17
2026-07-04 19:39:12 -04:00
phoenix 07f9db2298 tsk-2: Blank screen bug fix (#16)
Closes #2

Reviewed-on: phoenix/textsender#16
2026-07-04 16:56:35 -04:00
phoenix 7a357f6108 tsk-11: Make URLs more dynamic (#15)
Closes #11

Reviewed-on: phoenix/textsender#15
2026-07-04 16:25:45 -04:00
phoenix 53b80e3d2c tsk-4: Fix contacts not being saved (#14)
Closes #4

Reviewed-on: phoenix/textsender#14
2026-07-04 10:53:33 -04:00
phoenix cde21b09c8 tsk-6: Add feature to update password in profile (#13)
Closes #6

Reviewed-on: phoenix/textsender#13
2026-07-02 23:24:31 -04:00
phoenix be1238e24d tsk-5: Add profile page (#12)
Closes #5

Reviewed-on: phoenix/textsender#12
2026-07-01 23:56:22 -04:00
27 changed files with 826 additions and 306 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_AUTH_URL="http://localhost:9080"
VITE_API_URL="http://localhost:9081"
+2
View File
@@ -0,0 +1,2 @@
VITE_AUTH_URL="http://localhost:9080"
VITE_API_URL="http://localhost:9081"
+4
View File
@@ -22,3 +22,7 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Environment
.env.production
.env.development
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+2 -11
View File
@@ -1,15 +1,6 @@
# React + Vite # schedtxt
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Frontend web app used to send messages
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
# Getting started # Getting started
+1 -2
View File
@@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title> <title>schedtxt</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+17 -4
View File
@@ -1,13 +1,14 @@
{ {
"name": "textsender", "name": "schedtxt",
"version": "0.0.6", "version": "0.0.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "textsender", "name": "schedtxt",
"version": "0.0.6", "version": "0.0.11",
"dependencies": { "dependencies": {
"dotenv": "^17.4.2",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-router-dom": "^7.18.0" "react-router-dom": "^7.18.0"
@@ -1219,6 +1220,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.380", "version": "1.5.380",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz",
+3 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "textsender", "name": "schedtxt",
"private": true, "private": true,
"version": "0.0.6", "version": "0.0.11",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -12,6 +12,7 @@
"format:check": "npx prettier --check ." "format:check": "npx prettier --check ."
}, },
"dependencies": { "dependencies": {
"dotenv": "^17.4.2",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-router-dom": "^7.18.0" "react-router-dom": "^7.18.0"
View File
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+2
View File
@@ -8,6 +8,7 @@ import {
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import Login from './components/Login'; import Login from './components/Login';
import Register from './components/Register'; import Register from './components/Register';
import ProfileForm from './components/ProfileForm';
import './App.css'; import './App.css';
@@ -19,6 +20,7 @@ function App() {
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/user/profile" element={<ProfileForm />} />
</Routes> </Routes>
</Router> </Router>
); );
+6 -5
View File
@@ -1,17 +1,18 @@
import { API_BASE_URL } from '../constants/api'; import { API_BASE_URL } from '../constants/api';
export class CreateContactRequest { export class CreateContactRequest {
first_name = ''; firstname = '';
last_name = ''; lastname = '';
nickname = '';
phone_number = ''; phone_number = '';
user_id = ''; user_id = '';
} }
export class UpdateContactNamesRequest { export class UpdateContactNamesRequest {
first_name = ''; firstname = '';
last_name = ''; lastname = '';
nickname = ''; nickname = '';
contact_id = ''; id = '';
user_id = ''; user_id = '';
} }
+47
View File
@@ -20,3 +20,50 @@ export const loginApi = {
} }
}, },
}; };
export const userApi = {
get_user_profile: async (userId) => {
const reponse = await fetch(
`${AUTH_API_BASE_URL}/api/v1/user/profile/${userId}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
);
if (reponse.ok) {
return reponse.json();
} else {
throw new Error('Error logging in');
}
},
update_password: async (
userId,
currentPassword,
updatedPassword,
confirmedPassword
) => {
const request = {
user_id: userId,
current_password: currentPassword,
updated_password: updatedPassword,
confirmed_password: confirmedPassword,
};
const reponse = await fetch(
`${AUTH_API_BASE_URL}/api/v1/user/password/update`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
}
);
if (reponse.ok) {
return reponse.json();
} else {
throw new Error('Error logging in');
}
},
};
+19
View File
@@ -20,6 +20,25 @@ export const scheduleApi = {
} }
}, },
getScheduledMessage: async (userId, authBearerToken) => {
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message?user_id=${userId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
}
);
if (!response.ok) {
throw new Error('Error getting scheduled message');
} else {
return response.json();
}
},
prepareMessage: async (reqBody, authBearerToken) => { prepareMessage: async (reqBody, authBearerToken) => {
console.log('Preparing Scheduled message'); console.log('Preparing Scheduled message');
+21 -2
View File
@@ -11,6 +11,7 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
phoneNumber: '', phoneNumber: '',
firstName: '', firstName: '',
lastName: '', lastName: '',
nickName: '',
}); });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -66,8 +67,9 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
const userId = tokenService.getUserId(); const userId = tokenService.getUserId();
let contact = new CreateContactRequest(); let contact = new CreateContactRequest();
contact.phone_number = formData.phoneNumber; contact.phone_number = formData.phoneNumber;
contact.first_name = formData.firstName; contact.firstname = formData.firstName;
contact.last_name = formData.lastName; contact.lastname = formData.lastName;
contact.nickname = formData.nickName;
contact.user_id = userId; contact.user_id = userId;
const result = await contactApi.createContact( const result = await contactApi.createContact(
contact, contact,
@@ -178,6 +180,23 @@ const AddContact = ({ isOpen, onClose, onSuccess }) => {
<span className="form-hint">Optional field</span> <span className="form-hint">Optional field</span>
</div> </div>
<div className="form-group">
<label htmlFor="nickName" className="form-label">
Nick Name
</label>
<input
id="nickName"
name="nickName"
type="text"
className="form-input"
value={formData.nickName}
onChange={handleChange}
placeholder="Enter nick name (optional)"
disabled={isSubmitting}
/>
<span className="form-hint">Optional field</span>
</div>
<div className="form-actions"> <div className="form-actions">
<button <button
type="button" type="button"
+9 -20
View File
@@ -1,20 +1,22 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import AddContact from './AddContact'; import AddContact from './AddContact';
// import ViewContact from './ViewContact';
import ViewSentMessages from './ViewSentMessages'; import ViewSentMessages from './ViewSentMessages';
import './Dashboard.css'; import './Dashboard.css';
import SendMessageWizardModal from './SendMessageWizard/SendMessageWizardModal'; import SendMessageWizardModal from './SendMessageWizard/SendMessageWizardModal';
import ViewContactWizardModal from './ViewContactWizard/ViewContactWizardModal'; import ViewContactWizardModal from './ViewContactWizard/ViewContactWizardModal';
import { APP_NAME } from '../constants/app';
const Dashboard = () => { const Dashboard = () => {
const navigate = useNavigate();
const [activeView, setActiveView] = useState('dashboard'); const [activeView, setActiveView] = useState('dashboard');
const [isWizardOpen, setIsWizardOpen] = useState(false); const [isWizardOpen, setIsWizardOpen] = useState(false);
const [isViewContactWizardOpen, setIsViewContactWizardOpen] = useState(false); const [isViewContactWizardOpen, setIsViewContactWizardOpen] = useState(false);
const [isViewSentMessagesOpen, setViewSentMessagesOpen] = useState(false); const [isViewSentMessagesOpen, setViewSentMessagesOpen] = useState(false);
const [showContactForm, setShowContactForm] = useState(false); const [showContactForm, setShowContactForm] = useState(false);
// const [isPopupOpen, setIsPopupOpen] = useState(false);
const handleAddContactClick = () => { const handleAddContactClick = () => {
setShowContactForm(true); setShowContactForm(true);
@@ -22,15 +24,9 @@ const Dashboard = () => {
const handleContactSuccess = (contactData) => { const handleContactSuccess = (contactData) => {
console.log('Contact created successfully:', contactData); console.log('Contact created successfully:', contactData);
// You could:
// 1. Refresh the contacts list if you have one
// 2. Show a success notification
// 3. Update local state with the new contact
}; };
const handleWizardComplete = () => { const handleWizardComplete = () => {
// Add your logic here for each button
setIsWizardOpen(false); setIsWizardOpen(false);
}; };
@@ -47,7 +43,11 @@ const Dashboard = () => {
}; };
const handleNavClick = (view) => { const handleNavClick = (view) => {
if (view === 'profile') {
navigate('/user/profile');
} else {
setActiveView(view); setActiveView(view);
}
}; };
return ( return (
@@ -55,7 +55,7 @@ const Dashboard = () => {
{/* Navbar */} {/* Navbar */}
<nav className="dashboard-nav"> <nav className="dashboard-nav">
<div className="nav-logo"> <div className="nav-logo">
<span className="logo-text">Dashboard</span> <span className="logo-text">{APP_NAME}</span>
</div> </div>
<div className="nav-links"> <div className="nav-links">
<button <button
@@ -64,12 +64,6 @@ const Dashboard = () => {
> >
Dashboard Dashboard
</button> </button>
<button
className={`nav-link ${activeView === 'settings' ? 'active' : ''}`}
onClick={() => handleNavClick('settings')}
>
Settings
</button>
<button <button
className={`nav-link ${activeView === 'profile' ? 'active' : ''}`} className={`nav-link ${activeView === 'profile' ? 'active' : ''}`}
onClick={() => handleNavClick('profile')} onClick={() => handleNavClick('profile')}
@@ -161,11 +155,6 @@ const Dashboard = () => {
Dashboard view is active. Select one of the buttons above. Dashboard view is active. Select one of the buttons above.
</p> </p>
)} )}
{activeView === 'settings' && (
<p className="view-text">
Settings view - Configuration options will go here.
</p>
)}
{activeView === 'profile' && ( {activeView === 'profile' && (
<p className="view-text"> <p className="view-text">
Profile view - User information will go here. Profile view - User information will go here.
+95
View File
@@ -0,0 +1,95 @@
.profile-form {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
width: 100%;
max-width: 420px;
margin: 0 auto;
}
.profile-form h2 {
color: #333;
font-size: 28px;
font-weight: 600;
text-align: center;
margin-bottom: 30px;
}
.profile-form form {
margin-bottom: 25px;
}
.profile-form div {
margin-bottom: 20px;
}
.profile-form label {
display: block;
color: #555;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
}
.profile-form input[type='text'],
.profile-form input[type='tel'],
.profile-form input[type='password'] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.profile-form input[type='text']:focus,
.profile-form input[type='tel']:focus,
.profile-form input[type='password']:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.profile-form input[readonly] {
background-color: #f9fafb;
cursor: not-allowed;
}
.profile-form button[type='submit'] {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.profile-form button[type='submit']:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.profile-form button[type='button'] {
width: 100%;
background: #f1f5f9;
color: #334155;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.profile-form button[type='button']:hover {
background: #e2e8f0;
}
+252
View File
@@ -0,0 +1,252 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { tokenService } from '../services/TokenService';
import { userApi } from '../api/loginApi';
import { APP_NAME } from '../constants/app';
import './ProfileForm.css';
const ProfileForm = () => {
const navigate = useNavigate();
const [activeView, setActiveView] = useState('profile');
const [subView, setSubView] = useState('details');
const [formData, setFormData] = useState({
firstname: '',
lastname: '',
phone: '',
username: '',
createdDate: new Date().toISOString().split('T')[0],
lastLoginDate: new Date().toISOString().split('T')[0],
});
const [passwordData, setPasswordData] = useState({
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
});
useEffect(() => {
const fetchData = async () => {
const userId = tokenService.getUserId();
const response = await userApi.get_user_profile(userId);
if (response) {
const userProfile = response.data[0];
setFormData((prev) => ({
...prev,
firstname: userProfile.firstname,
lastname: userProfile.lastname,
phone: userProfile.phone_number,
username: userProfile.username,
createdDate: userProfile.created,
lastLoginDate: userProfile.last_login,
}));
} else {
console.error('Error getting user profile');
}
};
fetchData();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handlePasswordChange = (e) => {
const { name, value } = e.target;
setPasswordData((prev) => ({
...prev,
[name]: value,
}));
};
const handleNavClick = (view) => {
if (view === 'profile') {
navigate('/user/profile');
} else {
navigate(`/${view}`);
}
};
const handlePasswordSubmit = async (e) => {
e.preventDefault();
try {
const userId = tokenService.getUserId();
const currentPassword = passwordData.currentPassword;
const newPassword = passwordData.newPassword;
const confirmNewPassword = passwordData.confirmNewPassword;
const response = await userApi.update_password(
userId,
currentPassword,
newPassword,
confirmNewPassword
);
if (response) {
console.log('Successfully updated password');
} else {
console.error('Error updating password');
}
} catch (error) {
console.error('Login error:', error);
} finally {
}
};
const renderPasswordForm = () => (
<div className="profile-form">
<h2>Change Password</h2>
<form onSubmit={handlePasswordSubmit}>
<div>
<label htmlFor="currentPassword">Current Password</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
value={passwordData.currentPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div>
<label htmlFor="newPassword">New Password</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={passwordData.newPassword}
onChange={handlePasswordChange}
required
/>
</div>
<div>
<label htmlFor="confirmNewPassword">Confirm New Password</label>
<input
type="password"
id="confirmNewPassword"
name="confirmNewPassword"
value={passwordData.confirmNewPassword}
onChange={handlePasswordChange}
required
/>
</div>
<button type="submit">Update Password</button>
<button type="button" onClick={() => setSubView('details')}>
Back to Profile
</button>
</form>
</div>
);
const renderProfileForm = () => (
<div className="profile-form">
<h2>User Profile</h2>
<form>
<div>
<label htmlFor="firstname">First Name</label>
<input
type="text"
id="firstname"
name="firstname"
value={formData.firstname}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="lastname">Last Name</label>
<input
type="text"
id="lastname"
name="lastname"
value={formData.lastname}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
readOnly
/>
</div>
<div>
<label htmlFor="createdDate">Created Date</label>
<input
type="text"
id="createdDate"
name="createdDate"
value={formData.createdDate}
readOnly
/>
</div>
<div>
<label htmlFor="lastLoginDate">Last Login Date</label>
<input
type="text"
id="lastLoginDate"
name="lastLoginDate"
value={formData.lastLoginDate}
readOnly
/>
</div>
</form>
<div>
<button type="button" onClick={() => setSubView('password')}>
Change Password
</button>
</div>
</div>
);
return (
<div className="dashboard-container">
{/* Navbar */}
<nav className="dashboard-nav">
<div className="nav-logo">
<span className="logo-text">{APP_NAME}</span>
</div>
<div className="nav-links">
<button
className={`nav-link ${activeView === 'dashboard' ? 'active' : ''}`}
onClick={() => handleNavClick('dashboard')}
>
Dashboard
</button>
<button
className={`nav-link ${activeView === 'profile' ? 'active' : ''}`}
onClick={() => handleNavClick('profile')}
>
Profile
</button>
</div>
</nav>
{/* Main Content */}
{subView === 'details' ? renderProfileForm() : renderPasswordForm()}
</div>
);
};
export default ProfileForm;
@@ -28,7 +28,15 @@ const useSendMessageWizard = () => {
case 'select': case 'select':
return state.selectedContacts.length > 0; return state.selectedContacts.length > 0;
case 'message': case 'message':
return state.message.trim().length > 0; if (state.message.trim().length > 0) {
if (state.createdMessage != null) {
return true;
} else {
return false;
}
} else {
return false;
}
case 'schedule': case 'schedule':
console.log('Checking to see if schedule can go to the next step'); console.log('Checking to see if schedule can go to the next step');
if (state.scheduledTime == null) { if (state.scheduledTime == null) {
@@ -49,6 +57,7 @@ const useSendMessageWizard = () => {
state.step, state.step,
state.selectedContacts.length, state.selectedContacts.length,
state.message, state.message,
state.createdMessage,
state.scheduledTime, state.scheduledTime,
]); ]);
@@ -33,11 +33,11 @@ function ConfirmationStep({ selectedContact, contactDetails, isSubmitting }) {
<span className={styles.reviewLabel}>First Name</span> <span className={styles.reviewLabel}>First Name</span>
<div className={styles.reviewComparison}> <div className={styles.reviewComparison}>
<span className={styles.oldValue}> <span className={styles.oldValue}>
{selectedContact.first_name || 'Not set'} {selectedContact.firstname || 'Not set'}
</span> </span>
<span className={styles.arrow}></span> <span className={styles.arrow}></span>
<span className={styles.newValue}> <span className={styles.newValue}>
{contactDetails.first_name || 'Not set'} {contactDetails.firstname || 'Not set'}
</span> </span>
</div> </div>
</div> </div>
@@ -46,11 +46,11 @@ function ConfirmationStep({ selectedContact, contactDetails, isSubmitting }) {
<span className={styles.reviewLabel}>Last Name</span> <span className={styles.reviewLabel}>Last Name</span>
<div className={styles.reviewComparison}> <div className={styles.reviewComparison}>
<span className={styles.oldValue}> <span className={styles.oldValue}>
{selectedContact.last_name || 'Not set'} {selectedContact.lastname || 'Not set'}
</span> </span>
<span className={styles.arrow}></span> <span className={styles.arrow}></span>
<span className={styles.newValue}> <span className={styles.newValue}>
{contactDetails.last_name || 'Not set'} {contactDetails.lastname || 'Not set'}
</span> </span>
</div> </div>
</div> </div>
+12 -12
View File
@@ -3,8 +3,8 @@ import styles from './ModifyForm.module.css';
function ModifyForm({ contactDetails, setContactDetails }) { function ModifyForm({ contactDetails, setContactDetails }) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
first_name: '', firstname: '',
last_name: '', lastname: '',
nickname: '', nickname: '',
}); });
@@ -12,8 +12,8 @@ function ModifyForm({ contactDetails, setContactDetails }) {
useEffect(() => { useEffect(() => {
if (contactDetails) { if (contactDetails) {
setFormData({ setFormData({
first_name: contactDetails.first_name || '', firstname: contactDetails.firstname || '',
last_name: contactDetails.last_name || '', lastname: contactDetails.lastname || '',
nickname: contactDetails.nickname || '', nickname: contactDetails.nickname || '',
}); });
} }
@@ -37,16 +37,16 @@ function ModifyForm({ contactDetails, setContactDetails }) {
<h3 className={styles.sectionTitle}>Edit Contact Information</h3> <h3 className={styles.sectionTitle}>Edit Contact Information</h3>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label htmlFor="first_name" className={styles.formLabel}> <label htmlFor="firstname" className={styles.formLabel}>
First Name First Name
<span className={styles.requiredIndicator}>*</span> <span className={styles.requiredIndicator}>*</span>
</label> </label>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<input <input
type="text" type="text"
id="first_name" id="firstname"
name="first_name" name="firstname"
value={formData.first_name} value={formData.firstname}
onChange={handleChange} onChange={handleChange}
className={styles.formInput} className={styles.formInput}
placeholder="Enter contact's first name" placeholder="Enter contact's first name"
@@ -61,16 +61,16 @@ function ModifyForm({ contactDetails, setContactDetails }) {
</div> </div>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<label htmlFor="last_name" className={styles.formLabel}> <label htmlFor="lastname" className={styles.formLabel}>
Last Name Last Name
<span className={styles.requiredIndicator}>*</span> <span className={styles.requiredIndicator}>*</span>
</label> </label>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<input <input
type="text" type="text"
id="last_name" id="lastname"
name="last_name" name="lastname"
value={formData.last_name} value={formData.lastname}
onChange={handleChange} onChange={handleChange}
className={styles.formInput} className={styles.formInput}
placeholder="Enter contact's last name" placeholder="Enter contact's last name"
@@ -49,15 +49,15 @@ function ViewContact({ selectedContact, onSelectedContact }) {
const filteredContacts = contacts.filter( const filteredContacts = contacts.filter(
(contact) => (contact) =>
contact.phone_number.toLowerCase().includes(searchTerm.toLowerCase()) || contact.phone_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.first_name?.toLowerCase().includes(searchTerm.toLowerCase()) || contact.firstname?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.last_name?.toLowerCase().includes(searchTerm.toLowerCase()) || contact.lastname?.toLowerCase().includes(searchTerm.toLowerCase()) ||
contact.nickname?.toLowerCase().includes(searchTerm.toLowerCase()) contact.nickname?.toLowerCase().includes(searchTerm.toLowerCase())
); );
const formatContactDisplay = (contact) => { const formatContactDisplay = (contact) => {
const phone = contact.phone_number || 'No phone'; const phone = contact.phone_number || 'No phone';
const name = const name =
[contact.first_name, contact.last_name].filter(Boolean).join(' ') || [contact.firstname, contact.lastname].filter(Boolean).join(' ') ||
'Unnamed'; 'Unnamed';
const nickname = contact.nickname ? `"${contact.nickname}"` : ''; const nickname = contact.nickname ? `"${contact.nickname}"` : '';
@@ -117,7 +117,7 @@ function ViewContact({ selectedContact, onSelectedContact }) {
onClick={() => onSelectedContact(contact)} onClick={() => onSelectedContact(contact)}
> >
<div className={styles.contactAvatar}> <div className={styles.contactAvatar}>
{contact.first_name?.[0]?.toUpperCase() || '?'} {contact.firstname?.[0]?.toUpperCase() || '?'}
</div> </div>
<div className={styles.contactInfo}> <div className={styles.contactInfo}>
<div className={styles.contactMainInfo}> <div className={styles.contactMainInfo}>
@@ -131,7 +131,7 @@ function ViewContact({ selectedContact, onSelectedContact }) {
)} )}
</div> </div>
<div className={styles.contactName}> <div className={styles.contactName}>
{[contact.first_name, contact.last_name] {[contact.firstname, contact.lastname]
.filter(Boolean) .filter(Boolean)
.join(' ') || 'Unnamed Contact'} .join(' ') || 'Unnamed Contact'}
</div> </div>
@@ -21,13 +21,13 @@ const useViewContactWizard = () => {
case 'modify': case 'modify':
if (!state.contactDetails) return false; if (!state.contactDetails) return false;
if ( if (
!state.contactDetails.first_name || !state.contactDetails.firstname ||
state.contactDetails.first_name.trim() === '' state.contactDetails.firstname.trim() === ''
) { ) {
return false; return false;
} else if ( } else if (
!state.contactDetails.last_name || !state.contactDetails.lastname ||
state.contactDetails.last_name.trim() === '' state.contactDetails.lastname.trim() === ''
) { ) {
return false; return false;
} else { } else {
@@ -61,8 +61,8 @@ const useViewContactWizard = () => {
? null ? null
: { : {
id: contactId, id: contactId,
first_name: contact.first_name, firstname: contact.firstname,
last_name: contact.last_name, lastname: contact.lastname,
phone_number: contact.phone_number, phone_number: contact.phone_number,
user_id: contact.user_id, user_id: contact.user_id,
nickname: contact.nickname || '', nickname: contact.nickname || '',
@@ -90,8 +90,8 @@ const useViewContactWizard = () => {
step: nextStepValue, step: nextStepValue,
submissionError: null, submissionError: null,
contactDetails: { contactDetails: {
first_name: prev.selectedContact.first_name || '', firstname: prev.selectedContact.firstname || '',
last_name: prev.selectedContact.last_name || '', lastname: prev.selectedContact.lastname || '',
nickname: prev.selectedContact.nickname || '', nickname: prev.selectedContact.nickname || '',
phone_number: prev.selectedContact.phone_number || '', phone_number: prev.selectedContact.phone_number || '',
id: prev.selectedContact.id, id: prev.selectedContact.id,
@@ -139,10 +139,10 @@ const useViewContactWizard = () => {
console.log('Contact details to update: ', state.contactDetails); console.log('Contact details to update: ', state.contactDetails);
let reqBody = new UpdateContactNamesRequest(); let reqBody = new UpdateContactNamesRequest();
reqBody.first_name = state.contactDetails.first_name; reqBody.firstname = state.contactDetails.firstname;
reqBody.last_name = state.contactDetails.last_name; reqBody.lastname = state.contactDetails.lastname;
reqBody.nickname = state.contactDetails.nickname || ''; // Send empty string if no nickname reqBody.nickname = state.contactDetails.nickname || ''; // Send empty string if no nickname
reqBody.contact_id = state.selectedContact.id; reqBody.id = state.selectedContact.id;
reqBody.user_id = userId; reqBody.user_id = userId;
const response = await contactApi.updateContactNames( const response = await contactApi.updateContactNames(
+99
View File
@@ -355,3 +355,102 @@
.view-sent-message-list-container::-webkit-scrollbar-thumb:hover { .view-sent-message-list-container::-webkit-scrollbar-thumb:hover {
background: #a1a1a1; background: #a1a1a1;
} }
.view-sent-message-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.tab-button {
padding: 10px 20px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: 500;
color: #6b7280;
transition: all 0.3s ease;
}
.tab-button.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
}
.tab-button:hover {
color: #374151;
background-color: #f9fafb;
}
.search-container {
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-header h3 {
margin: 0;
color: #111827;
font-size: 18px;
font-weight: 600;
}
.last-updated {
font-size: 12px;
color: #6b7280;
}
.no-messages {
text-align: center;
padding: 40px;
color: #6b7280;
font-style: italic;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-processing {
background-color: #fef3c7;
color: #92400e;
}
.status-sent {
background-color: #dcfce7;
color: #166534;
}
.status-failed {
background-color: #fee2e2;
color: #991b1b;
}
.status-default {
background-color: #f3f4f6;
color: #374151;
}
+174 -220
View File
@@ -1,8 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { tokenService } from '../services/TokenService'; import { tokenService } from '../services/TokenService';
import { messageEventResponseApi } from '../api/messageEventResponseApi'; import { messageEventResponseApi } from '../api/messageEventResponseApi';
import { scheduleApi } from '../api/scheduleApi';
import './ViewSentMessages.css'; import './ViewSentMessages.css';
const ViewSentMessages = ({ const ViewSentMessages = ({
@@ -11,10 +10,12 @@ const ViewSentMessages = ({
title = 'View Sent Messages', title = 'View Sent Messages',
}) => { }) => {
const [sentMessages, setSentMessages] = useState([]); const [sentMessages, setSentMessages] = useState([]);
const [scheduledMessages, setScheduledMessages] = useState([]);
const [lastUpdated, setLastUpdated] = useState(null); const [lastUpdated, setLastUpdated] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState('sent');
// Fetch the sent messages or Message Event Responses // Fetch the sent messages or Message Event Responses
const fetchSentMessages = useCallback(async () => { const fetchSentMessages = useCallback(async () => {
@@ -33,7 +34,6 @@ const ViewSentMessages = ({
setLastUpdated(new Date()); setLastUpdated(new Date());
setSentMessages(response.data); setSentMessages(response.data);
} else { } else {
setLoading(false);
throw new Error(response.message || 'Failed to load sent messages'); throw new Error(response.message || 'Failed to load sent messages');
} }
} catch (err) { } catch (err) {
@@ -44,8 +44,32 @@ const ViewSentMessages = ({
} }
}, []); }, []);
const fetchScheduledMessages = useCallback(async () => {
try {
const userId = tokenService.getUserId();
const response = await scheduleApi.getScheduledMessage(
userId,
tokenService.bearerToken()
);
if (response) {
const scheduledMessages = response.data;
setScheduledMessages(scheduledMessages);
} else {
throw new Error(response.message || 'Failed to get scheduled messages');
}
} catch (err) {
console.error('Failed to load scheduled messages:', err);
setScheduledMessages([]);
}
}, []);
const handleRefresh = () => { const handleRefresh = () => {
if (activeTab === 'sent') {
fetchSentMessages(); fetchSentMessages();
} else {
fetchScheduledMessages();
}
}; };
useEffect(() => { useEffect(() => {
@@ -55,6 +79,13 @@ const ViewSentMessages = ({
} }
}, [isOpen, fetchSentMessages]); }, [isOpen, fetchSentMessages]);
// Fetch scheduled messages when tab is switched
useEffect(() => {
if (activeTab === 'scheduled' && isOpen) {
fetchScheduledMessages();
}
}, [activeTab, isOpen, fetchScheduledMessages]);
const filteredSentMessages = sentMessages.filter((sentMessage) => { const filteredSentMessages = sentMessages.filter((sentMessage) => {
const searchLower = searchTerm.toLowerCase(); const searchLower = searchTerm.toLowerCase();
return ( return (
@@ -66,6 +97,15 @@ const ViewSentMessages = ({
); );
}); });
// Filter scheduled messages
const filteredScheduledMessages = scheduledMessages.filter((message) => {
const searchLower = searchTerm.toLowerCase();
return (
(message.status && message.status.toLowerCase().includes(searchLower)) ||
(message.id && message.id.toString().includes(searchTerm))
);
});
const formatLastUpdated = () => { const formatLastUpdated = () => {
if (!lastUpdated) return 'Never'; if (!lastUpdated) return 'Never';
return lastUpdated.toLocaleTimeString([], { return lastUpdated.toLocaleTimeString([], {
@@ -74,6 +114,25 @@ const ViewSentMessages = ({
}); });
}; };
// Format date for scheduled messages
const formatScheduledDate = (dateString) => {
return new Date(dateString).toLocaleString();
};
// Get status badge styling
const getStatusBadgeClass = (status) => {
switch (status) {
case 'PROCESSING':
return 'status-processing';
case 'SENT':
return 'status-sent';
case 'FAILED':
return 'status-failed';
default:
return 'status-default';
}
};
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
@@ -90,13 +149,40 @@ const ViewSentMessages = ({
</button> </button>
</div> </div>
{/* Add tabs for navigation */}
<div className="view-sent-message-tabs">
<button
className={`tab-button ${activeTab === 'sent' ? 'active' : ''}`}
onClick={() => setActiveTab('sent')}
>
Sent Messages
</button>
<button
className={`tab-button ${activeTab === 'scheduled' ? 'active' : ''}`}
onClick={() => setActiveTab('scheduled')}
>
Scheduled Messages
</button>
</div>
{/* Search input */}
<div className="search-container">
<input
type="text"
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="view-sent-message-form"> <div className="view-sent-message-form">
{error && ( {error && (
<div className="submit-error"> <div className="submit-error">
<div style={{ marginBottom: '8px' }}>{error}</div> <div style={{ marginBottom: '8px' }}>{error}</div>
<button <button
onClick={handleRefresh} onClick={handleRefresh}
onStype={{ style={{
background: '#ef4444', background: '#ef4444',
color: 'white', color: 'white',
border: 'none', border: 'none',
@@ -113,233 +199,101 @@ const ViewSentMessages = ({
{loading ? ( {loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
<div className="loafing-spinner" /> <div className="loading-spinner" />
<p style={{ marginTop: '20px', color: '#4b5563' }}> <p>Loading messages...</p>
Loading sent messages
</p>
</div> </div>
) : ( ) : (
<> <>
<div {/* Sent Messages Tab Content */}
style={{ {activeTab === 'sent' && (
display: 'flex', <div className="messages-section">
justifyContent: 'space-between', <div className="section-header">
alignItems: 'center', <h3>Sent Messages</h3>
marginBottom: '20px', <span className="last-updated">
flexWrap: 'wrap', Last updated: {formatLastUpdated()}
gap: '10px',
}}
>
<button
onClick={handleRefresh}
style={{
background: '#f3f4f6',
color: '#4b5563',
border: '1px solid #e5e7eb',
borderRadius: '6px',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span></span> Refresh
</button>
<input
type="text"
placeholder="Search sent messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
flex: 1,
minWidth: '200px',
maxWidth: '300px',
padding: '8px 12px',
border: '1px solid #e5e7eb',
borderRadius: '6px',
fontSize: '14px',
}}
/>
<div
style={{
fontSize: '12px',
color: '#6b7280',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<span>Updated: {formatLastUpdated()}</span>
<span
style={{
display: 'inline-block',
width: '8px',
height: '8px',
borderRadius: '50%',
background: lastUpdated ? '#10b981' : '#9ca3af',
}}
/>
</div>
</div>
{/* Sent Messages List */}
{filteredSentMessages.length === 0 ? (
<div
style={{
textAlign: 'center',
padding: '40px',
color: '#4b5563',
}}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>
X
</div>
<p
style={{
fontSize: '18px',
fontWeight: '500',
marginBottom: '8px',
}}
>
{searchTerm
? 'No matching sent messages found'
: 'No sent messages available'}
</p>
<p style={{ marginBottom: '20px' }}>
{searchTerm
? 'Try a different search term'
: 'Create a contact and send a message to get started'}
</p>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
style={{
background: '#f3f4f6',
color: '#4b5563',
border: '1px solid #e5e7eb',
borderRadius: '6px',
padding: '8px 16px',
cursor: 'pointer',
fontSize: '14px',
marginBottom: '10px',
}}
>
Clear Search
</button>
)}
</div>
) : (
<>
<div
style={{
maxHeight: '400px',
overflowY: 'auto',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: '#fafafa',
}}
>
<div style={{ padding: '4px' }}>
{filteredSentMessages.map((sentMessage, index) => (
<div
key={sentMessage.id}
style={{
padding: '16px',
borderBottom:
index < filteredSentMessages.length - 1
? '1px solid #e5e7eb'
: 'none',
background: 'white',
transition: 'all 0.2s',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '8px',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
}}
>
<span
style={{
background: '#667eea',
color: 'white',
width: '28px',
height: '28px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: '600',
}}
>
{index + 1}
</span>
<div>
<h4
style={{
margin: 0,
color: '#111827',
fontSize: '16px',
fontWeight: '600',
}}
>
{sentMessage.status}
</h4>
<h5>Sent: {sentMessage.sent}</h5>
</div>
</div>
<span
style={{
fontSize: '12px',
color: '#9ca3af',
background: '#f3f4f6',
padding: '2px 8px',
borderRadius: '4px',
}}
>
Id: {sentMessage.id}
</span> </span>
</div> </div>
</div>
{filteredSentMessages.length > 0 ? (
<div className="view-sent-message-list-container">
<table className="view-sent-message-table">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Sent Time</th>
</tr>
</thead>
<tbody>
{filteredSentMessages.map((message) => (
<tr key={message.id}>
<td>{message.id.substring(0, 8)}...</td>
<td>
<span
className={`status-badge ${getStatusBadgeClass(message.status)}`}
>
{message.status}
</span>
</td>
<td>{message.sent || 'N/A'}</td>
</tr>
))} ))}
</tbody>
</table>
</div> </div>
) : (
<div className="no-messages">No sent messages found.</div>
)}
</div>
)}
{/* Scheduled Messages Tab Content */}
{activeTab === 'scheduled' && (
<div className="messages-section">
<div className="section-header">
<h3>Scheduled Messages</h3>
</div>
{filteredScheduledMessages.length > 0 ? (
<div className="view-sent-message-list-container">
<table className="view-sent-message-table">
<thead>
<tr>
<th>ID</th>
<th>Scheduled Time</th>
<th>Created Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{filteredScheduledMessages.map((message) => (
<tr key={message.id}>
<td>{message.id.substring(0, 8)}...</td>
<td>
{formatScheduledDate(message.scheduled)}
</td>
<td>{formatScheduledDate(message.created)}</td>
<td>
<span
className={`status-badge ${getStatusBadgeClass(message.status)}`}
>
{message.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="no-messages">
No scheduled messages found.
</div>
)}
</div> </div>
</>
)} )}
</> </>
)} )}
<div className="form-actions">
<button
type="button"
className="cancel-btn"
onClick={onClose}
disabled={loading}
>
Close
</button>
<button
type="button"
className="submit-btn"
onClick={handleRefresh}
disabled={loading}
>
{loading ? 'Refreshing...' : 'Refresh List'}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,2 +1,2 @@
export const AUTH_API_BASE_URL = 'http://localhost:9080'; export const AUTH_API_BASE_URL = import.meta.env.VITE_AUTH_URL;
export const API_BASE_URL = 'http://localhost:9081'; export const API_BASE_URL = import.meta.env.VITE_API_URL;
+1
View File
@@ -2,3 +2,4 @@ export const DATA_KEY_ACCESS_TOKEN = 'access_token';
export const DATA_KEY_REGISTERED_USER_ID = 'registered_user_id'; export const DATA_KEY_REGISTERED_USER_ID = 'registered_user_id';
export const DATA_KEY_USER_ID = 'user_id'; export const DATA_KEY_USER_ID = 'user_id';
export const DATA_KEY_ADDED_CONTACT_ID = 'added_contact_id'; export const DATA_KEY_ADDED_CONTACT_ID = 'added_contact_id';
export const APP_NAME = 'schedtxt';