diff --git a/src/admin/components/FilterSection.jsx b/src/admin/components/FilterSection.jsx index 5d8cb4aa..6cad3329 100644 --- a/src/admin/components/FilterSection.jsx +++ b/src/admin/components/FilterSection.jsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import serviceData from '../../client/data/serviceData'; // Ensure this path is correct +import staticServiceData from '../../client/data/staticServiceData'; // Ensure this path is correct const FilterSection = ({ onFilterChange }) => { const handleFilterChange = (e) => { @@ -10,7 +10,7 @@ const FilterSection = ({ onFilterChange }) => { // Memoize the service options to avoid re-computation on every render const serviceOptions = useMemo( () => - serviceData.map((service) => ( + staticServiceData.map((service) => ( diff --git a/src/admin/components/PageHeader.jsx b/src/admin/components/PageHeader.jsx index 09281ea0..1af03e0e 100644 --- a/src/admin/components/PageHeader.jsx +++ b/src/admin/components/PageHeader.jsx @@ -1,18 +1,32 @@ import { Icon } from '@iconify/react'; +import { useLocation } from 'react-router-dom'; + +const PageHeader = ({ onSearch, openModal }) => { + const location = useLocation(); + const isEditService = location.pathname === '/admin/services'; -const PageHeader = ({ onSearch }) => { return (
-

Invoice

-
- - onSearch(e.target.value)} // Trigger onSearch when input changes - /> -
+

{ !isEditService ? 'Invoice' : 'Services' }

+ {!isEditService ? ( +
+ + onSearch(e.target.value)} // Trigger onSearch when input changes + /> +
+ ): ( + + )} +
); } diff --git a/src/admin/components/ServiceFormModal.jsx b/src/admin/components/ServiceFormModal.jsx new file mode 100644 index 00000000..9b07f4a2 --- /dev/null +++ b/src/admin/components/ServiceFormModal.jsx @@ -0,0 +1,140 @@ +import React, { useState, useEffect } from 'react'; +import { Icon } from '@iconify/react/dist/iconify.js'; +import Modal from 'react-modal'; + +const ServiceFormModal = ({ isOpen, onClose, onSaveService, editingService }) => { + // Initialize service state with default or editingService values + const [service, setService] = useState({ + title: '', + description: '', + features: ['', '', ''], // Assume 3 features initially + }); + + // Update service state when editingService changes + useEffect(() => { + if (editingService) { + setService({ + title: editingService.title, + description: editingService.description, + features: editingService.points.map(point => point.title), // Map points to feature titles + }); + } + }, [editingService]); + + // Prevent body scroll when modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + // Handle input changes for both service details and features + const handleChange = (e) => { + const { name, value } = e.target; + + if (name.startsWith('feature')) { + // Update the corresponding feature based on index + const index = parseInt(name.replace('feature', '')) - 1; + setService(prevService => { + const updatedFeatures = [...prevService.features]; + updatedFeatures[index] = value; + return { ...prevService, features: updatedFeatures }; + }); + } else { + setService(prevService => ({ ...prevService, [name]: value })); + } + }; + + // Handle form submission to save the service + const handleSubmit = (e) => { + e.preventDefault(); + + // Validate that features are unique and not empty + const nonEmptyFeatures = service.features.filter(f => f.trim() !== ''); + const featureSet = new Set(nonEmptyFeatures); + + if (featureSet.size < nonEmptyFeatures.length) { + alert("Features must be unique and not empty."); + return; + } + + // Prepare service data for saving + const updatedService = { + title: service.title, + description: service.description, + points: nonEmptyFeatures.map(feature => ({ title: feature })), // Convert features back to points + }; + + // Save the service and close the modal + onSaveService(updatedService); + onClose(); + }; + + return ( + +

+ {editingService ? 'Edit' : 'Add A'} Service +

+ +
+
+
+ + +
+
+ + +
+ {service.features.map((feature, index) => ( +
+ + +
+ ))} +
+ +
+
+ ); +} + +export default ServiceFormModal; diff --git a/src/admin/pages/Services.js b/src/admin/pages/Services.js index 90331e78..a9ebe438 100644 --- a/src/admin/pages/Services.js +++ b/src/admin/pages/Services.js @@ -1,121 +1,151 @@ -import React, { useState } from 'react'; -import ReactDOM from 'react-dom'; +import React, { useState, useEffect } from 'react'; +import PageHeader from '../components/PageHeader'; +import { Icon } from '@iconify/react/dist/iconify.js'; +import ServiceFormModal from '../components/ServiceFormModal'; +const Services = () => { + const [services, setServices] = useState([]); + const [isModalOpen, setModalOpen] = useState(false); + const [editingService, setEditingService] = useState(null); + const [error, setError] = useState(null); + const API_URL = 'https://api.example.com/services'; // Replace with your actual API URL -// Service Form Component -function ServiceForm({ onAddService }) { - const [service, setService] = useState({ - name: '', - description: '', - features: '', - }); + useEffect(() => { + const fetchServices = async () => { + try { + const response = await fetch(API_URL); + if (!response.ok) { + throw new Error(`Failed to fetch services: ${response.statusText}`); + } + const data = await response.json(); + setServices(data); // Set the fetched services data + } catch (error) { + setError(error.message); // Handle any errors that occur during fetch + } + }; - const handleChange = (e) => { - setService({ ...service, [e.target.name]: e.target.value }); + fetchServices(); // Fetch services when the component mounts + }, []); + + const saveService = async (newService) => { + try { + if (editingService) { + // Update an existing service + const response = await fetch(`${API_URL}/${editingService.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newService), + }); + if (!response.ok) { + throw new Error(`Failed to update service: ${response.statusText}`); + } + const updatedService = await response.json(); + setServices(services.map(service => + service.id === editingService.id ? updatedService : service + )); + } else { + // Add a new service + const response = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newService), + }); + if (!response.ok) { + throw new Error(`Failed to add service: ${response.statusText}`); + } + const addedService = await response.json(); + setServices([...services, addedService]); + } + setEditingService(null); // Reset editing state + setModalOpen(false); // Close the modal after save + } catch (error) { + setError(error.message); // Handle any errors during save + } }; - const handleSubmit = (e) => { - e.preventDefault(); - onAddService({ - ...service, - features: service.features.split(',').map(feature => feature.trim()), - }); - setService({ name: '', description: '', features: '' }); + const deleteService = async (id) => { + try { + const response = await fetch(`${API_URL}/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error(`Failed to delete service: ${response.statusText}`); + } + setServices(services.filter(service => service.id !== id)); // Remove the deleted service from state + } catch (error) { + setError(error.message); // Handle any errors during deletion + } }; - return ( -
-
- - -
-
- - -
-
- - -
- -
- ); -} + const openModal = () => { + setEditingService(null); // Reset editing state for a new service + setModalOpen(true); // Open the modal + }; -// Services Component -function Services() { - const [services, setServices] = useState([ - { - name: 'Auto Resprays', - description: 'Revitalize your car’s appearance with our top-quality respray services...', - features: ['Full Body Resprays', 'Panel Resprays', 'Custom Paint Jobs'], - }, - { - name: 'Car Detailing', - description: 'Keep your car looking brand new with our comprehensive car detailing services...', - features: ['Exterior Detailing', 'Interior Detailing', 'Ceramic Coating'], - }, - ]); + const closeModal = () => { + setModalOpen(false); // Close the modal without saving + }; - const addService = (newService) => { - setServices([...services, newService]); + const handleEdit = (service) => { + setEditingService(service); // Set the service to be edited + setModalOpen(true); // Open the modal for editing }; return ( -
- - - - - - - - - - - - {services.map((service, index) => ( - - - - - +
+ {error && ( +
+

Error: {error}

+ +
+ )} + + + + +
+
NameDescriptionFeaturesActions
{service.name}{service.description} -
    - {service.features.map((feature, i) => ( -
  • {feature}
  • - ))} -
-
- - -
+ + + + + + - ))} - -
NameDescriptionFeaturesActions
+ + + {services.map((service, index) => ( + + {service.title} + {service.description} + + + + + + + + + ))} + + +
); } diff --git a/src/client/components/BookingHistoryModal.jsx b/src/client/components/BookingHistoryModal.jsx index 213038a3..d2ead374 100644 --- a/src/client/components/BookingHistoryModal.jsx +++ b/src/client/components/BookingHistoryModal.jsx @@ -81,6 +81,19 @@ const BookingHistoryModal = ({ isOpen, onClose, customerId }) => { } }; + // Prevent body scroll when modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + const updateBookingStatus = async (invoiceNumber, newStatus) => { try { const response = await fetch(`/api/customers/${customerId}/bookings/${invoiceNumber}`, { @@ -177,4 +190,4 @@ const BookingHistoryModal = ({ isOpen, onClose, customerId }) => { ); }; -export default BookingHistoryModal; +export default BookingHistoryModal; \ No newline at end of file diff --git a/src/client/components/Footer.jsx b/src/client/components/Footer.jsx index 778419d3..cc197711 100644 --- a/src/client/components/Footer.jsx +++ b/src/client/components/Footer.jsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; -import serviceData from '../data/serviceData'; +import staticServiceData from '../data/staticServiceData'; import locationIcon from '../assets/location.png'; import emailIcon from '../assets/email.png'; import phoneIcon from '../assets/phone.png'; @@ -12,7 +12,7 @@ import MenuItems from './MenuItems'; // Footer component displays the footer section of the website const Footer = () => { // Memoize service data to prevent re-rendering unless the data changes - const services = useMemo(() => serviceData, []); + const services = useMemo(() => staticServiceData, []); return ( <> diff --git a/src/client/components/LoginSignupModal.jsx b/src/client/components/LoginSignupModal.jsx index c93dd1d9..0701eec6 100644 --- a/src/client/components/LoginSignupModal.jsx +++ b/src/client/components/LoginSignupModal.jsx @@ -1,10 +1,10 @@ import React, { useState, useEffect, useCallback, useReducer, useContext } from 'react'; import Modal from 'react-modal'; -import { FaEnvelope, FaUser } from 'react-icons/fa'; -import { MdOutlineVpnKey } from 'react-icons/md'; -import classNames from 'classnames'; import { useNavigate } from 'react-router-dom'; import { UserContext } from '../context/UserContext'; +import PasswordResetModals from './PasswordResetModals'; +import { Icon } from '@iconify/react/dist/iconify.js'; +import classNames from 'classnames'; // Initial state for form inputs const initialState = { @@ -32,21 +32,30 @@ function reducer(state, action) { } } -// Component for individual input fields with icons and error handling -const InputField = ({ label, type, icon: Icon, value, onChange, error }) => ( +// Component for individual input fields with icons, placeholders, and error handling +const InputField = ({ label, type, icon, value, onChange, error, placeholder, togglePassword }) => (
- + +
+ {togglePassword && ( + + )} {error &&

{error}

}
@@ -57,13 +66,15 @@ const LoginSignupModal = ({ isOpen, onClose, initialAction }) => { const [state, dispatch] = useReducer(reducer, initialState); // State management for form inputs const [errors, setErrors] = useState({}); // State to track form validation errors const { setIsLoggedIn } = useContext(UserContext); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); // Correct visibility state const navigate = useNavigate(); + const [isResetPasswordOpen, setIsResetPasswordOpen] = useState(false); // Sanitize input values to prevent unnecessary data from being submitted const sanitizeInput = (input) => input.trim(); // Validate form inputs before submission - const validate = () => { + const validate = useCallback(() => { const newErrors = {}; if (!state.email) { @@ -74,9 +85,11 @@ const LoginSignupModal = ({ isOpen, onClose, initialAction }) => { if (!state.password) { newErrors.password = 'Password is required'; - } else if (state.password.length < 6) { - newErrors.password = 'Password must be at least 6 characters'; - } + } else if (state.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters'; + } else if(state.password.length > 24) { + newErrors.password = 'Password must be at most 24 characters'; + } if (action === 'Sign Up') { if (!state.firstname) { @@ -88,7 +101,7 @@ const LoginSignupModal = ({ isOpen, onClose, initialAction }) => { } return newErrors; - }; + }, [state.email, state.password, state.firstname, state.lastname, action]); // Handle form submission const handleSubmit = useCallback( @@ -156,7 +169,7 @@ const LoginSignupModal = ({ isOpen, onClose, initialAction }) => { alert('An error occurred while processing your request. Please try again later.'); } }, - [action, state, onClose, navigate, setIsLoggedIn] + [action, state, onClose, navigate, setIsLoggedIn, validate] ); // Toggle between Sign In and Sign Up forms @@ -180,85 +193,101 @@ const LoginSignupModal = ({ isOpen, onClose, initialAction }) => { }; }, [isOpen]); + const handleForgotPassword = () => { + onClose(); + setIsResetPasswordOpen(true); + }; + + return ( - -

{action}

-

Please enter your details

-
- {action === 'Sign Up' && ( - <> - dispatch({ type: 'SET_FIRSTNAME', payload: e.target.value })} - error={errors.firstname} - /> - dispatch({ type: 'SET_LASTNAME', payload: e.target.value })} - error={errors.lastname} - /> - - )} - dispatch({ type: 'SET_EMAIL', payload: e.target.value })} - error={errors.email} - /> - dispatch({ type: 'SET_PASSWORD', payload: e.target.value })} - error={errors.password} - /> - {action === 'Sign In' && ( - + <> + +

{action}

+

Please enter your details

+ + {action === 'Sign Up' && ( + <> + dispatch({ type: 'SET_FIRSTNAME', payload: e.target.value })} + error={errors.firstname} + placeholder="Enter your first name" + /> + dispatch({ type: 'SET_LASTNAME', payload: e.target.value })} + error={errors.lastname} + placeholder="Enter your last name" + /> + + )} + dispatch({ type: 'SET_EMAIL', payload: e.target.value })} + error={errors.email} + placeholder="onlyspeedstar@gmail.com" + /> + dispatch({ type: 'SET_PASSWORD', payload: e.target.value })} + togglePassword={() => setIsPasswordVisible(!isPasswordVisible)} + error={errors.password} + placeholder="Enter your password" + /> + {action === 'Sign In' && ( +
+ +
+ )} + + +

+ {action === 'Sign In' ? "Don't" : 'Already'} have an account? + +

- -

- {action === 'Sign In' ? "Don't" : 'Already'} have an account? - -

- -
+
+ setIsResetPasswordOpen(false)} + /> + ); }; diff --git a/src/client/components/MenuItems.jsx b/src/client/components/MenuItems.jsx index 6ef5b4ce..95650734 100644 --- a/src/client/components/MenuItems.jsx +++ b/src/client/components/MenuItems.jsx @@ -16,7 +16,7 @@ const MenuItems = ({ isHeader = false, className = "" , setIsOpen }) => { - `hover:text-[#DE0000] block w-fit ${isHeader ? 'py-2 md:py-3 lg:py-4 px-4 md:px-0' : '' } transition-colors duration-300 ${(isHeader && isActive) ? 'text-[#DE0000] font-bold' : ''}` + `hover:text-[#DE0000] ${isHeader ? 'block w-fit py-2 md:py-3 lg:py-4 px-4 md:px-0' : '' } transition-colors duration-300 ${(isHeader && isActive) ? 'text-[#DE0000] font-bold' : ''}` } onClick={handleMenuItemClick} > diff --git a/src/client/components/PasswordResetModals.jsx b/src/client/components/PasswordResetModals.jsx new file mode 100644 index 00000000..4e8df413 --- /dev/null +++ b/src/client/components/PasswordResetModals.jsx @@ -0,0 +1,240 @@ +import React, { useState, useContext, useEffect } from 'react'; +import Modal from 'react-modal'; +import { UserContext } from '../context/UserContext'; +import { Icon } from '@iconify/react/dist/iconify.js'; + +const InputField = ({ label, type, icon, value, onChange, error, placeholder, PasswordMarginBottom, togglePassword }) => ( +
+ +
+ +
+ + {togglePassword && ( + + )} + {error &&

{error}

} +
+
+); + +const PasswordResetModals = ({ isOpen, onClose }) => { + const [step, setStep] = useState(1); + const [email, setEmail] = useState(''); + const [emailError, setEmailError] = useState(''); + const [code, setCode] = useState(Array(6).fill('')); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [newPasswordError, setNewPasswordError] = useState(''); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] = useState(false); + const { setIsLoggedIn } = useContext(UserContext); + + const validateEmail = (email) => { + // Simple email validation regex + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleSendCode = () => { + if (!validateEmail(email)) { + setEmailError('Please enter a valid email address'); + return; + } + + // Logic to send the verification code to the email + setEmailError(''); + setStep(2); + }; + + const handleInputChange = (e, index) => { + const { value } = e.target; + + // Create a copy of the code array + const newCode = [...code]; + + if (/^[0-9]$/.test(value)) { + newCode[index] = value; + setCode(newCode); + + // Auto-focus on the next input field + if (index < 5 && value) { + document.getElementById(`code-input-${index + 1}`).focus(); + } + + // If all inputs are filled, automatically verify the code + if (index === 5 && newCode.every((digit) => digit !== '')) { + handleVerifyCode(newCode); + } + } + }; + + const handleKeyDown = (e, index) => { + if (e.key === 'Backspace' || e.key === 'ArrowLeft') { + e.preventDefault(); + const newCode = [...code]; + if (code[index] !== '') { + newCode[index] = ''; + setCode(newCode); + } else if (index > 0) { + document.getElementById(`code-input-${index - 1}`).focus(); + } + } else if (e.key === 'ArrowRight' && index < 5) { + document.getElementById(`code-input-${index + 1}`).focus(); + } + }; + + const handleVerifyCode = (enteredCodeArray) => { + const enteredCode = enteredCodeArray.join(''); + // Logic to verify the code + if (enteredCode === '123456') { // Replace with actual verification logic + setStep(3); + } else { + alert('Invalid code'); + } + }; + + const handleResetPassword = () => { + if (newPassword.length < 8) { + setNewPasswordError('Password must be at least 8 characters long'); + return; + } else if (newPassword.length > 24) { + setNewPasswordError('Password must be at most 24 characters long'); + return; + } + + if (newPassword !== confirmPassword) { + alert('Passwords do not match'); + return; + } + + setNewPasswordError(''); + + // Logic to reset the password + setIsLoggedIn(true); + onClose(); // Close the modal after resetting the password + }; + + // Disable background scrolling when the modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + + // Cleanup when component unmounts or modal closes + return () => { + document.body.style.overflow = ''; + }; + }, [isOpen]); + + return ( + + {step === 1 && ( +
+

Forgot Password

+

Enter your email to receive instructions to reset your password

+ setEmail(e.target.value)} + error={emailError} + placeholder="onlyspeedstar@gmail.com" + /> + +
+ )} + + {step === 2 && ( +
+

Forgot Password

+

We sent a code to {email.replace(/(.{2})(.*)(@.*)/, "$1******$3")}

+
+ {code.map((digit, index) => ( + handleInputChange(e, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + /> + ))} +
+ +
+ )} + + {step === 3 && ( +
+

Set New Password

+

+ Your password must contain at least 8 characters, and include at least one uppercase letter, one lowercase letter, one number, and one special character. +

+ setNewPassword(e.target.value)} + togglePassword={() => setIsPasswordVisible(!isPasswordVisible)} + PasswordMarginBottom={true} + error={newPasswordError} + /> + setConfirmPassword(e.target.value)} + togglePassword={() => setIsConfirmPasswordVisible(!isConfirmPasswordVisible)} + /> + +
+ )} + +
+ ); +}; + +export default PasswordResetModals; diff --git a/src/client/components/RatingPopup.jsx b/src/client/components/RatingPopup.jsx index e60bab00..dfd0ec35 100644 --- a/src/client/components/RatingPopup.jsx +++ b/src/client/components/RatingPopup.jsx @@ -5,14 +5,17 @@ const RatingPopup = ({ isOpen, onClose, onSubmit }) => { const [comment, setComment] = useState(''); const [isSubmitDisabled, setIsSubmitDisabled] = useState(true); + // Enable or disable the submit button based on rating and comment inputs useEffect(() => { setIsSubmitDisabled(rating === 0 || comment.trim() === ''); }, [rating, comment]); + // Handle star rating change const handleRatingChange = (value) => { setRating(value); }; + // Handle form submission const handleSubmit = async () => { const payload = { rating, comment }; @@ -32,27 +35,27 @@ const RatingPopup = ({ isOpen, onClose, onSubmit }) => { const data = await response.json(); console.log('Rating submitted successfully:', data); - onSubmit(payload); + onSubmit(payload); // Callback to parent component after successful submission } catch (error) { console.error('Error submitting rating:', error); } finally { - onClose(); + onClose(); // Close the popup after submission or error } }; - if (!isOpen) return null; + if (!isOpen) return null; // Do not render the popup if it's not open return ( -
-

Rate Our Service

+
+

Rate Our Service

{[1, 2, 3, 4, 5].map((star) => ( @@ -69,13 +72,13 @@ const RatingPopup = ({ isOpen, onClose, onSubmit }) => {
+
+ ); + } return (
- {/* Page title */}

Our Services

- {/* Map through serviceData to create service sections */} - {serviceData.map((section, index) => ( - - ))} + {services.map((section, index) => { + // Find matching static service + const matchedService = staticServiceData.find( + (staticService) => staticService.title === section.title + ); + + // Use matched service's image and icon, or default if no match + const image = matchedService ? matchedService.image : defaultService.image; + const icon = matchedService ? matchedService.icon : defaultService.icon; + + return ( + + ); + })}
); }; -export default ServicesPage; +export default ServicesPage; \ No newline at end of file