Integrate Glassmorphic selectors, update API submission endpoints and adjust scroll spy settings

This commit is contained in:
Vishva
2026-07-29 17:32:55 +05:30
parent abd344cad0
commit 39dfd32d7d
4 changed files with 520 additions and 93 deletions
+116 -85
View File
@@ -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 }) {
<form
onSubmit={async (e) => {
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 }) {
</div>
</div>
{/* Date & Time Picker */}
<div style={{ position: 'relative' }}>
<input
type="datetime-local"
required
value={strategyDateTime}
onChange={e => {
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) */}
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr',
gap: '1.5rem'
}}>
{/* Glassmorphic Date Picker */}
<div style={{ position: 'relative' }}>
<GlassmorphicDatePicker
value={strategyDate}
onChange={(newDate) => {
setStrategyDate(newDate);
if (strategyErrorMsg) setStrategyErrorMsg('');
}}
placeholder="Preferred Date"
/>
</div>
{/* Glassmorphic Time Slot Select */}
<div style={{ position: 'relative' }}>
<GlassmorphicSelect
value={strategyTime}
onChange={(newTime) => {
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' }
]}
/>
</div>
</div>
{/* 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,
+8 -8
View File
@@ -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
}}
/>
+270
View File
@@ -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 (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
{/* Date Trigger Input */}
<div
onClick={() => 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'
}}
>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{value ? formatDisplayDate(value) : placeholder}
</span>
<Calendar
size={18}
style={{
color: isOpen ? '#38bdf8' : '#94a3b8',
transition: 'color 0.3s ease',
flexShrink: 0,
marginLeft: '0.5rem'
}}
/>
</div>
{/* Glassmorphic Calendar Menu */}
{isOpen && (
<div
style={{
position: 'absolute',
top: 'calc(100% + 8px)',
left: 0,
zIndex: 999,
width: '280px',
background: 'rgba(7, 16, 32, 0.96)',
backdropFilter: 'blur(24px)',
WebkitBackdropFilter: 'blur(24px)',
border: '1px solid rgba(56, 189, 248, 0.35)',
borderRadius: '18px',
boxShadow: '0 20px 50px rgba(0, 0, 0, 0.85), 0 0 30px rgba(56, 189, 248, 0.15)',
padding: '1.25rem',
userSelect: 'none'
}}
>
{/* Header Month / Year Navigation */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '1rem'
}}>
<button
type="button"
onClick={handlePrevMonth}
style={{
background: 'rgba(255, 255, 255, 0.05)',
border: 'none',
borderRadius: '8px',
color: '#e2e8f0',
padding: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(56, 189, 248, 0.2)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
>
<ChevronLeft size={18} />
</button>
<span style={{ fontWeight: 700, fontSize: '0.95rem', color: '#ffffff' }}>
{MONTH_NAMES[viewMonth]} {viewYear}
</span>
<button
type="button"
onClick={handleNextMonth}
style={{
background: 'rgba(255, 255, 255, 0.05)',
border: 'none',
borderRadius: '8px',
color: '#e2e8f0',
padding: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(56, 189, 248, 0.2)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'rgba(255, 255, 255, 0.05)'}
>
<ChevronRight size={18} />
</button>
</div>
{/* Weekday Header */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '4px',
textAlign: 'center',
marginBottom: '0.5rem'
}}>
{WEEK_DAYS.map((day) => (
<span key={day} style={{ fontSize: '0.75rem', fontWeight: 600, color: '#94a3b8' }}>
{day}
</span>
))}
</div>
{/* Days Grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: '4px'
}}>
{daysArray.map((day, idx) => {
if (day === null) {
return <div key={`empty-${idx}`} />;
}
const dateStr = formatDateString(viewYear, viewMonth, day);
const isSelected = value === dateStr;
return (
<button
key={day}
type="button"
onClick={() => {
onChange(dateStr);
setIsOpen(false);
}}
style={{
height: '32px',
width: '32px',
margin: '0 auto',
borderRadius: '8px',
border: 'none',
background: isSelected ? '#38bdf8' : 'transparent',
color: isSelected ? '#020710' : '#ffffff',
fontWeight: isSelected ? 700 : 500,
fontSize: '0.85rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
boxShadow: isSelected ? '0 0 12px rgba(56, 189, 248, 0.5)' : 'none'
}}
onMouseEnter={(e) => {
if (!isSelected) {
e.currentTarget.style.background = 'rgba(56, 189, 248, 0.15)';
e.currentTarget.style.color = '#38bdf8';
}
}}
onMouseLeave={(e) => {
if (!isSelected) {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#ffffff';
}
}}
>
{day}
</button>
);
})}
</div>
</div>
)}
</div>
);
}
+126
View File
@@ -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 (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
{/* Input Trigger */}
<div
onClick={() => 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'
}}
>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{selectedOption ? selectedOption.label : placeholder}
</span>
<ChevronDown
size={18}
style={{
color: isOpen ? '#38bdf8' : '#94a3b8',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.3s ease, color 0.3s ease',
flexShrink: 0,
marginLeft: '0.5rem'
}}
/>
</div>
{/* Glassmorphic Menu */}
{isOpen && (
<div
style={{
position: 'absolute',
top: 'calc(100% + 8px)',
left: 0,
right: 0,
zIndex: 999,
background: 'rgba(7, 16, 32, 0.95)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
border: '1px solid rgba(56, 189, 248, 0.3)',
borderRadius: '16px',
boxShadow: '0 20px 45px rgba(0, 0, 0, 0.8), 0 0 25px rgba(56, 189, 248, 0.15)',
padding: '0.5rem',
maxHeight: '250px',
overflowY: 'auto'
}}
>
{options.map((opt) => {
const isSelected = opt.value === value;
return (
<div
key={opt.value}
onClick={() => {
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';
}
}}
>
<span>{opt.label}</span>
{isSelected && <Check size={16} style={{ color: '#38bdf8' }} />}
</div>
);
})}
</div>
)}
</div>
);
}