tsk-5: Add contact (#15)

Closes #5

Reviewed-on: phoenix/textsender#15
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
This commit is contained in:
phoenix
2025-12-21 00:27:20 +00:00
committed by phoenix
parent 6514f290d7
commit 6f952dc8df
8 changed files with 443 additions and 5 deletions
+159
View File
@@ -0,0 +1,159 @@
/* Contact Modal Styles */
.contact-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.contact-modal {
background: transparent;
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.contact-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.contact-title {
color: white;
font-size: 24px;
font-weight: 600;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 32px;
cursor: pointer;
line-height: 1;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.3s ease;
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.contact-card {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
width: 100%;
}
.contact-form {
margin-bottom: 0;
}
.required {
color: #ef4444;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 30px;
}
.cancel-btn {
flex: 1;
background: #f3f4f6;
color: #4b5563;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 14px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.cancel-btn:hover:not(:disabled) {
background: #e5e7eb;
transform: translateY(-1px);
}
.cancel-btn:active:not(:disabled) {
transform: translateY(0);
}
.cancel-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.submit-btn {
flex: 1;
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;
}
.submit-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.submit-btn:active:not(:disabled) {
transform: translateY(0);
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Responsive adjustments */
@media (max-width: 480px) {
.contact-card {
padding: 30px 20px;
}
.contact-title {
font-size: 20px;
}
.form-actions {
flex-direction: column;
}
}
.submit-error {
background-color: #fee;
border: 1px solid #fcc;
border-radius: 6px;
padding: 12px;
margin-bottom: 20px;
text-align: center;
color: #ef4444;
font-size: 14px;
}
+224
View File
@@ -0,0 +1,224 @@
import { useState } from 'react';
import { API_BASE_URL } from '../constants/api';
import {
DATA_KEY_ACCESS_TOKEN,
DATA_KEY_ADDED_CONTACT_ID,
DATA_KEY_USER_ID,
} from '../constants/app';
import './AddContact.css';
const AddContact = ({ isOpen, onClose, onSuccess }) => {
const [formData, setFormData] = useState({
phoneNumber: '',
firstName: '',
lastName: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState('');
if (!isOpen) return null;
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({
...prev,
[name]: '',
}));
}
// Clear submit error when user modifies form
if (submitError) {
setSubmitError('');
}
};
const validateForm = () => {
const newErrors = {};
// Phone number is required
if (!formData.phoneNumber.trim()) {
newErrors.phoneNumber = 'Phone number is required';
} else if (!/^[\d\s\-\+\(\)]{10,}$/.test(formData.phoneNumber)) {
newErrors.phoneNumber = 'Please enter a valid phone number';
}
return newErrors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSubmitting(true);
setSubmitError('');
try {
const userId = localStorage.getItem(DATA_KEY_USER_ID);
const accessToken = localStorage.getItem(DATA_KEY_ACCESS_TOKEN);
const reqBod = {
phone_number: formData.phoneNumber,
first_name: formData.firstName,
last_name: formData.lastName,
user_id: userId,
};
// Send the API request directly from the Contact component
const response = await fetch(`${API_BASE_URL}/api/v1/contact/new`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(reqBod),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
`Failed to create contact. Status: ${response.status}`
);
}
const result = await response.json();
if (result.data.length > 0) {
console.log('Contact added');
const contactId = result.data[0].id;
localStorage.setItem(DATA_KEY_ADDED_CONTACT_ID, contactId);
}
// Reset form
setFormData({ phoneNumber: '', firstName: '', lastName: '' });
// Call onSuccess callback if provided
if (onSuccess) {
onSuccess(result);
}
// Close the modal
onClose();
} catch (error) {
console.error('Error submitting form:', error);
setSubmitError(
error.message || 'Failed to create contact. Please try again.'
);
} finally {
setIsSubmitting(false);
}
};
const handleCancel = () => {
setFormData({ phoneNumber: '', firstName: '', lastName: '' });
setErrors({});
setSubmitError('');
onClose();
};
return (
<div className="contact-modal-overlay">
<div className="contact-modal">
<div className="contact-header">
<h2 className="contact-title">Add New Contact</h2>
<button
className="close-btn"
onClick={handleCancel}
aria-label="Close"
>
×
</button>
</div>
<div className="contact-card">
{submitError && <div className="submit-error">{submitError}</div>}
<form className="contact-form" onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="phoneNumber" className="form-label">
Phone Number <span className="required">*</span>
</label>
<input
id="phoneNumber"
name="phoneNumber"
type="tel"
className={`form-input ${errors.phoneNumber ? 'error' : ''}`}
value={formData.phoneNumber}
onChange={handleChange}
placeholder="Enter phone number"
disabled={isSubmitting}
required
/>
{errors.phoneNumber && (
<span className="error-message">{errors.phoneNumber}</span>
)}
</div>
<div className="form-group">
<label htmlFor="firstName" className="form-label">
First Name
</label>
<input
id="firstName"
name="firstName"
type="text"
className="form-input"
value={formData.firstName}
onChange={handleChange}
placeholder="Enter first name (optional)"
disabled={isSubmitting}
/>
<span className="form-hint">Optional field</span>
</div>
<div className="form-group">
<label htmlFor="lastName" className="form-label">
Last Name
</label>
<input
id="lastName"
name="lastName"
type="text"
className="form-input"
value={formData.lastName}
onChange={handleChange}
placeholder="Enter last name (optional)"
disabled={isSubmitting}
/>
<span className="form-hint">Optional field</span>
</div>
<div className="form-actions">
<button
type="button"
className="cancel-btn"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="submit-btn"
disabled={isSubmitting}
>
{isSubmitting ? 'Creating...' : 'Create Contact'}
</button>
</div>
</form>
</div>
</div>
</div>
);
};
export default AddContact;
+25
View File
@@ -211,3 +211,28 @@
font-size: 18px;
}
}
/* Add Contact Button Styles */
.add-contact-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin: 20px;
}
.add-contact-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.add-contact-btn:active {
transform: translateY(0);
}
/* Existing dashboard styles... */
+29 -3
View File
@@ -1,8 +1,25 @@
import { useState } from 'react';
import AddContact from './AddContact';
import './Dashboard.css';
const Dashboard = () => {
const [activeView, setActiveView] = useState('dashboard');
const [showContactForm, setShowContactForm] = useState(false);
const handleAddContactClick = () => {
setShowContactForm(true);
};
const handleContactSuccess = (contactData) => {
// Optional: Handle successful creation
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 handleButtonClick = (buttonNumber) => {
console.log(`Button ${buttonNumber} clicked`);
@@ -58,12 +75,21 @@ const Dashboard = () => {
<button
className="dashboard-btn secondary"
onClick={() => handleButtonClick(2)}
onClick={() => handleAddContactClick()}
>
<span className="btn-text">Choice 2</span>
<span className="btn-hint">Click for action 2</span>
<span className="btn-text">Add Contact</span>
<span className="btn-hint">
Create contacts to send text messages
</span>
</button>
{/* Contact Form Modal - Now self-contained */}
<AddContact
isOpen={showContactForm}
onClose={() => setShowContactForm(false)}
onSuccess={handleContactSuccess}
/>
<button
className="dashboard-btn tertiary"
onClick={() => handleButtonClick(3)}
+2 -1
View File
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AUTH_API_BASE_URL } from '../constants/api';
import { DATA_KEY_ACCESS_TOKEN } from '../constants/app';
import { DATA_KEY_ACCESS_TOKEN, DATA_KEY_USER_ID } from '../constants/app';
import './Login.css';
@@ -81,6 +81,7 @@ const Login = () => {
const token = loginResult.access_token;
console.log(token);
localStorage.setItem(DATA_KEY_ACCESS_TOKEN, token);
localStorage.setItem(DATA_KEY_USER_ID, loginResult.user_id);
alert(`Login successful! Welcome, ${formData.username}`);
navigate('/dashboard');
} else {
+1
View File
@@ -1 +1,2 @@
export const AUTH_API_BASE_URL = 'http://localhost:9080';
export const API_BASE_URL = 'http://localhost:8080';
+2
View File
@@ -1,2 +1,4 @@
export const DATA_KEY_ACCESS_TOKEN = 'access_token';
export const DATA_KEY_REGISTERED_USER_ID = 'registered_user_id';
export const DATA_KEY_USER_ID = 'user_id';
export const DATA_KEY_ADDED_CONTACT_ID = 'added_contact_id';