feat: Containerize services with Docker Compose (PostgreSQL, Strapi CMS, Express Backend, Nginx Frontend) on custom ports >9000

This commit is contained in:
Vishva
2026-08-03 19:58:10 +05:30
parent 48f8ab9716
commit 6244fd4072
247 changed files with 56672 additions and 169 deletions
+96 -14
View File
@@ -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
+761
View File
@@ -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>
);
}
+385
View File
@@ -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>
);
}
+419
View File
@@ -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>
);
}
+36 -17
View File
@@ -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]);