feat: Containerize services with Docker Compose (PostgreSQL, Strapi CMS, Express Backend, Nginx Frontend) on custom ports >9000
This commit is contained in:
+96
-14
@@ -12,6 +12,9 @@ import AboutUsPage from './components/AboutUsPage';
|
||||
import TurnstileCaptcha from './components/TurnstileCaptcha';
|
||||
import GlassmorphicSelect from './components/GlassmorphicSelect';
|
||||
import GlassmorphicDatePicker from './components/GlassmorphicDatePicker';
|
||||
import BlogDetailPage from './components/BlogDetailPage';
|
||||
import CareersPage from './components/CareersPage';
|
||||
import BlogsPage from './components/BlogsPage';
|
||||
|
||||
// Reusable Scroll Reveal Wrapper with Reduced Motion Support
|
||||
function Reveal({ children, delay = 0, style }) {
|
||||
@@ -405,24 +408,60 @@ export default function App({ previewMode = false }) {
|
||||
scrollTimeout = setTimeout(handleScrollEnd, 800);
|
||||
};
|
||||
|
||||
const [selectedBlog, setSelectedBlog] = useState(null);
|
||||
|
||||
const handleSelectBlog = (blog) => {
|
||||
setNavbarVisible(true);
|
||||
setMobileMenuOpen(false);
|
||||
setSelectedBlog(blog);
|
||||
setCurrentPage('blog');
|
||||
window.location.hash = blog?.slug ? `blog-${blog.slug}` : 'blog';
|
||||
smoothScrollTo(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const syncPageFromHash = () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash === '#contact' || hash === '#contactus') {
|
||||
const syncPageFromHash = (shouldScroll = false) => {
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (hash === 'contact' || hash === 'contactus') {
|
||||
setSelectedBlog(null);
|
||||
setCurrentPage('contact');
|
||||
setActiveSection('contactus');
|
||||
smoothScrollTo(0);
|
||||
} else if (hash === '#about' || hash === '#aboutus') {
|
||||
if (shouldScroll) smoothScrollTo(0);
|
||||
} else if (hash === 'about' || hash === 'aboutus') {
|
||||
setSelectedBlog(null);
|
||||
setCurrentPage('about');
|
||||
setActiveSection('aboutus');
|
||||
smoothScrollTo(0);
|
||||
} else if (hash === '#home' || hash === '') {
|
||||
if (shouldScroll) smoothScrollTo(0);
|
||||
} else if (hash === 'careers') {
|
||||
setSelectedBlog(null);
|
||||
setCurrentPage('careers');
|
||||
setActiveSection('careers');
|
||||
if (shouldScroll) smoothScrollTo(0);
|
||||
} else if (hash === 'blogs') {
|
||||
setSelectedBlog(null);
|
||||
setCurrentPage('blogs');
|
||||
setActiveSection('blogs');
|
||||
if (shouldScroll) smoothScrollTo(0);
|
||||
} else if (hash.startsWith('blog-') || hash === 'blog') {
|
||||
const slug = hash.replace('blog-', '');
|
||||
const found = insights.find(b => b.slug === slug || String(b.id) === slug);
|
||||
if (found) {
|
||||
setSelectedBlog(prev => (prev && (prev.id === found.id || (prev.slug && prev.slug === found.slug)) ? prev : found));
|
||||
} else if (insights && insights.length > 0) {
|
||||
setSelectedBlog(prev => prev || insights[0]);
|
||||
}
|
||||
setCurrentPage('blog');
|
||||
if (shouldScroll) smoothScrollTo(0);
|
||||
} else if (hash === 'home' || hash === '') {
|
||||
setSelectedBlog(null);
|
||||
setCurrentPage('home');
|
||||
}
|
||||
};
|
||||
window.addEventListener('hashchange', syncPageFromHash);
|
||||
return () => window.removeEventListener('hashchange', syncPageFromHash);
|
||||
}, []);
|
||||
syncPageFromHash(false);
|
||||
const handleHashChange = () => syncPageFromHash(true);
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
}, [insights]);
|
||||
const [hoveredItem, setHoveredItem] = useState(null);
|
||||
const [showCareersAcknowledgement, setShowCareersAcknowledgement] = useState(false);
|
||||
const [isStrategyOpen, setIsStrategyOpen] = useState(false);
|
||||
@@ -539,6 +578,7 @@ export default function App({ previewMode = false }) {
|
||||
}, [previewMode, currentPage]);
|
||||
|
||||
const getActiveNavItem = () => {
|
||||
if (currentPage === 'blogs' || currentPage === 'blog') return 'Blogs';
|
||||
if (currentPage === 'contact') return 'Contact Us';
|
||||
if (currentPage === 'about') return 'About Us';
|
||||
switch (activeSection) {
|
||||
@@ -549,8 +589,10 @@ export default function App({ previewMode = false }) {
|
||||
case 'whyus':
|
||||
return 'About Us';
|
||||
case 'products':
|
||||
case 'insights':
|
||||
return 'Products';
|
||||
case 'insights':
|
||||
case 'blogs':
|
||||
return 'Blogs';
|
||||
case 'contactus':
|
||||
return 'Contact Us';
|
||||
default:
|
||||
@@ -582,7 +624,7 @@ export default function App({ previewMode = false }) {
|
||||
};
|
||||
|
||||
const sectionX = isMobile ? '1.25rem' : '4rem';
|
||||
const navItems = ['Home', 'About Us', 'Products', 'Careers', 'Contact Us'];
|
||||
const navItems = ['Home', 'About Us', 'Products', 'Blogs', 'Careers', 'Contact Us'];
|
||||
const NAV_OFFSET = isMobile ? 72 : 88;
|
||||
|
||||
const scrollToSection = (sectionId) => {
|
||||
@@ -600,11 +642,21 @@ export default function App({ previewMode = false }) {
|
||||
const handleNavClick = (item) => {
|
||||
setNavbarVisible(true);
|
||||
setMobileMenuOpen(false);
|
||||
setSelectedBlog(null);
|
||||
|
||||
if (item === 'Careers') {
|
||||
setIsCareersOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item === 'Blogs') {
|
||||
setCurrentPage('blogs');
|
||||
setActiveSection('blogs');
|
||||
window.location.hash = 'blogs';
|
||||
smoothScrollTo(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item === 'Contact Us') {
|
||||
setCurrentPage('contact');
|
||||
setActiveSection('contactus');
|
||||
@@ -645,10 +697,12 @@ export default function App({ previewMode = false }) {
|
||||
if (currentPage !== 'home') {
|
||||
setCurrentPage('home');
|
||||
setActiveSection(id || 'home');
|
||||
window.location.hash = id || 'home';
|
||||
setTimeout(() => doScroll(id), 350);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.hash = id || 'home';
|
||||
doScroll(id);
|
||||
};
|
||||
|
||||
@@ -891,6 +945,12 @@ export default function App({ previewMode = false }) {
|
||||
<ContactSection isMobile={isMobile} onOpenStrategy={() => setIsStrategyOpen(true)} />
|
||||
) : currentPage === 'about' ? (
|
||||
<AboutUsPage isMobile={isMobile} onOpenStrategy={() => setIsStrategyOpen(true)} onNavigate={handleNavClick} />
|
||||
) : currentPage === 'careers' ? (
|
||||
<CareersPage isMobile={isMobile} onNavigate={handleNavClick} onOpenStrategy={() => setIsStrategyOpen(true)} />
|
||||
) : currentPage === 'blogs' ? (
|
||||
<BlogsPage blogs={insights} isMobile={isMobile} onSelectBlog={handleSelectBlog} onNavigate={handleNavClick} />
|
||||
) : currentPage === 'blog' ? (
|
||||
<BlogDetailPage blog={selectedBlog || insights[0]} allBlogs={insights} isMobile={isMobile} onNavigate={handleNavClick} onSelectBlog={handleSelectBlog} />
|
||||
) : (
|
||||
<>
|
||||
{/* Main Homepage Content */}
|
||||
@@ -1956,8 +2016,29 @@ export default function App({ previewMode = false }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Nav Arrows */}
|
||||
<div style={{ display: 'flex', gap: '0.6rem', alignItems: 'center', flexShrink: 0, marginTop: '0.5rem' }}>
|
||||
{/* Nav Arrows & View All Blogs */}
|
||||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexShrink: 0, marginTop: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
onClick={() => handleNavClick('Blogs')}
|
||||
style={{
|
||||
padding: '0.55rem 1.25rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(56,189,248,0.12)',
|
||||
border: '1px solid rgba(56,189,248,0.3)',
|
||||
color: '#38bdf8',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.85rem',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.4rem',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.background = '#38bdf8'; e.currentTarget.style.color = '#000'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.background = 'rgba(56,189,248,0.12)'; e.currentTarget.style.color = '#38bdf8'; }}
|
||||
>
|
||||
View All Blogs <ArrowRight size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scrollBlogCarousel(-1)}
|
||||
style={{
|
||||
@@ -2022,6 +2103,7 @@ export default function App({ previewMode = false }) {
|
||||
>
|
||||
<div
|
||||
className="blog-card"
|
||||
onClick={() => handleSelectBlog(insight)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Search, Calendar, Clock, ArrowRight, Tag, Sparkles, BookOpen, User, CheckCircle2, Send } from 'lucide-react';
|
||||
import { assetUrl } from '../lib/assetUrl';
|
||||
|
||||
export default function BlogsPage({ blogs = [], isMobile, onSelectBlog, onNavigate }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [newsletterEmail, setNewsletterEmail] = useState('');
|
||||
const [newsletterSubmitted, setNewsletterSubmitted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
// Extract unique categories from blogs
|
||||
const categories = useMemo(() => {
|
||||
const set = new Set(['All']);
|
||||
blogs.forEach((b) => {
|
||||
if (b.category) set.add(b.category);
|
||||
});
|
||||
return Array.from(set);
|
||||
}, [blogs]);
|
||||
|
||||
// Filter blogs based on search query and category
|
||||
const filteredBlogs = useMemo(() => {
|
||||
return blogs.filter((blog) => {
|
||||
const matchesCategory = selectedCategory === 'All' || blog.category === selectedCategory;
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
const matchesSearch =
|
||||
!query ||
|
||||
blog.title?.toLowerCase().includes(query) ||
|
||||
blog.excerpt?.toLowerCase().includes(query) ||
|
||||
blog.category?.toLowerCase().includes(query) ||
|
||||
blog.author?.toLowerCase().includes(query) ||
|
||||
blog.tags?.some((t) => t.toLowerCase().includes(query));
|
||||
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
}, [blogs, selectedCategory, searchQuery]);
|
||||
|
||||
// Featured blog (first filtered item or first overall)
|
||||
const featuredBlog = filteredBlogs.length > 0 ? filteredBlogs[0] : null;
|
||||
// Remaining blogs for grid
|
||||
const gridBlogs = filteredBlogs.length > 0 ? filteredBlogs.slice(1) : [];
|
||||
|
||||
const handleNewsletterSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (newsletterEmail) {
|
||||
setNewsletterSubmitted(true);
|
||||
setTimeout(() => {
|
||||
setNewsletterSubmitted(false);
|
||||
setNewsletterEmail('');
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
const bgPatternUrl = assetUrl('/assets/Background pattern About Us Hero section.svg');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#020611',
|
||||
color: '#ffffff',
|
||||
minHeight: '100vh',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
paddingTop: isMobile ? '5.5rem' : '7.5rem',
|
||||
paddingBottom: '6rem',
|
||||
}}
|
||||
>
|
||||
{/* Background Grid Overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: `url(${bgPatternUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
opacity: 0.15,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top Ambient Blue Glow */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-100px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '900px',
|
||||
height: '450px',
|
||||
background: 'radial-gradient(circle, rgba(56, 189, 248, 0.18) 0%, rgba(14, 165, 233, 0) 70%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header Hero Section - Full Viewport Width */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
marginBottom: '3rem',
|
||||
padding: isMobile ? '1rem 1.25rem 2rem' : '2.5rem 2rem 3rem',
|
||||
overflow: 'visible',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{/* Wave Pattern 2 Background - Unclipped full SVG shifted upwards */}
|
||||
<img
|
||||
src={assetUrl('/assets/Wave Pattern 2.svg')}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: isMobile ? '-140px' : '-240px',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
minHeight: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center top',
|
||||
opacity: 0.45,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Hero Text Content */}
|
||||
<div style={{ position: 'relative', zIndex: 1, maxWidth: '820px', margin: '0 auto', textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.4rem 1.1rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(56, 189, 248, 0.1)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.28)',
|
||||
color: '#38bdf8',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
marginBottom: '1.25rem',
|
||||
}}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
<span>Tech Insights & Publications</span>
|
||||
</div>
|
||||
|
||||
<h1
|
||||
style={{
|
||||
fontSize: isMobile ? '2.2rem' : '3.4rem',
|
||||
fontWeight: 800,
|
||||
lineHeight: 1.15,
|
||||
marginBottom: '1.2rem',
|
||||
letterSpacing: '-0.02em',
|
||||
color: '#ffffff',
|
||||
}}
|
||||
>
|
||||
Engineering Intelligence,{' '}
|
||||
<span
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #38bdf8 0%, #0ea5e9 50%, #0284c7 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
Industry Insights
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: isMobile ? '1rem' : '1.15rem',
|
||||
color: '#94a3b8',
|
||||
lineHeight: 1.6,
|
||||
margin: '0 auto 2rem',
|
||||
}}
|
||||
>
|
||||
Explore thought leadership, architectural breakdowns, AI case studies, and engineering updates from our tech leaders.
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
maxWidth: '560px',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
<Search
|
||||
size={20}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '1.2rem',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
color: '#38bdf8',
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search blogs, topics, or authors..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.95rem 1.2rem 0.95rem 3.2rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(15, 23, 42, 0.75)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.2)',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.95rem',
|
||||
outline: 'none',
|
||||
backdropFilter: 'blur(12px)',
|
||||
transition: 'all 0.3s ease',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#38bdf8';
|
||||
e.target.style.boxShadow = '0 0 20px rgba(56, 189, 248, 0.25)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = 'rgba(56, 189, 248, 0.2)';
|
||||
e.target.style.boxShadow = 'none';
|
||||
}}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '1.2rem',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#38bdf8',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ maxWidth: '1320px', margin: '0 auto', padding: isMobile ? '0 1.25rem' : '0 2.5rem', position: 'relative', zIndex: 1 }}>
|
||||
|
||||
{/* Category Filter Pills */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.6rem',
|
||||
overflowX: 'auto',
|
||||
paddingBottom: '1rem',
|
||||
marginBottom: '3rem',
|
||||
scrollbarWidth: 'none',
|
||||
justifyContent: isMobile ? 'flex-start' : 'center',
|
||||
}}
|
||||
>
|
||||
{categories.map((cat) => {
|
||||
const isSelected = selectedCategory === cat;
|
||||
return (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
style={{
|
||||
padding: '0.55rem 1.25rem',
|
||||
borderRadius: '50px',
|
||||
border: isSelected ? '1px solid #38bdf8' : '1px solid rgba(255, 255, 255, 0.1)',
|
||||
background: isSelected ? 'rgba(56, 189, 248, 0.18)' : 'rgba(15, 23, 42, 0.5)',
|
||||
color: isSelected ? '#38bdf8' : '#94a3b8',
|
||||
fontSize: '0.88rem',
|
||||
fontWeight: isSelected ? 600 : 500,
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.25s ease',
|
||||
backdropFilter: 'blur(8px)',
|
||||
boxShadow: isSelected ? '0 0 15px rgba(56, 189, 248, 0.2)' : 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected) {
|
||||
e.currentTarget.style.borderColor = 'rgba(56, 189, 248, 0.4)';
|
||||
e.currentTarget.style.color = '#ffffff';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected) {
|
||||
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.1)';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Featured Blog */}
|
||||
{featuredBlog && (
|
||||
<div style={{ marginBottom: '3.5rem' }}>
|
||||
<div
|
||||
onClick={() => onSelectBlog(featuredBlog)}
|
||||
style={{
|
||||
borderRadius: '24px',
|
||||
background: 'rgba(15, 23, 42, 0.65)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.2)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '1.1fr 1fr',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-4px)';
|
||||
e.currentTarget.style.borderColor = 'rgba(56, 189, 248, 0.5)';
|
||||
e.currentTarget.style.boxShadow = '0 12px 40px rgba(14, 165, 233, 0.15)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.borderColor = 'rgba(56, 189, 248, 0.2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: isMobile ? '240px' : '380px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={assetUrl(featuredBlog.image)}
|
||||
alt={featuredBlog.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '1.25rem',
|
||||
left: '1.25rem',
|
||||
padding: '0.35rem 0.9rem',
|
||||
borderRadius: '50px',
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)',
|
||||
color: '#ffffff',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.75rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
boxShadow: '0 4px 15px rgba(14, 165, 233, 0.4)',
|
||||
}}
|
||||
>
|
||||
Featured Story
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
<div
|
||||
style={{
|
||||
padding: isMobile ? '1.5rem' : '2.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '0.8rem' }}>
|
||||
<span style={{ fontSize: '0.8rem', color: '#38bdf8', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
{featuredBlog.category}
|
||||
</span>
|
||||
<span style={{ color: '#475569' }}>•</span>
|
||||
{featuredBlog.date && (
|
||||
<span style={{ fontSize: '0.82rem', color: '#94a3b8', display: 'flex', alignItems: 'center', gap: '0.3rem' }}>
|
||||
<Calendar size={14} style={{ color: '#38bdf8' }} />
|
||||
{featuredBlog.date}
|
||||
</span>
|
||||
)}
|
||||
{featuredBlog.readTime && (
|
||||
<span style={{ fontSize: '0.82rem', color: '#94a3b8', display: 'flex', alignItems: 'center', gap: '0.3rem' }}>
|
||||
<Clock size={14} style={{ color: '#38bdf8' }} />
|
||||
{featuredBlog.readTime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2
|
||||
style={{
|
||||
fontSize: isMobile ? '1.4rem' : '1.9rem',
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1.3,
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{featuredBlog.title}
|
||||
</h2>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: '0.95rem',
|
||||
color: '#cbd5e1',
|
||||
lineHeight: 1.6,
|
||||
marginBottom: '1.8rem',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{featuredBlog.excerpt || featuredBlog.summary}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 'auto', paddingTop: '1rem', borderTop: '1px solid rgba(255, 255, 255, 0.08)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '38px',
|
||||
height: '38px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #38bdf8 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#ffffff',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
boxShadow: '0 2px 10px rgba(14, 165, 233, 0.3)',
|
||||
}}
|
||||
>
|
||||
{featuredBlog.author ? featuredBlog.author[0] : 'C'}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.88rem', fontWeight: 600, color: '#ffffff' }}>
|
||||
{featuredBlog.author || 'Cavin Editorial'}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', color: '#64748b' }}>
|
||||
{featuredBlog.authorDesignation || 'Technology Team'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
color: '#38bdf8',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
Read Article <ArrowRight size={18} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Blog Grid Section */}
|
||||
{gridBlogs.length > 0 && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: '1.5rem', color: '#ffffff' }}>
|
||||
All Articles ({filteredBlogs.length})
|
||||
</h3>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fill, minmax(350px, 1fr))',
|
||||
gap: '1.8rem',
|
||||
}}
|
||||
>
|
||||
{gridBlogs.map((blog) => (
|
||||
<div
|
||||
key={blog.id || blog.slug}
|
||||
onClick={() => onSelectBlog(blog)}
|
||||
style={{
|
||||
borderRadius: '16px',
|
||||
background: 'rgba(15, 23, 42, 0.55)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-6px)';
|
||||
e.currentTarget.style.borderColor = 'rgba(56, 189, 248, 0.4)';
|
||||
e.currentTarget.style.boxShadow = '0 12px 30px rgba(14, 165, 233, 0.12)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.08)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
{/* Card Image */}
|
||||
<div style={{ position: 'relative', height: '200px', overflow: 'hidden' }}>
|
||||
<img
|
||||
src={assetUrl(blog.image)}
|
||||
alt={blog.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '0.85rem',
|
||||
left: '0.85rem',
|
||||
padding: '0.25rem 0.75rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(2, 6, 17, 0.8)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.3)',
|
||||
color: '#38bdf8',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 600,
|
||||
backdropFilter: 'blur(6px)',
|
||||
}}
|
||||
>
|
||||
{blog.category}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<div style={{ padding: '1.4rem', display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.8rem', fontSize: '0.78rem', color: '#64748b', marginBottom: '0.6rem' }}>
|
||||
{blog.date && (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<Calendar size={12} style={{ color: '#38bdf8' }} />
|
||||
{blog.date}
|
||||
</span>
|
||||
)}
|
||||
{blog.readTime && (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<Clock size={12} style={{ color: '#38bdf8' }} />
|
||||
{blog.readTime}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3
|
||||
style={{
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
lineHeight: 1.35,
|
||||
marginBottom: '0.65rem',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{blog.title}
|
||||
</h3>
|
||||
|
||||
<p
|
||||
style={{
|
||||
fontSize: '0.88rem',
|
||||
color: '#94a3b8',
|
||||
lineHeight: 1.55,
|
||||
marginBottom: '1.25rem',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{blog.excerpt || blog.summary}
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: '0.85rem',
|
||||
borderTop: '1px solid rgba(255, 255, 255, 0.06)',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '0.8rem', color: '#cbd5e1', fontWeight: 500 }}>
|
||||
{blog.author || 'Cavin Tech Team'}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.82rem', color: '#38bdf8', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
Read <ArrowRight size={14} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredBlogs.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '5rem 2rem',
|
||||
background: 'rgba(15, 23, 42, 0.4)',
|
||||
borderRadius: '24px',
|
||||
border: '1px dashed rgba(56, 189, 248, 0.2)',
|
||||
}}
|
||||
>
|
||||
<BookOpen size={48} style={{ color: '#38bdf8', marginBottom: '1rem', opacity: 0.8 }} />
|
||||
<h3 style={{ fontSize: '1.4rem', fontWeight: 700, color: '#ffffff', marginBottom: '0.5rem' }}>
|
||||
No Articles Found
|
||||
</h3>
|
||||
<p style={{ color: '#64748b', maxWidth: '440px', margin: '0 auto 1.5rem' }}>
|
||||
We couldn't find any blog posts matching "{searchQuery}" under {selectedCategory}. Try resetting your search or filter.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedCategory('All');
|
||||
}}
|
||||
style={{
|
||||
padding: '0.65rem 1.5rem',
|
||||
borderRadius: '50px',
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
|
||||
color: '#ffffff',
|
||||
fontWeight: 700,
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 15px rgba(14, 165, 233, 0.3)',
|
||||
}}
|
||||
>
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Newsletter Subscription Banner */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: '5rem',
|
||||
borderRadius: '24px',
|
||||
background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(2, 12, 30, 0.95) 100%)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.25)',
|
||||
padding: isMobile ? '2rem 1.5rem' : '3.5rem 3rem',
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '2rem',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 30px rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '560px' }}>
|
||||
<h3 style={{ fontSize: isMobile ? '1.5rem' : '1.9rem', fontWeight: 700, color: '#ffffff', marginBottom: '0.6rem' }}>
|
||||
Stay Ahead of the Tech Curve
|
||||
</h3>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.95rem', lineHeight: 1.6 }}>
|
||||
Subscribe to our monthly digest to receive deep dives on Agentic AI, Cloud Architectures, and IIoT directly in your inbox.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleNewsletterSubmit} style={{ width: isMobile ? '100%' : 'auto', flexShrink: 0 }}>
|
||||
{newsletterSubmitted ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.85rem 1.5rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(56, 189, 248, 0.15)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.35)',
|
||||
color: '#38bdf8',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<CheckCircle2 size={18} />
|
||||
Subscribed successfully!
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: '0.75rem', width: '100%' }}>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Enter your work email..."
|
||||
value={newsletterEmail}
|
||||
onChange={(e) => setNewsletterEmail(e.target.value)}
|
||||
style={{
|
||||
padding: '0.85rem 1.25rem',
|
||||
borderRadius: '50px',
|
||||
background: 'rgba(255, 255, 255, 0.06)',
|
||||
border: '1px solid rgba(56, 189, 248, 0.2)',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
minWidth: isMobile ? '100%' : '260px',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#38bdf8';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = 'rgba(56, 189, 248, 0.2)';
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '0.85rem 1.75rem',
|
||||
borderRadius: '50px',
|
||||
background: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
|
||||
color: '#ffffff',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.9rem',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.5rem',
|
||||
boxShadow: '0 4px 20px rgba(14, 165, 233, 0.35)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, #38bdf8 0%, #0ea5e9 100%)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)';
|
||||
}}
|
||||
>
|
||||
Subscribe <Send size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ArrowRight, CheckCircle2, Upload, Briefcase, Sparkles, Shield, Cpu, Users } from 'lucide-react';
|
||||
import { assetUrl } from '../lib/assetUrl';
|
||||
import { API_BASE } from '../lib/apiBase';
|
||||
import TurnstileCaptcha from './TurnstileCaptcha';
|
||||
|
||||
export default function CareersPage({ isMobile, onNavigate, onOpenStrategy }) {
|
||||
const [careersName, setCareersName] = useState('');
|
||||
const [careersEmail, setCareersEmail] = useState('');
|
||||
const [careersPhone, setCareersPhone] = useState('');
|
||||
const [careersPosition, setCareersPosition] = useState('Lead AI & Data Architect');
|
||||
const [careersFile, setCareersFile] = useState(null);
|
||||
const [careersTurnstileToken, setCareersTurnstileToken] = useState('');
|
||||
const [careersErrorMsg, setCareersErrorMsg] = useState('');
|
||||
const [isCareersSubmitting, setIsCareersSubmitting] = useState(false);
|
||||
const [showAcknowledgement, setShowAcknowledgement] = useState(false);
|
||||
|
||||
const openPositions = [
|
||||
{ title: 'Lead AI & Data Architect', department: 'AI & Analytics', type: 'Full-Time', location: 'Hybrid / Chennai' },
|
||||
{ title: 'Senior IIoT & Smart Factory Specialist', department: 'Industrial IoT', type: 'Full-Time', location: 'On-Site / Chennai' },
|
||||
{ title: 'Spatial Computing & MR UX Designer', department: 'AR/VR & Immersive Tech', type: 'Full-Time', location: 'Hybrid' },
|
||||
{ title: 'Cloud Infrastructure & DevOps Engineer', department: 'Enterprise Cloud', type: 'Full-Time', location: 'Remote / Hybrid' },
|
||||
];
|
||||
|
||||
const handleCareersSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!careersTurnstileToken) {
|
||||
setCareersErrorMsg('Please complete the Turnstile verification.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCareersSubmitting(true);
|
||||
setCareersErrorMsg('');
|
||||
|
||||
try {
|
||||
let uploadedResumeUrl = null;
|
||||
if (careersFile) {
|
||||
const fileFormData = new FormData();
|
||||
fileFormData.append('resume', careersFile);
|
||||
const uploadRes = await fetch(`${API_BASE}/api/upload/resume`, {
|
||||
method: 'POST',
|
||||
body: fileFormData,
|
||||
});
|
||||
|
||||
if (uploadRes.ok) {
|
||||
const uploadData = await uploadRes.json();
|
||||
uploadedResumeUrl = uploadData.url;
|
||||
}
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', careersName);
|
||||
formData.append('email', careersEmail);
|
||||
formData.append('phone', careersPhone);
|
||||
formData.append('position', careersPosition);
|
||||
formData.append('turnstileToken', careersTurnstileToken);
|
||||
if (careersFile) formData.append('resume', careersFile);
|
||||
if (uploadedResumeUrl) formData.append('resumeUrl', uploadedResumeUrl);
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/contact/careers`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAcknowledgement(true);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setCareersErrorMsg(data.error || 'Failed to submit application. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setCareersErrorMsg('Server error. Please check connection and try again.');
|
||||
} finally {
|
||||
setIsCareersSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ background: '#020c1e', color: '#ffffff', minHeight: '100vh', paddingTop: isMobile ? '5rem' : '7rem', paddingBottom: '6rem' }}>
|
||||
<div style={{ maxWidth: '1280px', margin: '0 auto', padding: isMobile ? '0 1.25rem' : '0 3rem' }}>
|
||||
|
||||
{/* Breadcrumb Navigation */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.85rem', color: '#64748b', marginBottom: '2rem' }}>
|
||||
<span style={{ cursor: 'pointer', transition: 'color 0.2s' }} onClick={() => onNavigate('Home')}>Home</span>
|
||||
<span>/</span>
|
||||
<span style={{ color: '#ffaa00', fontWeight: 600 }}>Careers</span>
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
borderRadius: '24px',
|
||||
background: 'linear-gradient(135deg, rgba(8, 20, 44, 0.9) 0%, rgba(3, 11, 26, 0.95) 100%)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
padding: isMobile ? '2.5rem 1.5rem' : '4rem 3.5rem',
|
||||
overflow: 'hidden',
|
||||
marginBottom: '4rem',
|
||||
boxShadow: '0 20px 50px rgba(0,0,0,0.5)'
|
||||
}}>
|
||||
{/* Subtle Ambient Background */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '-30%', right: '-10%', width: '500px', height: '500px',
|
||||
background: 'radial-gradient(circle, rgba(255, 170, 0, 0.12) 0%, transparent 70%)',
|
||||
pointerEvents: 'none', borderRadius: '50%'
|
||||
}} />
|
||||
|
||||
<div style={{ maxWidth: '780px', position: 'relative', zIndex: 1 }}>
|
||||
<span style={{
|
||||
background: 'rgba(255, 170, 0, 0.12)', border: '1px solid rgba(255, 170, 0, 0.3)',
|
||||
color: '#ffaa00', fontSize: '0.75rem', fontWeight: 700, textTransform: 'uppercase',
|
||||
letterSpacing: '0.12em', padding: '0.35rem 1rem', borderRadius: '50px', display: 'inline-block', marginBottom: '1.25rem'
|
||||
}}>
|
||||
Join Cavin Infotech
|
||||
</span>
|
||||
<h1 style={{ fontSize: 'clamp(2.2rem, 4vw, 3.4rem)', fontWeight: 800, lineHeight: 1.2, letterSpacing: '-0.02em', marginBottom: '1.25rem' }}>
|
||||
Engineer What's Next in <span style={{ color: '#ffaa00' }}>Enterprise Innovation</span>
|
||||
</h1>
|
||||
<p style={{ color: '#94a3b8', fontSize: '1.1rem', lineHeight: 1.6, marginBottom: '2rem' }}>
|
||||
We are a collective of architects, engineers, spatial UX designers, and AI specialists building high-availability industrial platforms for Fortune 500 enterprises.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
const formEl = document.getElementById('application-form');
|
||||
formEl?.scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #ffaa00 0%, #e69900 100%)',
|
||||
color: '#020c1e', border: 'none', padding: '0.85rem 2rem', borderRadius: '50px',
|
||||
fontSize: '0.95rem', fontWeight: 700, cursor: 'pointer',
|
||||
boxShadow: '0 8px 25px rgba(255, 170, 0, 0.25)', transition: 'transform 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-2px)'}
|
||||
onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}
|
||||
>
|
||||
Apply Now ↓
|
||||
</button>
|
||||
{onOpenStrategy && (
|
||||
<button
|
||||
onClick={onOpenStrategy}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.06)', color: '#ffffff',
|
||||
border: '1px solid rgba(255,255,255,0.15)', padding: '0.85rem 2rem', borderRadius: '50px',
|
||||
fontSize: '0.95rem', fontWeight: 600, cursor: 'pointer', transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = '#ffaa00'; e.currentTarget.style.color = '#ffaa00'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = 'rgba(255,255,255,0.15)'; e.currentTarget.style.color = '#ffffff'; }}
|
||||
>
|
||||
Book Strategy Call
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Culture & Perks Grid */}
|
||||
<div style={{ marginBottom: '5rem' }}>
|
||||
<h2 style={{ fontSize: '1.8rem', fontWeight: 700, marginBottom: '2rem', textAlign: 'center' }}>
|
||||
Why Work With Cavin Infotech?
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : 'repeat(4, 1fr)',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
{[
|
||||
{ icon: Cpu, title: 'Next-Gen Tech Stack', desc: 'Deploy Agentic AI pipelines, spatial MR cleanroom simulators, and low-latency IIoT edge networks.' },
|
||||
{ icon: Sparkles, title: 'High Impact Projects', desc: 'Transform operations for global manufacturing leaders, healthcare networks, and enterprise portfolios.' },
|
||||
{ icon: Shield, title: 'Autonomous Ownership', desc: 'No micro-management. Take ownership of key modules and lead technical architecture.' },
|
||||
{ icon: Users, title: 'Continuous Growth', desc: 'Generous learning stipends, certification support, and direct mentorship from industry leaders.' },
|
||||
].map((perk, idx) => (
|
||||
<div key={idx} style={{
|
||||
background: 'rgba(15, 23, 42, 0.6)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '16px', padding: '1.75rem',
|
||||
transition: 'transform 0.3s ease, border-color 0.3s ease'
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = 'rgba(255, 170, 0, 0.4)'; e.currentTarget.style.transform = 'translateY(-4px)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.08)'; e.currentTarget.style.transform = 'translateY(0)'; }}
|
||||
>
|
||||
<perk.icon size={28} style={{ color: '#ffaa00', marginBottom: '1rem' }} />
|
||||
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, marginBottom: '0.5rem', color: '#ffffff' }}>{perk.title}</h3>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.88rem', lineHeight: 1.55 }}>{perk.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two Column Layout: Openings + Application Form */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr',
|
||||
gap: '3rem',
|
||||
alignItems: 'start'
|
||||
}}>
|
||||
|
||||
{/* Open Roles List */}
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1.8rem', fontWeight: 700, marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||
<Briefcase style={{ color: '#ffaa00' }} /> Open Positions
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
{openPositions.map((job, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => setCareersPosition(job.title)}
|
||||
style={{
|
||||
background: careersPosition === job.title ? 'rgba(255, 170, 0, 0.08)' : 'rgba(15, 23, 42, 0.6)',
|
||||
border: careersPosition === job.title ? '1px solid rgba(255, 170, 0, 0.4)' : '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '16px', padding: '1.5rem', cursor: 'pointer',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ fontSize: '1.1rem', fontWeight: 700, color: '#ffffff' }}>{job.title}</h3>
|
||||
<span style={{
|
||||
background: 'rgba(255,255,255,0.06)', color: '#cbd5e1',
|
||||
fontSize: '0.75rem', fontWeight: 600, padding: '0.2rem 0.6rem', borderRadius: '4px'
|
||||
}}>
|
||||
{job.type}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1rem', color: '#64748b', fontSize: '0.82rem', fontWeight: 600 }}>
|
||||
<span>{job.department}</span>
|
||||
<span>•</span>
|
||||
<span>{job.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Application Form */}
|
||||
<div id="application-form" style={{
|
||||
background: 'rgba(10, 18, 38, 0.95)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
borderRadius: '24px', padding: isMobile ? '1.75rem' : '2.5rem',
|
||||
boxShadow: '0 20px 40px rgba(0,0,0,0.4)'
|
||||
}}>
|
||||
<h2 style={{ fontSize: '1.6rem', fontWeight: 700, marginBottom: '0.5rem', color: '#ffffff' }}>
|
||||
Submit Your Application
|
||||
</h2>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.9rem', marginBottom: '1.75rem' }}>
|
||||
Applying for: <strong style={{ color: '#ffaa00' }}>{careersPosition}</strong>
|
||||
</p>
|
||||
|
||||
{careersErrorMsg && (
|
||||
<div style={{
|
||||
background: 'rgba(239, 68, 68, 0.15)', border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
color: '#fca5a5', padding: '0.75rem 1rem', borderRadius: '10px', fontSize: '0.88rem', marginBottom: '1.25rem'
|
||||
}}>
|
||||
{careersErrorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showAcknowledgement ? (
|
||||
<form onSubmit={handleCareersSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: '#cbd5e1', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Select Position
|
||||
</label>
|
||||
<select
|
||||
value={careersPosition}
|
||||
onChange={e => setCareersPosition(e.target.value)}
|
||||
style={{
|
||||
width: '100%', background: '#070e1e', border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
color: '#ffffff', padding: '0.75rem 1rem', borderRadius: '10px', outline: 'none', fontSize: '0.92rem'
|
||||
}}
|
||||
>
|
||||
{openPositions.map((j, i) => <option key={i} value={j.title}>{j.title}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: '#cbd5e1', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text" required value={careersName}
|
||||
onChange={e => setCareersName(e.target.value)}
|
||||
placeholder="e.g. Alex Morgan"
|
||||
style={{
|
||||
width: '100%', background: '#070e1e', border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
color: '#ffffff', padding: '0.75rem 1rem', borderRadius: '10px', outline: 'none', fontSize: '0.92rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '1rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: '#cbd5e1', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
type="email" required value={careersEmail}
|
||||
onChange={e => setCareersEmail(e.target.value)}
|
||||
placeholder="alex@example.com"
|
||||
style={{
|
||||
width: '100%', background: '#070e1e', border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
color: '#ffffff', padding: '0.75rem 1rem', borderRadius: '10px', outline: 'none', fontSize: '0.92rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: '#cbd5e1', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Phone Number *
|
||||
</label>
|
||||
<input
|
||||
type="tel" required value={careersPhone}
|
||||
onChange={e => setCareersPhone(e.target.value)}
|
||||
placeholder="+1 (555) 000-0000"
|
||||
style={{
|
||||
width: '100%', background: '#070e1e', border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
color: '#ffffff', padding: '0.75rem 1rem', borderRadius: '10px', outline: 'none', fontSize: '0.92rem'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'block', fontSize: '0.82rem', color: '#cbd5e1', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Attach Resume / CV (.pdf, .doc, .docx) *
|
||||
</label>
|
||||
<div style={{
|
||||
border: '2px dashed rgba(255, 255, 255, 0.18)', borderRadius: '12px',
|
||||
padding: '1.25rem', textAlign: 'center', background: 'rgba(255, 255, 255, 0.02)',
|
||||
position: 'relative', cursor: 'pointer'
|
||||
}}>
|
||||
<input
|
||||
type="file" required accept=".pdf,.doc,.docx"
|
||||
onChange={e => setCareersFile(e.target.files[0])}
|
||||
style={{ position: 'absolute', inset: 0, opacity: 0, width: '100%', height: '100%', cursor: 'pointer' }}
|
||||
/>
|
||||
<Upload size={22} style={{ color: '#ffaa00', marginBottom: '0.4rem' }} />
|
||||
<div style={{ fontSize: '0.88rem', color: '#e2e8f0' }}>
|
||||
{careersFile ? careersFile.name : 'Click or drag file to upload resume'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TurnstileCaptcha
|
||||
onVerify={token => setCareersTurnstileToken(token)}
|
||||
onExpire={() => setCareersTurnstileToken('')}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit" disabled={isCareersSubmitting}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #ffaa00 0%, #e69900 100%)',
|
||||
color: '#020c1e', border: 'none', padding: '0.9rem', borderRadius: '50px',
|
||||
fontSize: '1rem', fontWeight: 700, cursor: isCareersSubmitting ? 'not-allowed' : 'pointer',
|
||||
boxShadow: '0 8px 20px rgba(255,170,0,0.25)', marginTop: '0.5rem'
|
||||
}}
|
||||
>
|
||||
{isCareersSubmitting ? 'Submitting Application...' : 'Submit Application'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '2rem 1rem' }}>
|
||||
<CheckCircle2 size={54} style={{ color: '#10b981', margin: '0 auto 1rem' }} />
|
||||
<h3 style={{ fontSize: '1.5rem', fontWeight: 700, marginBottom: '0.75rem' }}>Application Submitted!</h3>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.92rem', lineHeight: 1.6, marginBottom: '1.75rem' }}>
|
||||
Thank you for applying to Cavin Infotech. Our hiring team will review your resume and contact you shortly.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => onNavigate('Home')}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.15)',
|
||||
color: '#ffffff', padding: '0.7rem 1.5rem', borderRadius: '50px', fontSize: '0.9rem', fontWeight: 600, cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Back to Home
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Check, Calendar, ChevronDown, CheckCircle2 } from 'lucide-react';
|
||||
import { API_BASE } from '../lib/apiBase';
|
||||
import TurnstileCaptcha from './TurnstileCaptcha';
|
||||
|
||||
export default function StrategyCallPage({ isMobile, onNavigate, onClose }) {
|
||||
const [strategyName, setStrategyName] = useState('');
|
||||
const [strategyEmail, setStrategyEmail] = useState('');
|
||||
const [strategyMobile, setStrategyMobile] = useState('');
|
||||
const [strategyDesignation, setStrategyDesignation] = useState('');
|
||||
const [strategyDate, setStrategyDate] = useState('');
|
||||
const [strategyTime, setStrategyTime] = useState('');
|
||||
const [strategyInterests, setStrategyInterests] = useState({
|
||||
'AI & Analytics': false,
|
||||
'SAP Services': false,
|
||||
'XR Platforms': false,
|
||||
'Smart Factory': false,
|
||||
'Cloud Engineering': false,
|
||||
'Enterprise Software': false,
|
||||
});
|
||||
const [strategyTurnstileToken, setStrategyTurnstileToken] = useState('');
|
||||
const [strategyErrorMsg, setStrategyErrorMsg] = useState('');
|
||||
const [isStrategySubmitting, setIsStrategySubmitting] = useState(false);
|
||||
const [showAcknowledgement, setShowAcknowledgement] = useState(false);
|
||||
|
||||
const timeOptions = [
|
||||
'09:00 AM EST', '10:30 AM EST', '01:00 PM EST', '03:00 PM EST',
|
||||
'02:00 PM IST', '04:00 PM IST', '06:00 PM IST', '08:00 PM IST'
|
||||
];
|
||||
|
||||
const handleInterestToggle = (key) => {
|
||||
setStrategyInterests(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
const handleStrategySubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!strategyTurnstileToken) {
|
||||
setStrategyErrorMsg('Please complete the Turnstile verification.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedTopics = Object.entries(strategyInterests)
|
||||
.filter(([_, sel]) => sel)
|
||||
.map(([key]) => key);
|
||||
|
||||
setIsStrategySubmitting(true);
|
||||
setStrategyErrorMsg('');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/contact/strategy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: strategyName,
|
||||
email: strategyEmail,
|
||||
phone: strategyMobile,
|
||||
designation: strategyDesignation,
|
||||
date: strategyDate,
|
||||
time: strategyTime,
|
||||
topics: selectedTopics.length > 0 ? selectedTopics : ['General Strategy'],
|
||||
turnstileToken: strategyTurnstileToken,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setShowAcknowledgement(true);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setStrategyErrorMsg(data.error || 'Failed to schedule consultation. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStrategyErrorMsg('Server error. Please check connection and try again.');
|
||||
} finally {
|
||||
setIsStrategySubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
maxWidth: '860px',
|
||||
borderRadius: '24px',
|
||||
overflow: 'hidden',
|
||||
background: '#040b19',
|
||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
boxShadow: '0 30px 80px rgba(0, 0, 0, 0.8)',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '38% 62%',
|
||||
color: '#ffffff',
|
||||
position: 'relative'
|
||||
}}>
|
||||
|
||||
{/* LEFT SIDE PANEL (BLUE GRADIENT) */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(180deg, #153e75 0%, #0b2246 100%)',
|
||||
padding: isMobile ? '2rem 1.5rem' : '3rem 2.25rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '1.85rem',
|
||||
fontWeight: 700,
|
||||
color: '#ffffff',
|
||||
marginBottom: '2.5rem',
|
||||
lineHeight: 1.25,
|
||||
letterSpacing: '-0.01em'
|
||||
}}>
|
||||
How We Can<br />Help?
|
||||
</h2>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
{[
|
||||
'Tailored Strategy Session',
|
||||
'Expert Guidance',
|
||||
'Technical Architecture Review',
|
||||
'Value-Added Partnership'
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '0.85rem' }}>
|
||||
<Check size={18} style={{ color: '#ffffff', flexShrink: 0, marginTop: '0.15rem' }} />
|
||||
<span style={{ fontSize: '0.92rem', color: '#f8fafc', fontWeight: 500, lineHeight: 1.4 }}>
|
||||
{item}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT SIDE FORM PANEL */}
|
||||
<div style={{
|
||||
background: '#050c1b',
|
||||
padding: isMobile ? '2rem 1.5rem' : '2.5rem 2.25rem',
|
||||
position: 'relative'
|
||||
}}>
|
||||
|
||||
{strategyErrorMsg && (
|
||||
<div style={{
|
||||
background: 'rgba(239, 68, 68, 0.15)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
color: '#fca5a5',
|
||||
padding: '0.65rem 0.85rem',
|
||||
borderRadius: '8px',
|
||||
fontSize: '0.82rem',
|
||||
marginBottom: '1.25rem'
|
||||
}}>
|
||||
{strategyErrorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showAcknowledgement ? (
|
||||
<form onSubmit={handleStrategySubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
|
||||
{/* Row 1: Name & Email */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Name"
|
||||
value={strategyName}
|
||||
onChange={e => setStrategyName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: '#ffffff',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderBottomColor = '#38bdf8'}
|
||||
onBlur={e => e.target.style.borderBottomColor = 'rgba(255, 255, 255, 0.25)'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="Email"
|
||||
value={strategyEmail}
|
||||
onChange={e => setStrategyEmail(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: '#ffffff',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderBottomColor = '#38bdf8'}
|
||||
onBlur={e => e.target.style.borderBottomColor = 'rgba(255, 255, 255, 0.25)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Mobile & Designation */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
placeholder="Mobile"
|
||||
value={strategyMobile}
|
||||
onChange={e => setStrategyMobile(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: '#ffffff',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderBottomColor = '#38bdf8'}
|
||||
onBlur={e => e.target.style.borderBottomColor = 'rgba(255, 255, 255, 0.25)'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Designation"
|
||||
value={strategyDesignation}
|
||||
onChange={e => setStrategyDesignation(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: '#ffffff',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.9rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
}}
|
||||
onFocus={e => e.target.style.borderBottomColor = '#38bdf8'}
|
||||
onBlur={e => e.target.style.borderBottomColor = 'rgba(255, 255, 255, 0.25)'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Preferred Date & Preferred Time Slot */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: '1.5rem' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type="date"
|
||||
value={strategyDate}
|
||||
onChange={e => setStrategyDate(e.target.value)}
|
||||
placeholder="Preferred Date"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: strategyDate ? '#ffffff' : '#94a3b8',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.88rem',
|
||||
outline: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<select
|
||||
value={strategyTime}
|
||||
onChange={e => setStrategyTime(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.25)',
|
||||
color: strategyTime ? '#ffffff' : '#94a3b8',
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.88rem',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
appearance: 'none'
|
||||
}}
|
||||
>
|
||||
<option value="" disabled style={{ background: '#050c1b', color: '#94a3b8' }}>Preferred Time Slot</option>
|
||||
{timeOptions.map((opt, i) => (
|
||||
<option key={i} value={opt} style={{ background: '#050c1b', color: '#ffffff' }}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={16} style={{ position: 'absolute', right: 0, top: '50%', transform: 'translateY(-50%)', color: '#94a3b8', pointerEvents: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AREAS OF INTEREST BOX */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.02)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '1rem 1.25rem',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 700,
|
||||
color: '#64748b',
|
||||
letterSpacing: '0.08em',
|
||||
marginBottom: '0.85rem',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
AREAS OF INTEREST
|
||||
</span>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem 1rem' }}>
|
||||
{[
|
||||
'AI & Analytics', 'SAP Services',
|
||||
'XR Platforms', 'Smart Factory',
|
||||
'Cloud Engineering', 'Enterprise Software'
|
||||
].map((item, i) => (
|
||||
<label
|
||||
key={i}
|
||||
onClick={() => handleInterestToggle(item)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.6rem',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.82rem',
|
||||
color: strategyInterests[item] ? '#ffffff' : '#94a3b8',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '4px',
|
||||
border: strategyInterests[item] ? '1px solid #38bdf8' : '1px solid rgba(255, 255, 255, 0.3)',
|
||||
background: strategyInterests[item] ? 'rgba(56, 189, 248, 0.2)' : 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.15s ease'
|
||||
}}>
|
||||
{strategyInterests[item] && <Check size={12} style={{ color: '#38bdf8' }} />}
|
||||
</div>
|
||||
<span>{item}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TURNSTILE CAPTCHA */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '0.5rem' }}>
|
||||
<TurnstileCaptcha
|
||||
onVerify={token => setStrategyTurnstileToken(token)}
|
||||
onExpire={() => setStrategyTurnstileToken('')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* SUBMIT BUTTON */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isStrategySubmitting}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: '#ffffff',
|
||||
color: '#000000',
|
||||
border: 'none',
|
||||
padding: '0.85rem 1.5rem',
|
||||
borderRadius: '50px',
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 700,
|
||||
cursor: isStrategySubmitting ? 'not-allowed' : 'pointer',
|
||||
boxShadow: '0 4px 20px rgba(255, 255, 255, 0.25)',
|
||||
transition: 'all 0.2s ease',
|
||||
marginTop: '0.5rem'
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-2px)'}
|
||||
onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}
|
||||
>
|
||||
{isStrategySubmitting ? 'Booking...' : 'Book Session'}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '2.5rem 1rem' }}>
|
||||
<CheckCircle2 size={54} style={{ color: '#10b981', margin: '0 auto 1rem' }} />
|
||||
<h3 style={{ fontSize: '1.4rem', fontWeight: 700, marginBottom: '0.75rem', color: '#ffffff' }}>
|
||||
Session Booked!
|
||||
</h3>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.88rem', lineHeight: 1.6, marginBottom: '1.75rem' }}>
|
||||
Thank you, <strong style={{ color: '#ffffff' }}>{strategyName}</strong>. Your consultation has been scheduled.
|
||||
</p>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
color: '#ffffff',
|
||||
padding: '0.65rem 1.5rem',
|
||||
borderRadius: '50px',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,24 +20,29 @@ function allowedPreviewOrigins() {
|
||||
if (typeof window !== 'undefined') origins.add(window.location.origin);
|
||||
return origins;
|
||||
}
|
||||
|
||||
function loadFromLocalStorage() {
|
||||
const saved = localStorage.getItem('static_content');
|
||||
if (!saved) return null;
|
||||
try {
|
||||
const { _updatedAt, ...rest } = JSON.parse(saved);
|
||||
// Remove local storage logic that actively deletes static_content based on title
|
||||
// Let the API determine what is published.
|
||||
return { content: prefixAssets(rest), updatedAt: _updatedAt ?? null };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function ContentProvider({ children, previewMode = false }) {
|
||||
const [content, setContent] = useState(prefixAssets(defaultContent));
|
||||
const [content, setContent] = useState(() => prefixAssets(defaultContent));
|
||||
const [loading, setLoading] = useState(!previewMode);
|
||||
const [error, setError] = useState(null);
|
||||
const [updatedAt, setUpdatedAt] = useState(null);
|
||||
|
||||
function applyContent(data) {
|
||||
function applyContent(data, blogs = null) {
|
||||
const { _updatedAt, ...rest } = data;
|
||||
if (!rest.insights) rest.insights = { items: [] };
|
||||
if (blogs && Array.isArray(blogs) && blogs.length > 0) {
|
||||
rest.insights.items = blogs;
|
||||
}
|
||||
setContent(prefixAssets(rest));
|
||||
setUpdatedAt(_updatedAt ?? null);
|
||||
setError(null);
|
||||
@@ -45,13 +50,17 @@ export function ContentProvider({ children, previewMode = false }) {
|
||||
|
||||
function refresh() {
|
||||
restoreAllBlobUrls().then(() => {
|
||||
fetch(`${API_BASE}/api/content`)
|
||||
.then((res) => res.ok ? res.json() : Promise.reject())
|
||||
.then((data) => applyContent(data))
|
||||
.catch(() => {
|
||||
const local = loadFromLocalStorage();
|
||||
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||
});
|
||||
Promise.all([
|
||||
fetch(`${API_BASE}/api/content`).then(r => r.ok ? r.json() : null),
|
||||
fetch(`${API_BASE}/api/content/blogs`).then(r => r.ok ? r.json() : null)
|
||||
]).then(([contentData, blogsData]) => {
|
||||
if (contentData) {
|
||||
applyContent(contentData, blogsData);
|
||||
}
|
||||
}).catch(() => {
|
||||
const local = loadFromLocalStorage();
|
||||
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,17 +69,21 @@ export function ContentProvider({ children, previewMode = false }) {
|
||||
|
||||
// Initial load
|
||||
restoreAllBlobUrls().then(() => {
|
||||
fetch(`${API_BASE}/api/content`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to load content');
|
||||
return res.json();
|
||||
Promise.all([
|
||||
fetch(`${API_BASE}/api/content`).then(r => r.ok ? r.json() : null),
|
||||
fetch(`${API_BASE}/api/content/blogs`).then(r => r.ok ? r.json() : null)
|
||||
])
|
||||
.then(([contentData, blogsData]) => {
|
||||
if (!contentData) throw new Error('Failed to load content');
|
||||
applyContent(contentData, blogsData);
|
||||
})
|
||||
.then((data) => applyContent(data))
|
||||
.catch((err) => {
|
||||
console.warn('Using fallback content:', err.message);
|
||||
const local = loadFromLocalStorage();
|
||||
if (local) { setContent(local.content); setUpdatedAt(local.updatedAt); }
|
||||
else setError(err.message);
|
||||
else {
|
||||
applyContent(defaultContent);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
});
|
||||
@@ -79,13 +92,19 @@ export function ContentProvider({ children, previewMode = false }) {
|
||||
if (BC) BC.onmessage = (e) => { if (e.data?.type === 'updated') refresh(); };
|
||||
// Same-tab: custom event
|
||||
window.addEventListener('citpl:content-updated', refresh);
|
||||
// Cross-tab fallback: storage event
|
||||
// Cross-tab fallback: storage event & window focus for instant CMS sync
|
||||
const onStorage = (e) => { if (e.key === 'static_content') refresh(); };
|
||||
window.addEventListener('storage', onStorage);
|
||||
window.addEventListener('focus', refresh);
|
||||
|
||||
// Poll every 5s for live CMS updates
|
||||
const pollTimer = setInterval(refresh, 5000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('citpl:content-updated', refresh);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
window.removeEventListener('focus', refresh);
|
||||
clearInterval(pollTimer);
|
||||
if (BC) BC.onmessage = null;
|
||||
};
|
||||
}, [previewMode]);
|
||||
|
||||
Reference in New Issue
Block a user