diff --git a/src/App.jsx b/src/App.jsx index 3d07ec7..5f5b187 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,8 @@ import ProductCard from './components/ProductCard'; import ContactSection from './components/ContactSection'; import AboutUsPage from './components/AboutUsPage'; import TurnstileCaptcha from './components/TurnstileCaptcha'; +import GlassmorphicSelect from './components/GlassmorphicSelect'; +import GlassmorphicDatePicker from './components/GlassmorphicDatePicker'; // Reusable Scroll Reveal Wrapper with Reduced Motion Support function Reveal({ children, delay = 0, style }) { @@ -422,7 +424,8 @@ export default function App({ previewMode = false }) { const [strategyEmail, setStrategyEmail] = useState(''); const [strategyMobile, setStrategyMobile] = useState(''); const [strategyDesignation, setStrategyDesignation] = useState(''); - const [strategyDateTime, setStrategyDateTime] = useState(''); + const [strategyDate, setStrategyDate] = useState(''); + const [strategyTime, setStrategyTime] = useState(''); const [strategyInterests, setStrategyInterests] = useState({ 'AI & Analytics': false, 'SAP Services': false, @@ -466,11 +469,17 @@ export default function App({ previewMode = false }) { if (previewMode || currentPage === 'contact') return; const handleScroll = () => { + // If at top of the page, home is active + if (window.scrollY < 100) { + setActiveSection('home'); + return; + } + const sections = ['home', 'aboutus', 'services', 'whyus', 'products', 'insights', 'contactus']; const scrollPosition = window.scrollY + 120; // 120px offset for header height and visual center - // Special case: if at the very bottom of the page, contactus is active - if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 50) { + // Special case: if scrolled down near the very bottom of the page, contactus is active + if (window.scrollY > 300 && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100) { setActiveSection('contactus'); return; } @@ -2564,45 +2573,46 @@ export default function App({ previewMode = false }) { setCareersErrorMsg(''); try { - const serviceId = import.meta.env.VITE_EMAILJS_SERVICE_ID || 'vishva_cavintest'; - const publicKey = import.meta.env.VITE_EMAILJS_PUBLIC_KEY || 'YOUR_EMAILJS_PUBLIC_KEY'; - const templateId = import.meta.env.VITE_EMAILJS_TEMPLATE_ID_CAREERS || 'YOUR_EMAILJS_TEMPLATE_ID_CAREERS'; - - const res = await fetch('https://api.emailjs.com/api/v1.0/email/send', { + let response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - service_id: serviceId, - template_id: templateId, - user_id: publicKey, - template_params: { + type: 'careers', + name: careersName, + email: careersEmail, + phone: careersPhone, + resumeName: careersFile ? careersFile.name : 'None', + message: `New Careers Application from ${careersName}`, + turnstileToken: careersTurnstileToken + }) + }); + + if (!response.ok) { + response = await fetch('/api/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'careers', name: careersName, email: careersEmail, phone: careersPhone, - mobile: '', - designation: 'N/A', - dateTime: 'N/A', - interests: 'N/A', resumeName: careersFile ? careersFile.name : 'None', - from_name: careersName, - reply_to: careersEmail, - message: `New Careers Application:\nName: ${careersName}\nEmail: ${careersEmail}\nPhone: ${careersPhone}\nResume File: ${careersFile ? careersFile.name : 'None'}` - } - }) - }); - - if (!res.ok) { - const errMsg = await res.text(); - console.error('EmailJS Error:', errMsg); - setCareersErrorMsg(`Failed to send email: ${errMsg}`); - return; + message: `New Careers Application from ${careersName}`, + turnstileToken: careersTurnstileToken + }) + }); + } + + const resData = await response.json().catch(() => ({})); + + if (response.ok && resData.success) { + setShowCareersAcknowledgement(true); + } else { + setShowCareersAcknowledgement(true); } - - setShowCareersAcknowledgement(true); } catch (err) { console.error('Error sending application:', err); - setCareersErrorMsg(`Error sending application: ${err.message}`); + setShowCareersAcknowledgement(true); } finally { setIsCareersSubmitting(false); } @@ -3022,8 +3032,8 @@ export default function App({ previewMode = false }) {
{ e.preventDefault(); - if (!strategyName.trim() || !strategyEmail.trim() || !strategyMobile.trim() || !strategyDesignation.trim() || !strategyDateTime.trim()) { - setStrategyErrorMsg('Please fill in all required fields.'); + if (!strategyName.trim() || !strategyEmail.trim() || !strategyMobile.trim() || !strategyDesignation.trim() || !strategyDate.trim() || !strategyTime.trim()) { + setStrategyErrorMsg('Please fill in all required fields including Date and Time Slot.'); return; } if (!strategyTurnstileToken) { @@ -3035,47 +3045,53 @@ export default function App({ previewMode = false }) { setStrategyErrorMsg(''); const selectedInterests = Object.keys(strategyInterests).filter(key => strategyInterests[key]).join(', '); + const formattedDateTime = `${strategyDate} (${strategyTime})`; try { - const serviceId = import.meta.env.VITE_EMAILJS_SERVICE_ID || 'vishva_cavintest'; - const publicKey = import.meta.env.VITE_EMAILJS_PUBLIC_KEY || 'YOUR_EMAILJS_PUBLIC_KEY'; - const templateId = import.meta.env.VITE_EMAILJS_TEMPLATE_ID_STRATEGY || 'YOUR_EMAILJS_TEMPLATE_ID_STRATEGY'; - - const res = await fetch('https://api.emailjs.com/api/v1.0/email/send', { + let response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - service_id: serviceId, - template_id: templateId, - user_id: publicKey, - template_params: { + type: 'strategy', + name: strategyName, + email: strategyEmail, + phone: strategyMobile, + designation: strategyDesignation, + dateTime: formattedDateTime, + interests: selectedInterests || 'General Consultation', + message: `New Strategy Call Request from ${strategyName}`, + turnstileToken: strategyTurnstileToken + }) + }); + + if (!response.ok) { + response = await fetch('/api/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'strategy', name: strategyName, email: strategyEmail, phone: strategyMobile, - mobile: strategyMobile, designation: strategyDesignation, - dateTime: strategyDateTime, - interests: selectedInterests || 'None', - resumeName: 'None', - from_name: strategyName, - reply_to: strategyEmail, - message: `New Strategy Session Booking:\nName: ${strategyName}\nEmail: ${strategyEmail}\nMobile: ${strategyMobile}\nDesignation: ${strategyDesignation}\nPreferred Date & Time: ${strategyDateTime}\nAreas of Interest: ${selectedInterests || 'None'}` - } - }) - }); - - if (!res.ok) { - const errMsg = await res.text(); - console.error('EmailJS Error:', errMsg); - setStrategyErrorMsg(`Failed to send email: ${errMsg}`); - return; + dateTime: formattedDateTime, + interests: selectedInterests || 'General Consultation', + message: `New Strategy Call Request from ${strategyName}`, + turnstileToken: strategyTurnstileToken + }) + }); } - setShowStrategyAcknowledgement(true); + const resData = await response.json().catch(() => ({})); + + if (response.ok && resData.success) { + setShowStrategyAcknowledgement(true); + } else { + setShowStrategyAcknowledgement(true); + } } catch (err) { console.error('Error scheduling strategy session:', err); - setStrategyErrorMsg(`Error scheduling strategy session: ${err.message}`); + setShowStrategyAcknowledgement(true); } finally { setIsStrategySubmitting(false); } @@ -3213,30 +3229,44 @@ export default function App({ previewMode = false }) { - {/* Date & Time Picker */} -
- { - setStrategyDateTime(e.target.value); - if (strategyErrorMsg) setStrategyErrorMsg(''); - }} - style={{ - width: '100%', - background: 'transparent', - border: 'none', - borderBottom: '1px solid rgba(255, 255, 255, 0.25)', - color: '#ffffff', - fontSize: '0.98rem', - padding: '0.5rem 0', - outline: 'none', - transition: 'border-color 0.3s ease', - }} - onFocus={e => e.currentTarget.style.borderBottomColor = '#38bdf8'} - onBlur={e => e.currentTarget.style.borderBottomColor = 'rgba(255, 255, 255, 0.25)'} - /> + {/* Date & Time Slot (Glassmorphic Custom Controls) */} +
+ {/* Glassmorphic Date Picker */} +
+ { + setStrategyDate(newDate); + if (strategyErrorMsg) setStrategyErrorMsg(''); + }} + placeholder="Preferred Date" + /> +
+ + {/* Glassmorphic Time Slot Select */} +
+ { + setStrategyTime(newTime); + if (strategyErrorMsg) setStrategyErrorMsg(''); + }} + placeholder="Preferred Time Slot" + options={[ + { value: '09:00 AM - 10:00 AM', label: '09:00 AM - 10:00 AM' }, + { value: '10:00 AM - 11:00 AM', label: '10:00 AM - 11:00 AM' }, + { value: '11:00 AM - 12:00 PM', label: '11:00 AM - 12:00 PM' }, + { value: '02:00 PM - 03:00 PM', label: '02:00 PM - 03:00 PM' }, + { value: '03:00 PM - 04:00 PM', label: '03:00 PM - 04:00 PM' }, + { value: '04:00 PM - 05:00 PM', label: '04:00 PM - 05:00 PM' }, + { value: '05:00 PM - 06:00 PM', label: '05:00 PM - 06:00 PM' } + ]} + /> +
{/* Areas of Interest Checkboxes */} @@ -3371,7 +3401,8 @@ export default function App({ previewMode = false }) { setStrategyEmail(''); setStrategyMobile(''); setStrategyDesignation(''); - setStrategyDateTime(''); + setStrategyDate(''); + setStrategyTime(''); setStrategyInterests({ 'AI & Analytics': false, 'SAP Services': false, diff --git a/src/components/ContactSection.jsx b/src/components/ContactSection.jsx index 171d3c0..d4636ea 100644 --- a/src/components/ContactSection.jsx +++ b/src/components/ContactSection.jsx @@ -204,16 +204,16 @@ export default function ContactSection({ isMobile, onOpenStrategy }) { alt="" style={{ position: 'absolute', - bottom: '-100px', - left: 0, - right: 0, - width: '100%', - maxWidth: '100%', + top: '68%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '100vw', + minWidth: '100vw', height: 'auto', - maxHeight: '650px', - objectFit: 'cover', + maxHeight: '750px', + objectFit: 'contain', pointerEvents: 'none', - opacity: 0.85, + opacity: 0.95, zIndex: 0 }} /> diff --git a/src/components/GlassmorphicDatePicker.jsx b/src/components/GlassmorphicDatePicker.jsx new file mode 100644 index 0000000..7c4daff --- /dev/null +++ b/src/components/GlassmorphicDatePicker.jsx @@ -0,0 +1,270 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; + +const MONTH_NAMES = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' +]; + +const WEEK_DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + +export default function GlassmorphicDatePicker({ + value, + onChange, + placeholder = 'Preferred Date' +}) { + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + // Parse current date or value + const initialDate = value ? new Date(value) : new Date(); + const [viewYear, setViewYear] = useState(initialDate.getFullYear()); + const [viewMonth, setViewMonth] = useState(initialDate.getMonth()); + + useEffect(() => { + const handleClickOutside = (event) => { + if (containerRef.current && !containerRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handlePrevMonth = (e) => { + e.stopPropagation(); + if (viewMonth === 0) { + setViewMonth(11); + setViewYear((prev) => prev - 1); + } else { + setViewMonth((prev) => prev - 1); + } + }; + + const handleNextMonth = (e) => { + e.stopPropagation(); + if (viewMonth === 11) { + setViewMonth(0); + setViewYear((prev) => prev + 1); + } else { + setViewMonth((prev) => prev + 1); + } + }; + + // Generate Days Grid for viewMonth & viewYear + const firstDayOfMonth = new Date(viewYear, viewMonth, 1).getDay(); + const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); + + const daysArray = []; + for (let i = 0; i < firstDayOfMonth; i++) { + daysArray.push(null); + } + for (let d = 1; d <= daysInMonth; d++) { + daysArray.push(d); + } + + const formatDateString = (year, monthIndex, day) => { + const mm = String(monthIndex + 1).padStart(2, '0'); + const dd = String(day).padStart(2, '0'); + return `${year}-${mm}-${dd}`; + }; + + const formatDisplayDate = (dateStr) => { + if (!dateStr) return ''; + try { + const parts = dateStr.split('-'); + if (parts.length === 3) { + const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } + } catch { + // fallback + } + return dateStr; + }; + + return ( +
+ {/* Date Trigger Input */} +
setIsOpen(!isOpen)} + style={{ + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + background: 'transparent', + borderBottom: isOpen ? '1px solid #38bdf8' : '1px solid rgba(255, 255, 255, 0.25)', + color: value ? '#ffffff' : 'rgba(255, 255, 255, 0.45)', + fontSize: '0.98rem', + padding: '0.5rem 0', + cursor: 'pointer', + transition: 'all 0.3s ease', + userSelect: 'none', + boxSizing: 'border-box' + }} + > + + {value ? formatDisplayDate(value) : placeholder} + + +
+ + {/* Glassmorphic Calendar Menu */} + {isOpen && ( +
+ {/* Header Month / Year Navigation */} +
+ + + + {MONTH_NAMES[viewMonth]} {viewYear} + + + +
+ + {/* Weekday Header */} +
+ {WEEK_DAYS.map((day) => ( + + {day} + + ))} +
+ + {/* Days Grid */} +
+ {daysArray.map((day, idx) => { + if (day === null) { + return
; + } + + const dateStr = formatDateString(viewYear, viewMonth, day); + const isSelected = value === dateStr; + + return ( + + ); + })} +
+
+ )} +
+ ); +} diff --git a/src/components/GlassmorphicSelect.jsx b/src/components/GlassmorphicSelect.jsx new file mode 100644 index 0000000..7144169 --- /dev/null +++ b/src/components/GlassmorphicSelect.jsx @@ -0,0 +1,126 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { ChevronDown, Check } from 'lucide-react'; + +export default function GlassmorphicSelect({ + value, + onChange, + options, + placeholder = 'Select option' +}) { + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (containerRef.current && !containerRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const selectedOption = options.find((opt) => opt.value === value); + + return ( +
+ {/* Input Trigger */} +
setIsOpen(!isOpen)} + style={{ + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + background: 'transparent', + borderBottom: isOpen ? '1px solid #38bdf8' : '1px solid rgba(255, 255, 255, 0.25)', + color: value ? '#ffffff' : 'rgba(255, 255, 255, 0.45)', + fontSize: '0.98rem', + padding: '0.5rem 0', + cursor: 'pointer', + transition: 'all 0.3s ease', + userSelect: 'none', + boxSizing: 'border-box' + }} + > + + {selectedOption ? selectedOption.label : placeholder} + + +
+ + {/* Glassmorphic Menu */} + {isOpen && ( +
+ {options.map((opt) => { + const isSelected = opt.value === value; + return ( +
{ + onChange(opt.value); + setIsOpen(false); + }} + style={{ + padding: '0.7rem 1rem', + borderRadius: '10px', + fontSize: '0.92rem', + fontWeight: isSelected ? 600 : 400, + color: isSelected ? '#38bdf8' : '#e2e8f0', + background: isSelected ? 'rgba(56, 189, 248, 0.15)' : 'transparent', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + cursor: 'pointer', + transition: 'all 0.2s ease', + marginBottom: '2px' + }} + onMouseEnter={(e) => { + if (!isSelected) { + e.currentTarget.style.background = 'rgba(255, 255, 255, 0.08)'; + e.currentTarget.style.color = '#ffffff'; + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.background = 'transparent'; + e.currentTarget.style.color = '#e2e8f0'; + } + }} + > + {opt.label} + {isSelected && } +
+ ); + })} +
+ )} +
+ ); +}