diff --git a/package-lock.json b/package-lock.json index 676688d..1630952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "sonner": "^2.0.3", "tailwind-merge": "*", "tailwindcss": "^4.1.12", + "three": "^0.183.2", "vaul": "^1.1.2" }, "devDependencies": { @@ -65,6 +66,7 @@ "@types/react": "^19.1.12", "@types/react-dom": "^19.1.8", "@types/react-slick": "^0.23.13", + "@types/three": "^0.183.1", "@vitejs/plugin-react": "^5.0.2", "@vitejs/plugin-react-swc": "^3.10.2", "vite": "^6.3.5" @@ -387,6 +389,13 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@esbuild/win32-x64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", @@ -2023,6 +2032,13 @@ "tailwindcss": "4.1.12" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2178,12 +2194,42 @@ "@types/react": "*" } }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.2.tgz", @@ -2226,6 +2272,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -2680,6 +2733,13 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/framer-motion": { "version": "12.23.12", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz", @@ -2925,6 +2985,13 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "dev": true, + "license": "MIT" + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3579,6 +3646,12 @@ "node": ">=18" } }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/package.json b/package.json index 63f6782..b3d93bd 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "sonner": "^2.0.3", "tailwind-merge": "*", "tailwindcss": "^4.1.12", + "three": "^0.183.2", "vaul": "^1.1.2" }, "devDependencies": { @@ -60,6 +61,7 @@ "@types/react": "^19.1.12", "@types/react-dom": "^19.1.8", "@types/react-slick": "^0.23.13", + "@types/three": "^0.183.1", "@vitejs/plugin-react": "^5.0.2", "@vitejs/plugin-react-swc": "^3.10.2", "vite": "^6.3.5" diff --git a/src/App.tsx b/src/App.tsx index 0c86e89..9d99916 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,7 +32,6 @@ import HomePage from './pages/HomePage'; import { AboutUs } from './components/AboutUs'; import { Services } from './components/Services'; import { LearningFacilityNew } from './components/LearningFacilityNew'; -import HomePageNew from './pages/HomePageNew'; import { FooterNew } from './components/FooterNew'; import { Privacy } from "./pages/Privacy"; import { TermsCondition } from "./pages/TermsCondition"; @@ -108,8 +107,8 @@ export default function App() { } /> {/* Dynamic Routes */} - } /> - } /> + } /> + {/* } /> */} {/* } /> } /> */} } /> diff --git a/src/assets/panoramas/cayley_interior.jpg b/src/assets/panoramas/cayley_interior.jpg new file mode 100644 index 0000000..83c499b Binary files /dev/null and b/src/assets/panoramas/cayley_interior.jpg differ diff --git a/src/assets/panoramas/cedar_bridge_sunset.jpg b/src/assets/panoramas/cedar_bridge_sunset.jpg new file mode 100644 index 0000000..b0a5b0f Binary files /dev/null and b/src/assets/panoramas/cedar_bridge_sunset.jpg differ diff --git a/src/components/AboutUs.tsx b/src/components/AboutUs.tsx index fa79625..ec0c2cb 100644 --- a/src/components/AboutUs.tsx +++ b/src/components/AboutUs.tsx @@ -35,6 +35,7 @@ import Balaji from '../assets/Balaji-Chandrakumar.jpeg'; import Ramesh from '../assets/Ramesh-Padmanabhan.jpeg'; import Diju from '../assets/Diju.jpeg'; import svgPaths from '../imports/svg-kw7r0ellyk'; +import { useGetAboutUsQuery } from '../redux/services/aboutUsApi'; // Leadership Orientations Data const leadershipOrientations = [ @@ -83,8 +84,8 @@ const benefits = [ 'We recommend that the Leadership intervention is designed for a period of 12-15 months with multiple touch points which can constitute a combination of classroom, fire side chats, one-on-one sessions, address by an expert, use of profilers, accessing online content on concepts and accomplished leaders\' experiences' ]; -// Team Members Data with Full Profiles -const teamMembers = [ +// Team Members Data with Full Profiles (Static - can be kept or also fetched from API if needed) +const staticTeamMembers = [ { name: 'Mr. K Ramkumar', role: 'Managing Director', @@ -271,14 +272,38 @@ After a career break, she joined KLC as Practice Head. She now co-creates leader } ]; +// Loading Skeleton Component +const AboutUsSkeleton = () => ( +
+ {/* Hero Section Skeleton */} +
+
+
+
+
+
+
+
+
+
+
+
+ + {/* Add more skeleton sections as needed */} +
Loading...
+
+); export function AboutUs() { const [isVisible, setIsVisible] = useState(false); const [expandedValue, setExpandedValue] = useState('context'); - const [selectedMember, setSelectedMember] = useState(null); + const [selectedMember, setSelectedMember] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const handleMemberClick = (member: typeof teamMembers[0]) => { + // Fetch About Us data from API + const { data: aboutUsData, isLoading, isError, error } = useGetAboutUsQuery(); + + const handleMemberClick = (member: typeof staticTeamMembers[0]) => { setSelectedMember(member); setIsModalOpen(true); }; @@ -296,7 +321,6 @@ export function AboutUs() { document.body.style.overflow = ''; } - // Cleanup on unmount return () => { document.body.style.overflow = ''; }; @@ -305,9 +329,8 @@ export function AboutUs() { useEffect(() => { setIsVisible(true); - // Timeline fill animation on scroll const handleScroll = () => { - const timelineSection = document.querySelector('#timeline-fill-line'); + const timelineSection = document.querySelector('#timeline-fill-line') as HTMLElement | null; const timelineContainer = timelineSection?.parentElement; if (!timelineSection || !timelineContainer) return; @@ -315,47 +338,56 @@ export function AboutUs() { const rect = timelineContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; - // Calculate how much of the timeline is visible const sectionTop = rect.top; const sectionHeight = rect.height; const visibleTop = Math.max(0, windowHeight - sectionTop); const visibleHeight = Math.min(visibleTop, sectionHeight); const scrollProgress = Math.max(0, Math.min(1, visibleHeight / sectionHeight)); - // Calculate the exact position to end at Phase 3 dot - const maxFillHeight = 'calc(100% - 1rem)'; // Match the background line limit + const maxFillHeight = 'calc(100% - 1rem)'; - // Apply progressive fill that respects the maximum height constraint if (scrollProgress >= 0.9) { - // When nearly complete, set to exact end position timelineSection.style.height = maxFillHeight; } else { - // Progressive fill up to 90% of the way timelineSection.style.height = `${scrollProgress * 90}%`; } }; - // Add scroll listener window.addEventListener('scroll', handleScroll); - - // Initial call to set the initial state handleScroll(); - // Cleanup return () => { window.removeEventListener('scroll', handleScroll); }; }, []); + // Show loading skeleton while fetching data + if (isLoading) { + return ; + } + + // Show error state if API call fails + if (isError) { + return ( +
+
+

Error Loading Page

+

Failed to load About Us content. Please try again later.

+ +
+
+ ); + } + return (
- {/* Hero Section - Our Vision Page Style */} + {/* Hero Section - Dynamic from API */}
@@ -364,32 +396,22 @@ export function AboutUs() {
- {/* Back Navigation */} - {/*
- -
*/} -

- Advancing Leadership Through Insight + {aboutUsData?.hero_section?.headline || "Advancing Leadership Through Insight"}

- Founded in 2016 with the vision of being a world class institution in the thought and practice of leadership. We facilitate institutions to build Leadership capacity and capability while helping individuals unleash their potential. + + {aboutUsData?.hero_section?.subtext || "Founded in 2016 with the vision of being a world class institution in the thought and practice of leadership. We facilitate institutions to build Leadership capacity and capability while helping individuals unleash their potential."} +

navigateTo('/contact?topic=management-development')} + text={aboutUsData?.hero_section?.cta_text || "Talk to Us"} + onClick={() => navigateTo(aboutUsData?.hero_section?.cta_destination || '/contact?topic=management-development')} ariaLabel="Talk to us about management development" className="primary-cta-button-blue cta-text-white" /> @@ -399,7 +421,7 @@ export function AboutUs() {
- {/* Section 1: Our Promise */} + {/* Section 1: Our Promise - Dynamic from API */}
@@ -410,7 +432,7 @@ export function AboutUs() { viewport={{ once: true }} className="text-center" > - +

- {/* Section 2: How We Work */} + {/* Section 2: How We Work - Dynamic from API */}
@@ -434,109 +456,143 @@ export function AboutUs() { viewport={{ once: true }} className="text-center mb-16" > - -

How We Work

+ +

{aboutUsData?.how_we_work_title || "How We Work"}

- {/* Four Key Points Grid */} + {/* Four Key Points Grid - Using API data if available, otherwise fallback to static */}
- -
-
0) ? ( + aboutUsData.how_we_work.map((item, index) => ( + - -
-
-

Co-created interventions

-

- We collaborate with you to design solutions that fit your unique organizational context and strategic objectives. -

-
-
-
+
+
+ {index === 0 && } + {index === 1 && } + {index === 2 && } + {index === 3 && } +
+
+

{item.title}

+

+ {item.description} +

+
+
+ + )) + ) : ( + // Fallback to static data if API data is not available + <> + +
+
+ +
+
+

Co-created interventions

+

+ We collaborate with you to design solutions that fit your unique organizational context and strategic objectives. +

+
+
+
- -
-
- -
-
-

Grounded in business context

-

- Every solution is tailored to your specific business environment, challenges, and growth objectives. -

-
-
-
+
+
+ +
+
+

Grounded in business context

+

+ Every solution is tailored to your specific business environment, challenges, and growth objectives. +

+
+
+ - -
-
- -
-
-

Research-backed, behaviour-anchored

-

- Our methodologies are rooted in rigorous research and focused on sustainable behavioral transformation. -

-
-
-
+
+
+ +
+
+

Research-backed, behaviour-anchored

+

+ Our methodologies are rooted in rigorous research and focused on sustainable behavioral transformation. +

+
+
+ - -
-
- -
-
-

Delivered through immersive formats

-

- Interactive, experiential learning approaches that engage participants and drive lasting impact. -

-
-
-
+
+
+ +
+
+

Delivered through immersive formats

+

+ Interactive, experiential learning approaches that engage participants and drive lasting impact. +

+
+
+ + + )}
- {/* Section 3: Who We Are - Updated Statistics */} + {/* Section 3: Who We Are - Updated Statistics - Dynamic from API */}
@@ -552,7 +608,7 @@ export function AboutUs() {
- Who we are + {aboutUsData?.who_we_are_title || "Who we are"}
@@ -568,96 +624,122 @@ export function AboutUs() {
- {/* Updated Statistics Grid */} + {/* Updated Statistics Grid - Dynamic from API */}
- -
- 150+ -
-
-
- CORPORATES -
-
+ {(aboutUsData?.stat_section && aboutUsData.stat_section.length > 0) ? ( + aboutUsData.stat_section.map((stat, index) => ( + +
+ {stat.number}{stat.suffix} +
+
+
+ {stat.label.toUpperCase()} +
+
+ )) + ) : ( + // Fallback to static statistics if API data is not available + <> + +
+ 150+ +
+
+
+ CORPORATES +
+
- -
- 27,000+ -
-
-
- LEADERS -
-
+ +
+ 27,000+ +
+
+
+ LEADERS +
+
- -
- 5,000+ -
-
-
- ROOM NIGHTS -
-
+ +
+ 5,000+ +
+
+
+ ROOM NIGHTS +
+
- -
- India & APAC -
-
-
- PRESENCE -
-
+ +
+ India & APAC +
+
+
+ PRESENCE +
+
+ + )}
- - - {/* Section 4: Our Team */} + {/* Section 4: Our Team - Dynamic from API */}
@@ -669,8 +751,8 @@ export function AboutUs() { viewport={{ once: true }} className="text-center mb-16" > - -

Our Team

+ +

{aboutUsData?.our_team_title || "Our Team"}

We have a team of 7 consultants and 4 young consultants. All our senior Consultants are ex-business professionals with experience ranging from 15-30 years in varied business functions and carry a deep understanding of the area they are engaging in. Two of them bring in Board room experience. – Meet them @@ -678,9 +760,9 @@ export function AboutUs() {

- {/* Team Members Grid */} + {/* Team Members Grid - Using static team members with full profiles */}
- {teamMembers.map((member, index) => ( + {staticTeamMembers.map((member, index) => ( ))}
+ + {/* Alternative: Use API team data if needed */} + {/* {aboutUsData?.our_team && aboutUsData.our_team.length > 0 && ( +
+ {aboutUsData.our_team.map((member, index) => ( + +
+
+ {member.alt_text} +
+
+
+
+ View Profile +
+
+
+
+
+

+ {member.name_role.split(' - ')[0]} +

+

+ {member.name_role.split(' - ')[1] || ''} +

+

{member.bio}

+
+
+ ))} +
+ )} */}
- {/* Section 5: Our Methodology */} + {/* Section 5: Our Methodology (Static - unchanged) */}
@@ -750,19 +881,19 @@ export function AboutUs() {
- {/* Vertical Line Fill - Blue - Animated on Scroll - Ends exactly at Phase 3 dot */} + {/* Vertical Line Fill - Blue - Animated on Scroll */}
@@ -1168,8 +1299,6 @@ export function AboutUs() {
- - {/* Testimonials Section */} ({ id: 'all', name: 'All Categories' }); const [selectedReadTime, setSelectedReadTime] = useState('All Read Times'); - const [selectedDateRange, setSelectedDateRange] = useState('All Time'); - const [selectedTopic, setSelectedTopic] = useState('All Topics'); - const [sortBy, setSortBy] = useState('Most Recent'); + const [selectedDateRange, setSelectedDateRange] = useState('all_time'); + const [selectedTopic, setSelectedTopic] = useState<{ + id: string; + name: string; + }>({ + id: 'all', + name: 'All Topics' + }); + const [sortBy, setSortBy] = useState('most_recent'); const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [currentPage, setCurrentPage] = useState(1); - const articlesPerPage = 4; + const [debouncedSearch, setDebouncedSearch] = useState(''); + const articlesPerPage = 6; const containerRef = useRef(null); + const [allTags, setAllTags] = useState<{ id: string; name: string }[]>([]); - // Use articlesData instead of the old articles variable - const articles = articlesData; - - // Get unique values for filters - FIXED: using articlesData - const categories = ['All Categories', ...Array.from(new Set(articlesData.map(article => article.category)))]; - const authors = ['All Authors', ...Array.from(new Set(articlesData.map(article => article.author)))]; - const readTimes = ['All Read Times', 'Under 5 min', '5-10 min', 'Over 10 min']; - const dateRanges = ['All Time', 'Last 7 days', 'Last 30 days', 'Last 3 months']; - const allTags = Array.from(new Set(articlesData.flatMap(article => article.tags))); - const sortOptions = [ - { value: 'Most Recent', label: 'Most Recent' }, - { value: 'oldest', label: 'Oldest First' }, - { value: 'title', label: 'Title A-Z' }, - { value: 'readTime', label: 'Read Time' }, - { value: 'popular', label: 'Most Popular' } - ]; - - // Filter and sort articles - FIXED: using articlesData - const filteredArticles = articlesData.filter(article => { - const matchesSearch = article.title.toLowerCase().includes(searchTerm.toLowerCase()) || - article.excerpt.toLowerCase().includes(searchTerm.toLowerCase()) || - article.author.toLowerCase().includes(searchTerm.toLowerCase()) || - article.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); - const matchesCategory = selectedCategory === 'All Categories' || article.category === selectedCategory; - const matchesAuthor = selectedAuthor === 'All Authors' || article.author === selectedAuthor; - - // FIXED: Read time filter - properly parse the read time - const readTimeMinutes = parseInt(article.readTime.replace(' min read', '')) || 0; - const matchesReadTime = selectedReadTime === 'All Read Times' || - (selectedReadTime === 'Under 5 min' && readTimeMinutes < 5) || - (selectedReadTime === '5-10 min' && readTimeMinutes >= 5 && readTimeMinutes <= 10) || - (selectedReadTime === 'Over 10 min' && readTimeMinutes > 10); - - // Date range filter - const articleDate = new Date(article.date); - const now = new Date(); - const matchesDateRange = selectedDateRange === 'All Time' || - (selectedDateRange === 'Last 7 days' && (now.getTime() - articleDate.getTime()) <= 7 * 24 * 60 * 60 * 1000) || - (selectedDateRange === 'Last 30 days' && (now.getTime() - articleDate.getTime()) <= 30 * 24 * 60 * 60 * 1000) || - (selectedDateRange === 'Last 3 months' && (now.getTime() - articleDate.getTime()) <= 90 * 24 * 60 * 60 * 1000); - - // Topic filter - const matchesTopic = selectedTopic === 'All Topics' || - article.tags.includes(selectedTopic); - - return matchesSearch && matchesCategory && matchesAuthor && matchesReadTime && matchesDateRange && matchesTopic; - }).sort((a, b) => { - switch (sortBy) { - case 'Most Recent': - return new Date(b.date).getTime() - new Date(a.date).getTime(); - case 'oldest': - return new Date(a.date).getTime() - new Date(b.date).getTime(); - case 'title': - return a.title.localeCompare(b.title); - case 'readTime': - // FIXED: Sort by read time - properly parse the values - return (parseInt(a.readTime.replace(' min read', '')) || 0) - (parseInt(b.readTime.replace(' min read', '')) || 0); - case 'popular': - return parseInt(b.views) - parseInt(a.views); - default: - return 0; - } + // Fetch categories for filter dropdown + const { + data: categoriesData, + isLoading: isLoadingCategories + } = useGetFaqCategoriesQuery({ + limit: 100, + offset: 0 }); - // Paginate results - const totalPages = Math.ceil(filteredArticles.length / articlesPerPage); - const startIndex = (currentPage - 1) * articlesPerPage; - const currentArticles = filteredArticles.slice(startIndex, startIndex + articlesPerPage); + // Filter categories to only those for blog and create options with id and name + const categories: CategoryOption[] = [ + { id: 'all', name: 'All Categories' }, + ...(categoriesData?.data?.items + ?.filter((category: any) => category.for_blog) + .map((category: any) => ({ + id: category.id, + name: category.category_name + })) || []) + ]; + // Debounce search term + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchTerm); + }, 500); + return () => clearTimeout(timer); + }, [searchTerm]); + + // Fetch blogs from API with all parameters + const { + data: blogsData, + isLoading: isLoadingBlogs, + isError: isBlogsError, + refetch: refetchBlogs + } = useGetBlogsQuery({ + limit: articlesPerPage, + offset: (currentPage - 1) * articlesPerPage, + search: debouncedSearch || undefined, + content_status: 'publish', + content_type: 'BLOG', + date_range: selectedDateRange !== 'all_time' ? selectedDateRange : undefined, + sort_by: sortBy, + content_category_id: selectedCategory.id !== 'all' ? selectedCategory.id : undefined, // Send UUID + tag_id: selectedTopic.id !== 'all' ? selectedTopic.id : undefined, + }); + + + const sortOptions = [ + { value: 'most_recent', label: 'Most Recent' }, + { value: 'oldest_first', label: 'Oldest First' }, + { value: 'title_az', label: 'Title A-Z' } + ]; + + const readTimes = ['All Read Times', 'Under 5 min', '5-10 min', 'Over 10 min']; + const dateRanges = [ + { value: 'all_time', label: 'All Time' }, + { value: 'last_7_days', label: 'Last 7 days' }, + { value: 'last_30_days', label: 'Last 30 days' }, + { value: 'last_3_months', label: 'Last 3 months' } + ]; + + // Format date function const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', @@ -116,25 +130,94 @@ export function Articles() { }); }; - const clearAllFilters = () => { - setSearchTerm(''); - setSelectedCategory('All Categories'); - setSelectedAuthor('All Authors'); - setSelectedReadTime('All Read Times'); - setSelectedDateRange('All Time'); - setSelectedTopic('All Topics'); - setSortBy('Most Recent'); + // Calculate read time based on content length (approx) + const calculateReadTime = (content: string): string => { + const wordsPerMinute = 200; + const wordCount = content.split(/\s+/).length; + const minutes = Math.ceil(wordCount / wordsPerMinute); + return `${minutes} min read`; }; - const hasActiveFilters = searchTerm || - selectedCategory !== 'All Categories' || - selectedAuthor !== 'All Authors' || + // Filter articles by read time (client-side only - API doesn't support this) + const getReadTimeFilteredArticles = () => { + if (selectedReadTime === 'All Read Times' || !blogsData?.data?.items) { + return blogsData?.data?.items || []; + } + + return (blogsData?.data?.items || []).filter((blog: BlogItem) => { + const readTimeMinutes = parseInt(calculateReadTime(blog.content).replace(' min read', '')) || 0; + return (selectedReadTime === 'Under 5 min' && readTimeMinutes < 5) || + (selectedReadTime === '5-10 min' && readTimeMinutes >= 5 && readTimeMinutes <= 10) || + (selectedReadTime === 'Over 10 min' && readTimeMinutes > 10); + }); + }; + + const finalFilteredArticles = getReadTimeFilteredArticles(); + + const totalPages = Math.ceil((blogsData?.data?.pagination?.total || 0) / articlesPerPage); + + const clearAllFilters = () => { + setSearchTerm(''); + setDebouncedSearch(''); + setSelectedCategory({ id: 'all', name: 'All Categories' }); + setSelectedReadTime('All Read Times'); + setSelectedDateRange('all_time'); + setSelectedTopic({ id: 'all', name: 'All Topics' }); + setSortBy('most_recent'); + setCurrentPage(1); + }; + + const hasActiveFilters = Boolean( + searchTerm || + selectedCategory.id !== 'all' || selectedReadTime !== 'All Read Times' || - selectedDateRange !== 'All Time' || - selectedTopic !== 'All Topics'; + selectedDateRange !== 'all_time' || + selectedTopic.id !== 'all' + ); + + useEffect(() => { + if (!blogsData?.data?.items || allTags.length > 0) return; + + const tags = Array.from( + new Map( + blogsData.data.items + .flatMap((blog: BlogItem) => blog.blog_tags || []) + .map(tag => [tag.id, { id: tag.id, name: tag.tag_name }]) + ).values() + ); + + setAllTags(tags); +}, [blogsData]); + + // Handle loading state + if (isLoadingBlogs || isLoadingCategories) { + return ( +
+ +
+ ); + } + + // Handle error state + if (isBlogsError) { + return ( +
+
+
+ +
+

Failed to load articles

+

Please try again later

+ +
+
+ ); + } return ( -
+
{/* Hero Section */}
@@ -169,18 +252,16 @@ export function Articles() {
- {/* FIXED: Using articlesData.length */} -
{articlesData.length}+
+
{blogsData?.data?.pagination?.total || 0}+
Expert Articles
- {/* FIXED: Using categories from articlesData */}
{categories.length - 1}
Categories
-
25,400
-
Total Reads
+
{allTags.length}+
+
Topics
@@ -197,7 +278,7 @@ export function Articles() { setSearchTerm(e.target.value)} className="pl-10 pr-4 py-3 text-body rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 w-full bg-gray-50" @@ -298,46 +379,37 @@ export function Articles() { - { + const category = categories.find(c => c.id === value); + if (category) { + setSelectedCategory(category); + } + }} + > + + + {selectedCategory.name} + {categories.map((category) => ( - - {category} + + {category.name} ))}
- {/* Author Filter */} -
- - -
- - {/* Read Time Filter */} + {/* Read Time Filter - Client-side only */}
- + - {dateRanges.map((dateRange) => ( - - {dateRange} + {dateRanges.map((range) => ( + + {range.label} ))} @@ -374,17 +446,28 @@ export function Articles() { - { + if (value === 'all') { + setSelectedTopic({ id: 'all', name: 'All Topics' }); + return; + } + const topic = allTags.find(t => t.id === value); + if (topic) setSelectedTopic(topic); + }} + > + - + All Topics + {allTags.map((tag) => ( - - {tag} + + {tag.name} ))} @@ -399,73 +482,85 @@ export function Articles() { {/* Right Content Area - Scrollable Articles */}
- Showing {currentArticles.length} of {filteredArticles.length} articles + Showing {finalFilteredArticles.length} of {blogsData?.data?.pagination?.total || 0} articles
{/* Articles Results */} - {currentArticles.length === 0 ? ( + {finalFilteredArticles.length === 0 ? (
+

No articles found matching your criteria.

+ {hasActiveFilters && ( + + )}
) : ( <> {/* Grid View */} {viewMode === 'grid' && (
- {currentArticles.map((article) => ( + {finalFilteredArticles.map((article: BlogItem) => ( navigateTo(`/learning/articles/${article.slug}`)} + onClick={() => { + // Use slug_name to create the URL with full UUID + const url = getSlugWithId(article.slug_name, article.id); + navigateTo(`/learning/articles/${url}`); + }} >
- {article.featured && ( -
- - Featured - -
- )}
- {article.category} + {article.content_category} - {article.readTime} + + {calculateReadTime(article.content)} +
-

+

{article.title}

- {article.excerpt} + {article.short_description || article.content.substring(0, 150) + '...'}

-
-
- - {article.author} -
+
- {formatDate(article.date)} + {formatDate(article.updated_at)}
+ + {/* Display tags if available */} + {article.blog_tags && article.blog_tags.length > 0 && ( +
+ {article.blog_tags.slice(0, 3).map((tag, idx) => ( + + {tag.tag_name} + + ))} + {article.blog_tags.length > 3 && ( + +{article.blog_tags.length - 3} + )} +
+ )} ))} @@ -475,29 +570,23 @@ export function Articles() { {/* List View */} {viewMode === 'list' && (
- {currentArticles.map((article) => ( + {finalFilteredArticles.map((article: BlogItem) => ( navigateTo(`/learning/articles/${article.slug}`)} + onClick={() => { + // Use slug_name to create the URL with full UUID + const url = getSlugWithId(article.slug_name, article.id); + navigateTo(`/learning/articles/${url}`); + }} >
- {article.featured && ( -
- - Featured - -
- )}
@@ -505,38 +594,39 @@ export function Articles() {
- {article.category} + {article.content_category} - {article.readTime} -
- - {article.views} -
+ + {calculateReadTime(article.content)} +
-

+

{article.title}

- {article.excerpt} + {article.short_description || article.content.substring(0, 200) + '...'}

-
- -
- {article.author} - • {article.authorTitle} -
-
- {formatDate(article.date)} + {formatDate(article.updated_at)}
+ + {/* Display tags if available */} + {article.blog_tags && article.blog_tags.length > 0 && ( +
+ {article.blog_tags.slice(0, 2).map((tag, idx) => ( + + {tag.tag_name} + + ))} + {article.blog_tags.length > 2 && ( + +{article.blog_tags.length - 2} + )} +
+ )}
@@ -592,7 +682,7 @@ export function Articles() { containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }} className={`min-w-10 ${currentPage === page - ? 'bg-blue-600 text-white hover:bg-blue-700' + ? 'bg-[#04045B] text-white hover:bg-[#04045B]' : 'border-gray-300 text-gray-700 hover:bg-gray-50' }`} > diff --git a/src/components/BlogDetail.tsx b/src/components/BlogDetail.tsx index e2a02e9..4d9200c 100644 --- a/src/components/BlogDetail.tsx +++ b/src/components/BlogDetail.tsx @@ -1,21 +1,16 @@ import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; import { Button } from './ui/button'; import { Card, CardContent } from './ui/card'; import { Badge } from './ui/badge'; import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; -import { navigateTo } from './Router'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { CTABannerSection } from './CTABannerSection'; import { useCart } from './CartContext'; -// CRITICAL: Import articlesData from the centralized data file -// DO NOT create a local articlesData variable - it will shadow this import -// All article data must be maintained in /data/articlesData.ts import { Calendar, Clock, - ArrowRight, ChevronUp, - Share2, Bookmark, Twitter, Facebook, @@ -24,202 +19,91 @@ import { Heart, Eye, BookOpen, - ArrowLeft, - Star, - MessageCircle, - Users, - TrendingUp, - Award + ArrowLeft } from 'lucide-react'; -import { articlesData } from '../data/articlesData'; -import { useParams } from 'react-router-dom'; - -interface BlogPost { - id: string; - title: string; - slug: string; - excerpt: string; - content: string; - author: string; - authorBio: string; - authorImage: string; - publishedDate: string; - updatedDate?: string; - readTime: string; - views: number; - likes: number; - category: string; - tags: string[]; - image: string; - featured: boolean; -} - -interface RelatedPost { - id: string; - title: string; - slug: string; - excerpt: string; - author: string; - publishedDate: string; - readTime: string; - category: string; - image: string; -} +import { useGetBlogByIDQuery, useGetBlogsQuery } from '../redux/services/blogApi'; +import { FullScreenLoader } from './FullScreenLoader'; +import { extractIdFromSlug, extractSlugFromSlugAndId, getSlugWithId } from '../utils/urlHelpers'; interface BlogDetailProps { params?: { - slug?: string; + slugAndId?: string; }; } -// NOTE: Using articlesData imported from '../data/articlesData' (line 10) -// DO NOT create a local articlesData variable here as it will shadow the import - -// Function to get blog post by slug -const getBlogPostBySlug = (slug: string): BlogPost => { - console.log('🔍 Searching for slug:', slug); - console.log('📋 Available slugs:', articlesData.map(a => ({ id: a.id, slug: a.slug }))); - - const article = articlesData.find(article => article.slug === slug); - - if (!article) { - console.error('❌ Article not found. Available slugs:', articlesData.map(a => a.slug)); - return { - id: 'not-found', - title: 'Article Not Found', - slug: 'not-found', - excerpt: `The requested article "${slug}" could not be found.`, - content: ` -

We're sorry, but the article you're looking for could not be found. It may have been moved or removed.

-

Available articles:

-
    - ${articlesData.slice(0, 6).map(article => - `
  • ${article.title}
  • ` - ).join('')} -
- `, - author: 'KLC Team', - authorBio: 'The Kautilya Leadership Center team', - authorImage: 'https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop', - publishedDate: '2024-01-01', - updatedDate: '2024-01-01', - readTime: '1 min read', - views: 0, - likes: 0, - category: 'General', - tags: [], - image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop', - featured: false - }; - } - - console.log('✅ Article found:', article.title); - - return { - id: article.id, - title: article.title, - slug: article.slug, - excerpt: article.excerpt || '', - content: article.content || '', - author: article.author || 'Unknown Author', - authorBio: article.authorBio || `${article.author} is a contributor to Kautilya Leadership Center.`, - authorImage: article.authorAvatar || 'https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop', - publishedDate: article.date || '2024-01-01', - updatedDate: article.date || '2024-01-01', - readTime: article.readTime || '5 min read', - views: parseInt((article.views || '0').replace('k', '00').replace('.', '')) || 0, - likes: article.likes || 0, - category: article.category || 'General', - tags: article.tags || [], - image: article.thumbnail || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop', - featured: article.featured || false - }; -}; - -// Generate related posts dynamically from articlesData -const getRelatedPosts = (currentSlug: string, currentCategory: string): RelatedPost[] => { - // Filter articles by same category or exclude current article - const related = articlesData - .filter(article => article.slug !== currentSlug) - .slice(0, 3) - .map(article => ({ - id: article.id, - title: article.title, - slug: article.slug, - excerpt: article.excerpt, - author: article.author, - publishedDate: article.date, - readTime: article.readTime, - category: article.category, - image: article.thumbnail - })); - - return related; -}; - -const relatedPosts: RelatedPost[] = [ - { - id: '5', - title: 'A New Lens on Leadership', - slug: 'new-lens-on-leadership', - excerpt: 'Your leadership calls, and how you interpret opportunities and threats, are influenced by your lenses, which are unique and personal to you.', - author: 'K Ramkumar', - publishedDate: '2016-08-16', - readTime: '12 min read', - category: 'Leadership Philosophy', - image: 'https://images.unsplash.com/photo-1560550900-5c10828c40aa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsZWFkZXJzaGlwJTIwbGVucyUyMHBlcnNwZWN0aXZlfGVufDF8fHx8MTc1OTk5NTg0N3ww&ixlib=rb-4.1.0&q=80&w=400' - }, - { - id: '2', - title: 'Inspiration – the magic potion to leadership', - slug: 'inspiration-magic-potion-leadership', - excerpt: 'Explore how inspiration serves as a fundamental catalyst for effective leadership, transforming both leaders and their teams toward extraordinary achievements.', - author: 'Ramkumar Krishnaswamy', - publishedDate: '2017-10-13', - readTime: '7 min read', - category: 'Leadership Philosophy', - image: 'https://images.unsplash.com/photo-1559027615-cd4628902d4a?w=400&h=300&fit=crop' - }, - { - id: '3', - title: 'Lessons on Leadership Paradox', - slug: 'lessons-leadership-paradox', - excerpt: 'Uncover the inherent paradoxes in leadership and learn how embracing these contradictions can make you a more effective and authentic leader.', - author: 'Ramkumar Krishnaswamy', - publishedDate: '2017-08-10', - readTime: '7 min read', - category: 'Leadership Strategy', - image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=300&fit=crop' - } -]; - export function BlogDetail({ params }: BlogDetailProps) { - const { slug } = useParams<{ slug: string }>(); + const { slugAndId } = useParams<{ slugAndId: string }>(); + const navigate = useNavigate(); const [scrollProgress, setScrollProgress] = useState(0); const [showBackToTop, setShowBackToTop] = useState(false); const [isLiked, setIsLiked] = useState(false); const [isBookmarked, setIsBookmarked] = useState(false); const { addToCart } = useCart(); - - // Get blog post data based on slug - const blogPost = getBlogPostBySlug(slug || ""); + + // Extract full ID from URL using the new function + const fullId = slugAndId ? extractIdFromSlug(slugAndId) : null; + const urlSlug = slugAndId ? extractSlugFromSlugAndId(slugAndId) : ''; + + // Fetch blog details by ID directly - no dependency on list API + const { + data: blogPost, + isLoading: isLoadingBlog, + isError: isBlogError, + refetch: refetchBlog + } = useGetBlogByIDQuery(fullId as string, { + skip: !fullId, + refetchOnMountOrArgChange: true, + }); + + // Fetch related blogs (excluding current blog) + const { + data: relatedBlogsData, + isLoading: isLoadingRelated + } = useGetBlogsQuery({ + limit: 3, + offset: 0, + content_status: 'publish', + content_type: 'BLOG', + }, { + skip: !fullId, + }); + + // SEO: Check if URL slug matches the actual slug_name and redirect if needed + useEffect(() => { + if (blogPost && fullId) { + // Get the expected slug from the blog post + const expectedSlug = blogPost.slug_name; + // Create the expected URL with proper formatting + const expectedUrl = getSlugWithId(expectedSlug, fullId); + // Get the current URL slug + const currentSlugAndId = slugAndId || ''; + + // Compare (case-insensitive) + if (currentSlugAndId.toLowerCase() !== expectedUrl.toLowerCase()) { + // Redirect to the correct URL + navigate(`/learning/articles/${expectedUrl}`, { replace: true }); + } + } + }, [blogPost, fullId, slugAndId, navigate]); useEffect(() => { - document.title = `${blogPost.title} | KLC Blog`; + if (blogPost?.title) { + document.title = `${blogPost.title} | KLC Blog`; + } window.scrollTo(0, 0); - + const handleScroll = () => { const winScroll = document.body.scrollTop || document.documentElement.scrollTop; const height = document.documentElement.scrollHeight - document.documentElement.clientHeight; const scrolled = (winScroll / height) * 100; - + setScrollProgress(scrolled); setShowBackToTop(winScroll > 300); }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); - }, [blogPost.title]); + }, [blogPost?.title]); const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('en-US', { @@ -231,8 +115,8 @@ export function BlogDetail({ params }: BlogDetailProps) { const handleShare = (platform: string) => { const url = window.location.href; - const title = blogPost.title; - + const title = blogPost?.title || ''; + const shareUrls = { twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`, facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, @@ -252,13 +136,71 @@ export function BlogDetail({ params }: BlogDetailProps) { window.scrollTo({ top: 0, behavior: 'smooth' }); }; + // Calculate read time based on content length (approx) + const calculateReadTime = (content: string): string => { + const wordsPerMinute = 200; + const wordCount = content.split(/\s+/).length; + const minutes = Math.ceil(wordCount / wordsPerMinute); + return `${minutes} min read`; + }; + + // Filter related blogs (exclude current blog) + const relatedPosts = relatedBlogsData?.data?.items + ?.filter((blog: any) => blog.id !== fullId) + .map((blog: any) => ({ + id: blog.id, + title: blog.title, + slug: blog.slug_name, + excerpt: blog.short_description || blog.content.substring(0, 150) + '...', + author: blog.author_name || 'KLC Team', + publishedDate: blog.updated_at, + readTime: calculateReadTime(blog.content), + category: blog.content_category, + image: blog.banner_img || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=400&h=300&fit=crop' + })) || []; + + // Handle related post click - use full UUID + const handleRelatedPostClick = (postSlug: string, postId: string) => { + const url = getSlugWithId(postSlug, postId); + navigate(`/learning/articles/${url}`); + }; + + // Handle loading state + if (isLoadingBlog) { + return ( +
+ +
+ ); + } + + // Handle error state + if (isBlogError || !blogPost) { + return ( +
+
+
+ +
+

Article Not Found

+

+ The article you're looking for could not be found. It may have been moved or removed. +

+ +
+
+ ); + } + return (
{/* Scroll Progress Bar */}
-
navigateTo('/learning/articles')} + onClick={() => navigate('/learning/articles')} className="p-0 h-auto font-medium hover:bg-transparent transition-colors" style={{ color: '#6F6F6F' }} > @@ -295,7 +237,7 @@ export function BlogDetail({ params }: BlogDetailProps) { Back to Articles - {blogPost.category} + {blogPost.content_category || 'Article'}
@@ -307,64 +249,64 @@ export function BlogDetail({ params }: BlogDetailProps) {
{/* Category Badge */}
- - {blogPost.category} + {blogPost.content_category || 'Article'} - + {/* Improved Typography Hierarchy */}

{blogPost.title}

- + {/* Constrained Width Excerpt for Better Readability */}

- {blogPost.excerpt} + {blogPost.short_description || blogPost.content.substring(0, 200) + '...'}

{/* Enhanced Meta Bar with Cleaner Spacing */} -
{/* Author Info with Improved Layout */}
- - {blogPost.author.split(' ').map(n => n[0]).join('')} + + KLC
- {blogPost.author} + KLC Team
- + {/* Cleaner Meta Information with Subtle Dividers */}
- {formatDate(blogPost.publishedDate)} + {formatDate(blogPost.updated_at || new Date().toISOString())} - +
- + - {blogPost.readTime} + {calculateReadTime(blogPost.content)} - +
- + - {blogPost.views.toLocaleString()} + 0
@@ -379,9 +321,9 @@ export function BlogDetail({ params }: BlogDetailProps) { className={`transition-colors ${isLiked ? 'text-red-500' : 'text-[#6F6F6F]'}`} > - {blogPost.likes + (isLiked ? 1 : 0)} + 0 - + - + {/* Share Options */}
+ )} {/* CTA Section */} diff --git a/src/components/CTABannerSection.tsx b/src/components/CTABannerSection.tsx index 23bb951..5f995ca 100644 --- a/src/components/CTABannerSection.tsx +++ b/src/components/CTABannerSection.tsx @@ -4,14 +4,45 @@ import { BrandedTag } from "./about/BrandedTag"; import { PrimaryCTAButton } from "./PrimaryCTAButton"; import { navigateTo } from "./Router"; -export function CTABannerSection() { +interface CTABannerSectionProps { + ctaBands?: Array<{ + id: string; + background_image_url: string; + background_image_alt_text: string; + text: string; + cta_text: string; + cta_destination: string; + }>; + isLoading?: boolean; +} + +export function CTABannerSection({ ctaBands = [], isLoading }: CTABannerSectionProps) { + // Get the first CTA band or use default values + const ctaBand = ctaBands && ctaBands.length > 0 ? ctaBands[0] : null; + + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } + + // If no CTA band is available, don't render anything + if (!ctaBand) { + return null; + } + return (
{/* Background Image */}
@@ -34,11 +65,11 @@ export function CTABannerSection() { {/* Branded Tag */} - {/* Main Headline */} + {/* Main Headline - Use API text or fallback */}

- Ready to transform your leadership? + {ctaBand.text || "Ready to transform your leadership?"} - {/* CTA Button - Updated to redirect to contact page */} + {/* CTA Button */} navigateTo('/contact?topic=consulting')} + text={ctaBand.cta_text || "Schedule a Consultation"} + onClick={() => navigateTo(ctaBand.cta_destination || '/contact?topic=consulting')} ariaLabel="Schedule a consultation with our leadership experts" className="cta-banner-yellow" /> diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 75077a5..d29ac3c 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -9,6 +9,7 @@ import { PrimaryCTAButton } from './PrimaryCTAButton'; import { ImageWithFallback } from './figma/ImageWithFallback'; import { navigateTo } from './Router'; import kautilyabg from '../assets/Kautilya.png'; +import { useCreateLeadMutation, useGetLeadCategoriesQuery } from '../redux/services/contactUsApi'; interface ContactProps { topic?: string; @@ -19,13 +20,23 @@ export function Contact({ topic }: ContactProps) { name: '', mobileNumber: '', emailId: '', - interestedIn: topic || '', + interestedIn: '', message: '' }); - + const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + const { data: categoryData } = useGetLeadCategoriesQuery({ + limit: 20, + offset: 0, + status: "active" + }); + + const categories = categoryData?.data?.items || []; + + const [createLead] = useCreateLeadMutation(); + useEffect(() => { console.log('Contact component mounted with topic:', topic); // Set default interested in based on topic parameter @@ -65,10 +76,22 @@ export function Contact({ topic }: ContactProps) { e.preventDefault(); setIsSubmitting(true); - // Simulate form submission - await new Promise(resolve => setTimeout(resolve, 2000)); - - setIsSubmitted(true); + try { + await createLead({ + full_name: formData.name, + phone_number: formData.mobileNumber, + email_address: formData.emailId, + lead_category_xid: formData.interestedIn, + message: formData.message, + lead_status: "NEW" + }).unwrap(); + + setIsSubmitted(true); + + } catch (error) { + console.error("Lead creation failed", error); + } + setIsSubmitting(false); }; @@ -80,16 +103,21 @@ export function Contact({ topic }: ContactProps) {
+

Thank You!

+

- We've received your message and will get back to you within 24 hours. + We've received your message and will get back to you within 24 hours. Our leadership development experts are excited to help transform your organization.

- navigateTo('/')} - ariaLabel="Return to homepage" - /> + +
+ navigateTo('/')} + ariaLabel="Return to homepage" + /> +

@@ -107,7 +135,7 @@ export function Contact({ topic }: ContactProps) { alt="Google Maps showing office location and navigation" className="w-full h-full object-cover" /> - + {/* Dark overlay for better text readability */}
@@ -140,7 +168,7 @@ export function Contact({ topic }: ContactProps) {
{/* Contact Info Header */} -
@@ -154,7 +182,7 @@ export function Contact({ topic }: ContactProps) {
{/* Corporate Office */}
-
@@ -173,7 +201,7 @@ export function Contact({ topic }: ContactProps) { {/* Registered Office */}
-
@@ -192,7 +220,7 @@ export function Contact({ topic }: ContactProps) { {/* Email */}
-
@@ -209,7 +237,7 @@ export function Contact({ topic }: ContactProps) { {/* Phone */}
-
@@ -226,7 +254,7 @@ export function Contact({ topic }: ContactProps) {
{/* Quick Actions */} -
@@ -236,7 +264,7 @@ export function Contact({ topic }: ContactProps) { variant="outline" className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300" onClick={() => navigateTo('/services/learning-facility')} - style={{ + style={{ borderColor: 'var(--color-brand-primary)', color: 'var(--color-brand-primary)' }} @@ -248,7 +276,7 @@ export function Contact({ topic }: ContactProps) { variant="outline" className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300" onClick={() => navigateTo('/leadership-journey')} - style={{ + style={{ borderColor: 'var(--color-brand-primary)', color: 'var(--color-brand-primary)' }} @@ -267,7 +295,7 @@ export function Contact({ topic }: ContactProps) { {/* Form Header */}
-
@@ -279,7 +307,7 @@ export function Contact({ topic }: ContactProps) { Fill out the form below and we'll get back to you within 24 hours.

- + {/* Form Content */}
@@ -294,7 +322,7 @@ export function Contact({ topic }: ContactProps) { onChange={(e) => handleInputChange('name', e.target.value)} placeholder="Enter your full name" className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500" - style={{ + style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-body)' }} @@ -312,7 +340,7 @@ export function Contact({ topic }: ContactProps) { onChange={(e) => handleInputChange('mobileNumber', e.target.value)} placeholder="+91 98765 43210" className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500" - style={{ + style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-body)' }} @@ -330,7 +358,7 @@ export function Contact({ topic }: ContactProps) { onChange={(e) => handleInputChange('emailId', e.target.value)} placeholder="example@company.com" className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500" - style={{ + style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-body)' }} @@ -345,9 +373,9 @@ export function Contact({ topic }: ContactProps) { value={formData.interestedIn} onValueChange={(value: string) => handleInputChange('interestedIn', value)} > - - Leadership Development - Executive Coaching - Management Development - Culture Competence - Consulting Services - Learning Facility - Online Courses - General Inquiry + + {categories.map((cat: any) => ( + + {cat.category_type} + + ))} +
@@ -378,7 +405,7 @@ export function Contact({ topic }: ContactProps) { placeholder="Please enter your message or inquiry..." rows={6} className="w-full border-gray-200 focus:border-blue-500 focus:ring-blue-500 resize-none" - style={{ + style={{ fontFamily: 'var(--font-family-base)', fontSize: 'var(--font-body)' }} @@ -392,7 +419,7 @@ export function Contact({ topic }: ContactProps) {
{}} + onClick={() => { handleSubmit }} ariaLabel="Send contact message" className="w-full sm:w-auto" /> @@ -415,19 +442,19 @@ export function Contact({ topic }: ContactProps) { alt="Professional team collaborating in modern office" className="w-full h-full object-cover" /> - + {/* Subtle dark overlay for overall image */}
- +
{/* Content Container */}
{/* CTA Content Block */} -
@@ -436,8 +463,8 @@ export function Contact({ topic }: ContactProps) { {/* Main Headline */}

- Ready to transform your leadership? - @@ -446,7 +473,7 @@ export function Contact({ topic }: ContactProps) { to start your development journey now.

- navigateTo('/contact?topic=consulting')} ariaLabel="Schedule a consultation with our leadership experts" diff --git a/src/components/FooterNew.tsx b/src/components/FooterNew.tsx index 157b742..ef22463 100644 --- a/src/components/FooterNew.tsx +++ b/src/components/FooterNew.tsx @@ -72,7 +72,7 @@ export function FooterNew() { About Us + +
+ {currentScene.title} +
+ + +
+
+
+ + {/* Info Panel - Bottom Left */} +
+

{currentScene.title}

+
+ 🎯 Capacity: {currentScene.capacity} +
+

+ {currentScene.description} +

+
+ {currentScene.features.map((feature, idx) => ( + + ✓ {feature} + + ))} +
+ +
+ + {/* Scene Thumbnails - Bottom Right */} +
+ {tourScenes.map((scene, idx) => ( + + ))} +
+ + {/* Loading Overlay */} + {isLoading && ( +
+
+
+

Loading 360° Experience...

+
+
+ )} + + {/* Instruction Hint */} +
+ 🖱️ Drag to look around • 👆 Pinch/Scroll to zoom in/out +
+
+
+ ); +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index 6c34d6c..9cbf77c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,15 @@ -// declarations.d.ts +/// +// Vite ENV typing +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +// Image modules declare module "*.png" { const src: string; export default src; @@ -23,4 +33,4 @@ declare module "*.svg" { export { ReactComponent }; const src: string; export default src; -} +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 02bf0f5..b69fa08 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,9 +4,14 @@ import "../src/styles/globals.css"; import { BrowserRouter } from "react-router-dom"; import ScrollToTop from "./components/ScrollToTop"; +import { Provider } from "react-redux"; +import { store } from "./redux/store/Store"; + createRoot(document.getElementById("root")!).render( - - - - + + + + + + ); \ No newline at end of file diff --git a/src/pages/FAQ.tsx b/src/pages/FAQ.tsx index 5c04778..290f64a 100644 --- a/src/pages/FAQ.tsx +++ b/src/pages/FAQ.tsx @@ -1,16 +1,45 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Plus, Minus, HelpCircle, Mail } from 'lucide-react'; import { PrimaryCTAButton } from '../components/PrimaryCTAButton'; import { useNavigate } from 'react-router-dom'; +import { useGetFaqsQuery, useGetFaqCategoriesQuery } from '../redux/services/faqApi'; interface FAQItemProps { question: string; answer: string; isOpen: boolean; onToggle: () => void; + tags?: Array<{ tag_name: string; display_order: number }>; } -const FAQItem: React.FC = ({ question, answer, isOpen, onToggle }) => { +interface FAQItemData { + id: string; + question: string; + answer: string; + content_status: string; + content_category_xid: string; + content_category: { + id: string; + category_name: string; + }; + faq_tags: Array<{ + id: string; + tag_name: string; + display_order: number; + }>; + created_at: string; + updated_at: string; +} + +interface CategoryData { + id: string; + category_name: string; + display_order: number; + for_faq: boolean; + for_blog: boolean; +} + +const FAQItem: React.FC = ({ question, answer, isOpen, onToggle, tags }) => { return (
- -
-

+

{answer}

+ + {/* Display tags if available */} + {tags && tags.length > 0 && ( +
+ {tags.map((tag, index) => ( + + {tag.tag_name} + + ))} +
+ )}
); }; export function FAQ() { - const [openItems, setOpenItems] = useState([]); + const [openItems, setOpenItems] = useState([]); const [activeCategory, setActiveCategory] = useState('all'); + const [filteredFaqs, setFilteredFaqs] = useState([]); const navigate = useNavigate(); - const toggleItem = (index: number) => { - setOpenItems(prev => - prev.includes(index) - ? prev.filter(i => i !== index) - : [...prev, index] + // Fetch FAQ categories + const { + data: categoriesResponse, + isLoading: isLoadingCategories + } = useGetFaqCategoriesQuery({ + limit: 100, + offset: 0 + }); + + // Fetch FAQs with publish status + const { + data: faqResponse, + isLoading: isLoadingFaqs, + isError, + refetch + } = useGetFaqsQuery({ + content_status: 'publish', + limit: 20 + }); + + // Filter categories to only those marked for FAQ + const categories = [ + { id: 'all', label: 'All Questions' }, + ...(categoriesResponse?.data?.items + ?.filter((category: CategoryData) => category.for_faq) + .map((category: CategoryData) => ({ + id: category.id, // Use the actual category ID + label: category.category_name, + display_order: category.display_order + })) + .sort((a:any, b:any) => (a.display_order || 0) - (b.display_order || 0)) || []) + ]; + + // Filter FAQs based on active category + useEffect(() => { + if (faqResponse?.data?.items) { + if (activeCategory === 'all') { + setFilteredFaqs(faqResponse.data.items); + } else { + // Filter by content_category_xid using the actual category ID + const filtered = faqResponse.data.items.filter( + (item: FAQItemData) => item.content_category_xid === activeCategory + ); + setFilteredFaqs(filtered); + } + } + }, [activeCategory, faqResponse]); + + const toggleItem = (id: string) => { + setOpenItems(prev => + prev.includes(id) + ? prev.filter(i => i !== id) + : [...prev, id] ); }; - const categories = [ - { id: 'all', label: 'All Questions' }, - { id: 'getting-started', label: 'Getting Started' }, - { id: 'membership', label: 'Members and Pricing' }, - { id: 'requests', label: 'Book Requests and Recommendations' }, - { id: 'account', label: 'Account & Technical Issues' } - ]; + // Handle loading state + if (isLoadingFaqs || isLoadingCategories) { + return ( +
+
+
+

Loading FAQs...

+
+
+ ); + } - const faqData = [ - { - question: "How do I sign up for an account?", - answer: "Signing up is easy! Just download the app, click on 'Sign Up' and follow the prompts. You can use your email address, Google, or Facebook to create an account.", - category: 'getting-started' - }, - { - question: "What are the membership club packages?", - answer: "We offer subscription packages to our learners which allow you to avail of the different products/services available at a discounted rate.", - category: 'membership' - }, - { - question: "Can I buy the products without purchasing the membership package?", - answer: "Yes. You can either buy individual products separately or avail of our subscription packages to get attractive discounts.", - category: 'membership' - }, - { - question: "Can I change my subscription plan mid-way?", - answer: "Yes, you can change your subscription to opt for a higher package. You would be required to pay a differential amount.", - category: 'membership' - }, - { - question: "What is auto-renewal?", - answer: "Each plan can be setup for auto-renewal. A transaction of Rs. 5 is charged for this by the payment vendor. You can discontinue the auto-renewal at any point.", - category: 'membership' - }, - { - question: "How can I cancel or change my order?", - answer: "Just drop an email to connect@leadershipcentre.in with the reference number of your purchase order. Wherever applicable (in case we have not shipped it yet), we will cancel your order. Alternatively, you will have to send the details to us and we will offer you a refund or exchange.", - category: 'requests' - }, - { - question: "Does Leadership Centre provide cash on delivery?", - answer: "We do not offer cash on delivery (COD).", - category: 'requests' - }, - { - question: "Do you accept returns?", - answer: "As our products / services are electronic in nature and access is granted soon as your payment is received/recorded, return of a product / service is not enabled. However, if you wish to cancel an order after effecting payment, you may do so prior to viewing / downloading / consuming the product / service purchased within 15 days (from the date of purchase and payment).", - category: 'requests' - }, - { - question: "I have paid via netbanking/debit/credit card, how will I get refund?", - answer: "We shall be refunding the money back into the same account via which you have paid for your purchase on Leadership Centre.", - category: 'account' - }, - { - question: "How do I reset my password?", - answer: "To reset your password, go to the login screen and click 'Forgot Password'. Enter your email address and we'll send you a secure link to reset your password.", - category: 'account' - } - ]; - - const filteredFAQs = activeCategory === 'all' - ? faqData - : faqData.filter(faq => faq.category === activeCategory); + // Handle error state + if (isError) { + return ( +
+
+ +

Failed to load FAQs

+

Please try again later

+ refetch()} + className="cta-text-black" + /> +
+
+ ); + } return (
@@ -135,49 +191,63 @@ export function FAQ() {

FAQs

- + {/* Description */}

Everything you need to know about features, membership, and troubleshooting.

- {/* Category Filter Tags */} -
- {categories.map((category) => ( - - ))} -
+ {/* Category Filter Tags - Only show if there are categories */} + {categories.length > 1 && ( +
+ {categories.map((category) => ( + + ))} +
+ )}
{/* FAQ Section */}
-
- {filteredFAQs.map((faq, index) => ( - toggleItem(index)} - /> - ))} -
+ {filteredFaqs.length > 0 ? ( +
+ {filteredFaqs.map((faq) => ( + toggleItem(faq.id)} + /> + ))} +
+ ) : ( +
+ +

No FAQs found

+

+ {activeCategory === 'all' + ? 'No published FAQs available at the moment.' + : 'No FAQs available in this category.'} +

+
+ )}
@@ -188,24 +258,24 @@ export function FAQ() {
- +

Still have questions?

- +

- Can't find the answer you're looking for? Our support team is here to help you with any questions + Can't find the answer you're looking for? Our support team is here to help you with any questions about our leadership development programs and services.

- +
navigate('/contact')} className="cta-text-black" /> - - { const navigate = useNavigate(); + + const { data, isLoading } = useGetHomepageQuery({ + landing_page_type: "home", + }); + + const heroSections = data?.hero_sections ?? []; + const stats = data?.stats_sections ?? []; + const highlightCards = data?.highlight_cards ?? []; + const ctaBands = data?.cta_bands ?? []; + return ( <> - - + + + {/* Stats Section */} + + - + +
-
- {/* Branded Tag */} +
+ { /> - {/* Main Heading */} { Experience Our Space Virtually - {/* Subheading */} { transition={{ duration: 0.8, delay: 0.4 }} viewport={{ once: true }} > - Take a virtual walk through our state-of-the-art facility designed to inspire leadership excellence and foster collaborative learning. + Take a virtual walk through our state-of-the-art facility designed to + inspire leadership excellence and foster collaborative learning. - {/* Main CTA Button - Explore Our Space */} {
navigate('/services/learning-facility')} + onClick={() => navigate("/services/learning-facility")} ariaLabel="Explore our virtual learning space and facilities" />
+
+ - {/* */} - {/* */} - + ); }; diff --git a/src/pages/HomePageNew.tsx b/src/pages/HomePageNew.tsx deleted file mode 100644 index bd85fcf..0000000 --- a/src/pages/HomePageNew.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -// import { navigateTo } from './Router'; -import { navigateTo } from '../components/Router'; -import svgPaths from "../imports/svg-i1joeov37f"; -import { StatsSection } from '../components/StatsSection'; -import { LogosSection } from '../components/LogosSection'; -import { ServicesSection } from '../components/ServicesSection'; -import { ServicesSectionNew } from '../components/ServiceSectionNew'; -import { LearningEnvionment } from '../components/LearningEnvionment'; -import { motion } from "motion/react"; -import { BrandedTag } from '../components/about/BrandedTag'; -import { PrimaryCTAButton } from '../components/PrimaryCTAButton'; -import { VirtualSpaceSection } from '../components/VirtualSpaceSection'; -import { useNavigate } from 'react-router-dom'; -import HeroSection from '../components/HeroSection'; -interface SlideData { - id: number; - title: string; - backgroundImage: string; - shortTitle: string; - ctaText: string; -} - -export default function HomePageNew() { - - - - - - - - - - - const navigate = useNavigate() - return ( - <> - - - - - {/* */} -
-
- {/* Branded Tag */} - - - - - {/* Main Heading */} - - Experience Our Space Virtually - - - {/* Subheading */} - - Take a virtual walk through our state-of-the-art facility designed to inspire leadership excellence and foster collaborative learning. - - - {/* Main CTA Button - Explore Our Space */} - -
- navigate('/services/learning-facility')} - ariaLabel="Explore our virtual learning space and facilities" - /> -
-
-
- -
- - - ); -} \ No newline at end of file diff --git a/src/components/hooks/useAnimatedCounter.tsx b/src/redux/hooks/useAnimatedCounter.tsx similarity index 100% rename from src/components/hooks/useAnimatedCounter.tsx rename to src/redux/hooks/useAnimatedCounter.tsx diff --git a/src/redux/services/aboutUsApi.ts b/src/redux/services/aboutUsApi.ts new file mode 100644 index 0000000..a0c195f --- /dev/null +++ b/src/redux/services/aboutUsApi.ts @@ -0,0 +1,82 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import baseQueryWithReauth from "./baseQuery"; + + +export interface HeroSection { + id: string; + background_image_url: string; + background_image_alt_text: string; + headline: string; + subtext: string; + cta_text: string; + cta_destination: string; +} + +export interface HowWeWorkItem { + id: string; + title: string; + description: string; + image_url: string; + display_order: number; +} + +export interface StatItem { + id: string; + number: number; + suffix: string; + label: string; + display_order: number; +} + +export interface TeamMember { + id: string; + display_order: number; + name_role: string; + photo_url: string; + alt_text: string; + bio: string; +} + +export interface AboutUsData { + hero_section: HeroSection; + our_promise_title: string; + how_we_work_title: string; + who_we_are_title: string; + our_team_title: string; + how_we_work: HowWeWorkItem[]; + stat_section: StatItem[]; + our_team: TeamMember[]; +} + +export interface AboutUsResponse { + success: boolean; + status: number; + message: string; + data: AboutUsData; +} + +export const aboutUsApi = createApi({ + reducerPath: "aboutUsApi", + baseQuery: baseQueryWithReauth, + tagTypes: ["AboutUs"], + endpoints: (builder) => ({ + + // ✅ GET About Us + getAboutUs: builder.query({ + query: () => ({ + url: "/admin/about-us", + method: "GET", + }), + + // 🔥 extract only useful data + transformResponse: (response: AboutUsResponse) => response.data, + + providesTags: ["AboutUs"], + }), + + }), +}); + +export const { + useGetAboutUsQuery, +} = aboutUsApi; \ No newline at end of file diff --git a/src/services/baseQuery.ts b/src/redux/services/baseQuery.ts similarity index 73% rename from src/services/baseQuery.ts rename to src/redux/services/baseQuery.ts index 8e74e0f..b853706 100644 --- a/src/services/baseQuery.ts +++ b/src/redux/services/baseQuery.ts @@ -1,14 +1,12 @@ import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'; const rawBaseQuery = fetchBaseQuery({ - baseUrl: 'http://localhost:3000/api', + baseUrl: import.meta.env.VITE_API_URL, }); const baseQueryWithReauth = async (args: any, api: any, extraOptions: any) => { const result = await rawBaseQuery(args, api, extraOptions); - // Optional: reauthentication logic if result.error?.status === 401 - return result; }; diff --git a/src/redux/services/blogApi.ts b/src/redux/services/blogApi.ts new file mode 100644 index 0000000..3ca93f2 --- /dev/null +++ b/src/redux/services/blogApi.ts @@ -0,0 +1,126 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import baseQueryWithReauth from "./baseQuery"; + +export interface BlogTag { + id: string; + blog_xid: string; + tag_name: string; + display_order: number; +} + +export interface BlogItem { + id: string; + content_category: string; + content_type: string; + title: string; + slug_name: string; + content: string; + banner_img: string; + meta_title: string; + meta_description: string; + content_status: string; + updated_at: string; + blog_tags: BlogTag[]; + short_description: string | null; + content_category_id?: string; // Add this field to store the category ID +} + +export interface Pagination { + limit: number; + offset: number; + total: number; +} + +export interface BlogListResponse { + success: boolean; + status: number; + message: string; + data: { + pagination: Pagination; + items: BlogItem[]; + }; + errors: any; + correlation_id: string; +} + +export interface BlogListParams { + limit?: number; + offset?: number; + search?: string; + content_status?: string; + content_type?: string; + date_range?: + | "all_time" + | "last_7_days" + | "last_30_days" + | "last_3_months" + | "last_6_months"; + sort_by?: "most_recent" | "oldest_first" | "title_az"; + content_category_id?: string; // Changed from category to content_category_id + tag_id?: string; +} + +export interface BlogByIdResponse { + success: boolean; + status: number; + message: string; + data: BlogItem; + errors: any; + correlation_id: string; +} + +export const blogApi = createApi({ + reducerPath: "blogApi", + baseQuery: baseQueryWithReauth, + tagTypes: ["blog"], + endpoints: (builder) => ({ + // ✅ GET BLOGS LIST + getBlogs: builder.query({ + query: ({ + limit = 10, + offset = 0, + search, + content_status = "publish", + content_type, + date_range, + sort_by = "most_recent", + content_category_id, + tag_id, + }) => { + // Build params object + const params: Record = { + limit, + offset, + sort_by, + }; + + // Only add params if they have values + if (search) params.search = search; + if (content_status) params.content_status = content_status; + if (content_type) params.content_type = content_type; + if (date_range) params.date_range = date_range; + if (content_category_id) + params.content_category_id = content_category_id; // Send UUID + if (tag_id && tag_id !== "all") params.tag_id = tag_id; + + return { + url: "/admin/blogs/list", + method: "GET", + params, + }; + }, + providesTags: ["blog"], + }), + + getBlogByID: builder.query({ + query: (id) => ({ + url: `/admin/blogs/list/${id}`, + method: "GET", + }), + transformResponse: (response: BlogByIdResponse) => response.data, + providesTags: ["blog"], + }), + }), +}); + +export const { useGetBlogsQuery, useGetBlogByIDQuery } = blogApi; diff --git a/src/redux/services/contactUsApi.ts b/src/redux/services/contactUsApi.ts new file mode 100644 index 0000000..4fc7479 --- /dev/null +++ b/src/redux/services/contactUsApi.ts @@ -0,0 +1,39 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import baseQueryWithReauth from "./baseQuery"; + +export const contactUsApi = createApi({ + reducerPath: "contactUsApi", + baseQuery: baseQueryWithReauth, + tagTypes: ["LeadCategories", "Leads"], + endpoints: (builder) => ({ + + // GET Lead Categories + getLeadCategories: builder.query({ + query: ({ limit = 10, offset = 0, status = "active" }) => ({ + url: "admin/prepopulate/lead-categories/list", + params: { + limit, + offset, + status, + }, + }), + providesTags: ["LeadCategories"], + }), + + // CREATE Lead + createLead: builder.mutation({ + query: (body) => ({ + url: "admin/leads/create", + method: "POST", + body, + }), + invalidatesTags: ["Leads"], + }), + + }), +}); + +export const { + useGetLeadCategoriesQuery, + useCreateLeadMutation, +} = contactUsApi; \ No newline at end of file diff --git a/src/redux/services/faqApi.ts b/src/redux/services/faqApi.ts new file mode 100644 index 0000000..2c8b5c4 --- /dev/null +++ b/src/redux/services/faqApi.ts @@ -0,0 +1,44 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import baseQueryWithReauth from "./baseQuery"; + +export const faqApi = createApi({ + reducerPath: "faqApi", + baseQuery: baseQueryWithReauth, + tagTypes: ["Faq", "FaqTags"], + endpoints: (builder) => ({ + + // GET FAQs LIST + getFaqs: builder.query({ + query: ({ limit = 10, offset = 0, search_term, content_status, content_category_xid }) => ({ + url: "admin/faq/list", + params: { + limit, + offset, + search_term, + content_status, + content_category_xid, + }, + }), + providesTags: ["Faq"], + }), + + // GET category TAGS LIST + getFaqCategories: builder.query({ + query: ({ limit = 10, offset = 0, search_query }) => ({ + url: "admin/prepopulate/content-categories/list", + params: { + limit, + offset, + search_query, + }, + }), + providesTags: ["FaqTags"], + }), + + }), +}); + +export const { + useGetFaqsQuery, + useGetFaqCategoriesQuery, +} = faqApi; \ No newline at end of file diff --git a/src/redux/services/homepageApi.ts b/src/redux/services/homepageApi.ts new file mode 100644 index 0000000..817150e --- /dev/null +++ b/src/redux/services/homepageApi.ts @@ -0,0 +1,92 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import baseQueryWithReauth from "./baseQuery"; + +/* ================= HERO TYPES ================= */ + +export interface HeroSection { + id: string; + landing_page_type: string; + background_image_url: string; + background_image_alt_text: string; + headline: string; + subtext: string; + cta_text: string; + cta_destination: string; +} + +/* ================= STATS TYPES ================= */ + +export interface StatItem { + id: string; + landing_page_type: string; + number: number; + suffix: string; + label: string; + display_order: number; +} + +/* ================= HIGHLIGHT CARD ================= */ + +export interface HighlightCard { + card_title: string; + icon_url: string; + accessible_label: string; + body_text: string; + display_order: number; +} + +/* ================= CTA BAND ================= */ + +export interface CtaBand { + id: string; + background_image_url: string; + background_image_alt_text: string; + text: string; + cta_text: string; + cta_destination: string; +} + +/* ================= RESPONSE ================= */ + +export interface HomePageResponse { + success: boolean; + status: number; + message: string; + data: { + hero_sections: HeroSection[]; + stats_sections: StatItem[]; + highlight_cards: HighlightCard[]; + cta_bands: CtaBand[]; + }; + errors: any; + correlation_id: string; +} + +/* ================= API ================= */ + +export const homepageApi = createApi({ + reducerPath: "homepageApi", + baseQuery: baseQueryWithReauth, + tagTypes: ["Homepage"], + endpoints: (builder) => ({ + + getHomepage: builder.query< + HomePageResponse["data"], + { landing_page_type: "home" | "services" | "about_us" } + >({ + query: ({ landing_page_type }) => ({ + url: "/admin/home-page/list", + params: { landing_page_type }, + }), + + transformResponse: (response: HomePageResponse) => response.data, + + providesTags: [{ type: "Homepage", id: "LIST" }], + }), + + }), +}); + +/* ================= HOOKS ================= */ + +export const { useGetHomepageQuery } = homepageApi; \ No newline at end of file diff --git a/src/redux/store/Store.tsx b/src/redux/store/Store.tsx new file mode 100644 index 0000000..29dc1df --- /dev/null +++ b/src/redux/store/Store.tsx @@ -0,0 +1,27 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { homepageApi } from "../services/homepageApi"; +import { faqApi } from "../services/faqApi"; +import { contactUsApi } from "../services/contactUsApi"; +import { blogApi } from "../services/blogApi"; +import { aboutUsApi } from "../services/aboutUsApi"; + +export const store = configureStore({ + reducer: { + [homepageApi.reducerPath]: homepageApi.reducer, + [faqApi.reducerPath]: faqApi.reducer, + [contactUsApi.reducerPath]: contactUsApi.reducer, + [blogApi.reducerPath]: blogApi.reducer, + [aboutUsApi.reducerPath]: aboutUsApi.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat( + homepageApi.middleware, + faqApi.middleware, + contactUsApi.middleware, + blogApi.middleware, + aboutUsApi.middleware, + ), +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file diff --git a/src/services/storeSwitchToDashboard.tsx b/src/services/storeSwitchToDashboard.tsx deleted file mode 100644 index 404b64d..0000000 --- a/src/services/storeSwitchToDashboard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// storeSwitchToDashboard.service.ts -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; -import baseQueryWithReauth from "./baseQuery"; - -export type ContactUsFormData = { - t_id: string; - name: string; - email: string; - country: string; - phone_number: string; - service: string; - budget: string; - message: string; - development_stage: string; - startTime: string; - nda_signing: string | boolean; - from_page: string; - ip?: string; - user_agent?: string; - contact_us_attachment?: File; -}; - -export const storeSwitchToDashboard = createApi({ - reducerPath: "storeSwitchToDashboard", - baseQuery: baseQueryWithReauth, - endpoints: (builder) => ({ - storeSwitchToDashboard: builder.mutation({ - query: (formData) => ({ - url: "/api/store-contact-us-data", - method: "POST", - body: formData, - }), - }), - }), -}); - -export const { useStoreSwitchToDashboardMutation } = storeSwitchToDashboard; \ No newline at end of file diff --git a/src/store/Store.tsx b/src/store/Store.tsx deleted file mode 100644 index f06519a..0000000 --- a/src/store/Store.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// store.ts -import { configureStore } from '@reduxjs/toolkit'; -import { setupListeners } from '@reduxjs/toolkit/query'; -import { storeSwitchToDashboard } from '../services/storeSwitchToDashboard'; - -export const store = configureStore({ - reducer: { - [storeSwitchToDashboard.reducerPath]: storeSwitchToDashboard.reducer, - }, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(storeSwitchToDashboard.middleware), -}); - -setupListeners(store.dispatch); - -export default store; \ No newline at end of file diff --git a/src/styles/globals.css b/src/styles/globals.css index 63db54f..57e4aee 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1092,6 +1092,10 @@ html { } } +button{ + cursor: pointer; +} + /* Responsive modal styles - No Scroll Version */ @media (max-width: 1024px) { .virtual-space-modal-container { diff --git a/src/utils/getReadingTime.ts b/src/utils/getReadingTime.ts new file mode 100644 index 0000000..aff56c5 --- /dev/null +++ b/src/utils/getReadingTime.ts @@ -0,0 +1,14 @@ +export const getReadingTime = (text: string): string => { + if (!text) return "0 min read"; + + // Remove HTML tags if present + const cleanText = text.replace(/<[^>]+>/g, ""); + + // Count words + const words = cleanText.trim().split(/\s+/).length; + + const wordsPerMinute = 200; + const minutes = Math.ceil(words / wordsPerMinute); + + return `${minutes} min read`; +}; \ No newline at end of file diff --git a/src/utils/urlHelpers.ts b/src/utils/urlHelpers.ts new file mode 100644 index 0000000..c492d65 --- /dev/null +++ b/src/utils/urlHelpers.ts @@ -0,0 +1,52 @@ +// utils/urlHelpers.ts + +/** + * Creates a URL-friendly slug from a title or string + * Example: "Ad ut neque enim omn" -> "ad-ut-neque-enim-omn" + */ +export const createSlug = (text: string): string => { + return text + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') // Remove special characters + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-'); // Replace multiple hyphens with single hyphen +}; + +/** + * Creates an SEO-friendly URL slug with full UUID + * Example: "Ad ut neque enim omn", "e7d611b6-853b-4785-b508-599eeed2af92" -> "ad-ut-neque-enim-omn-e7d611b6-853b-4785-b508-599eeed2af92" + */ +export const getSlugWithId = (title: string, id: string) => { + return `${createSlug(title)}-${id}`; +}; + +/** + * Extracts the slug from a slug+id string (removes the last part which is the UUID) + * Example: "ad-ut-neque-enim-omn-e7d611b6-853b-4785-b508-599eeed2af92" -> "ad-ut-neque-enim-omn" + */ +export const extractSlugFromSlugAndId = (slugAndId: string): string => { + const parts = slugAndId.split('-'); + // Remove the last 5 parts (UUID is 5 segments when split by hyphen) + for (let i = 0; i < 5; i++) { + parts.pop(); + } + return parts.join('-'); +}; + +/** + * Extracts the full UUID from a slug+id string + * Example: "ad-ut-neque-enim-omn-e7d611b6-853b-4785-b508-599eeed2af92" -> "e7d611b6-853b-4785-b508-599eeed2af92" + */ +export const extractIdFromSlug = (slugAndId: string): string | null => { + const parts = slugAndId.split('-'); + + if (parts.length < 5) return null; + + const id = parts.slice(-5).join('-'); + + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + return uuidRegex.test(id) ? id : null; +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3d0a51a..7bad187 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,13 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} + "include": [ + "src", + "src/redux/hooks", + "src/redux/store", + "src/redux/services" + ], + "references": [ + { "path": "./tsconfig.node.json" } + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 8d765c4..300b466 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,68 +1,92 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import * as path from "path"; - import { defineConfig } from 'vite'; - import react from '@vitejs/plugin-react-swc'; -import * as path from 'path'; - - export default defineConfig({ - plugins: [react()], - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - alias: { - 'vaul@1.1.2': 'vaul', - 'sonner@2.0.3': 'sonner', - 'recharts@2.15.2': 'recharts', - 'react-resizable-panels@2.1.7': 'react-resizable-panels', - 'react-hook-form@7.55.0': 'react-hook-form', - 'react-day-picker@8.10.1': 'react-day-picker', - 'next-themes@0.4.6': 'next-themes', - 'lucide-react@0.487.0': 'lucide-react', - 'input-otp@1.4.2': 'input-otp', - 'figma:asset/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png': path.resolve(__dirname, './src/assets/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png'), - 'figma:asset/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png': path.resolve(__dirname, './src/assets/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png'), - 'figma:asset/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png': path.resolve(__dirname, './src/assets/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png'), - 'figma:asset/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png': path.resolve(__dirname, './src/assets/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png'), - 'figma:asset/a28d79dd35b730f689b77dbb30452ca27bd25759.png': path.resolve(__dirname, './src/assets/a28d79dd35b730f689b77dbb30452ca27bd25759.png'), - 'figma:asset/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png': path.resolve(__dirname, './src/assets/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png'), - 'figma:asset/4833274f0a593cd31fdefe553b70bb016de281af.png': path.resolve(__dirname, './src/assets/4833274f0a593cd31fdefe553b70bb016de281af.png'), - 'figma:asset/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png': path.resolve(__dirname, './src/assets/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png'), - 'embla-carousel-react@8.6.0': 'embla-carousel-react', - 'cmdk@1.1.1': 'cmdk', - 'class-variance-authority@0.7.1': 'class-variance-authority', - '@radix-ui/react-tooltip@1.1.8': '@radix-ui/react-tooltip', - '@radix-ui/react-toggle@1.1.2': '@radix-ui/react-toggle', - '@radix-ui/react-toggle-group@1.1.2': '@radix-ui/react-toggle-group', - '@radix-ui/react-tabs@1.1.3': '@radix-ui/react-tabs', - '@radix-ui/react-switch@1.1.3': '@radix-ui/react-switch', - '@radix-ui/react-slot@1.1.2': '@radix-ui/react-slot', - '@radix-ui/react-slider@1.2.3': '@radix-ui/react-slider', - '@radix-ui/react-separator@1.1.2': '@radix-ui/react-separator', - '@radix-ui/react-select@2.1.6': '@radix-ui/react-select', - '@radix-ui/react-scroll-area@1.2.3': '@radix-ui/react-scroll-area', - '@radix-ui/react-radio-group@1.2.3': '@radix-ui/react-radio-group', - '@radix-ui/react-progress@1.1.2': '@radix-ui/react-progress', - '@radix-ui/react-popover@1.1.6': '@radix-ui/react-popover', - '@radix-ui/react-navigation-menu@1.2.5': '@radix-ui/react-navigation-menu', - '@radix-ui/react-menubar@1.1.6': '@radix-ui/react-menubar', - '@radix-ui/react-label@2.1.2': '@radix-ui/react-label', - '@radix-ui/react-hover-card@1.1.6': '@radix-ui/react-hover-card', - '@radix-ui/react-dropdown-menu@2.1.6': '@radix-ui/react-dropdown-menu', - '@radix-ui/react-dialog@1.1.6': '@radix-ui/react-dialog', - '@radix-ui/react-context-menu@2.2.6': '@radix-ui/react-context-menu', - '@radix-ui/react-collapsible@1.1.3': '@radix-ui/react-collapsible', - '@radix-ui/react-checkbox@1.1.4': '@radix-ui/react-checkbox', - '@radix-ui/react-avatar@1.1.3': '@radix-ui/react-avatar', - '@radix-ui/react-aspect-ratio@1.1.2': '@radix-ui/react-aspect-ratio', - '@radix-ui/react-alert-dialog@1.1.6': '@radix-ui/react-alert-dialog', - '@radix-ui/react-accordion@1.2.3': '@radix-ui/react-accordion', - '@': path.resolve(__dirname, './src'), - }, +export default defineConfig({ + plugins: [react()], + resolve: { + extensions: [".js", ".jsx", ".ts", ".tsx", ".json"], + alias: { + "vaul@1.1.2": "vaul", + "sonner@2.0.3": "sonner", + "recharts@2.15.2": "recharts", + "react-resizable-panels@2.1.7": "react-resizable-panels", + "react-hook-form@7.55.0": "react-hook-form", + "react-day-picker@8.10.1": "react-day-picker", + "next-themes@0.4.6": "next-themes", + "lucide-react@0.487.0": "lucide-react", + "input-otp@1.4.2": "input-otp", + "figma:asset/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png": path.resolve( + __dirname, + "./src/assets/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png", + ), + "figma:asset/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png": path.resolve( + __dirname, + "./src/assets/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png", + ), + "figma:asset/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png": path.resolve( + __dirname, + "./src/assets/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png", + ), + "figma:asset/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png": path.resolve( + __dirname, + "./src/assets/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png", + ), + "figma:asset/a28d79dd35b730f689b77dbb30452ca27bd25759.png": path.resolve( + __dirname, + "./src/assets/a28d79dd35b730f689b77dbb30452ca27bd25759.png", + ), + "figma:asset/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png": path.resolve( + __dirname, + "./src/assets/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png", + ), + "figma:asset/4833274f0a593cd31fdefe553b70bb016de281af.png": path.resolve( + __dirname, + "./src/assets/4833274f0a593cd31fdefe553b70bb016de281af.png", + ), + "figma:asset/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png": path.resolve( + __dirname, + "./src/assets/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png", + ), + "embla-carousel-react@8.6.0": "embla-carousel-react", + "cmdk@1.1.1": "cmdk", + "class-variance-authority@0.7.1": "class-variance-authority", + "@radix-ui/react-tooltip@1.1.8": "@radix-ui/react-tooltip", + "@radix-ui/react-toggle@1.1.2": "@radix-ui/react-toggle", + "@radix-ui/react-toggle-group@1.1.2": "@radix-ui/react-toggle-group", + "@radix-ui/react-tabs@1.1.3": "@radix-ui/react-tabs", + "@radix-ui/react-switch@1.1.3": "@radix-ui/react-switch", + "@radix-ui/react-slot@1.1.2": "@radix-ui/react-slot", + "@radix-ui/react-slider@1.2.3": "@radix-ui/react-slider", + "@radix-ui/react-separator@1.1.2": "@radix-ui/react-separator", + "@radix-ui/react-select@2.1.6": "@radix-ui/react-select", + "@radix-ui/react-scroll-area@1.2.3": "@radix-ui/react-scroll-area", + "@radix-ui/react-radio-group@1.2.3": "@radix-ui/react-radio-group", + "@radix-ui/react-progress@1.1.2": "@radix-ui/react-progress", + "@radix-ui/react-popover@1.1.6": "@radix-ui/react-popover", + "@radix-ui/react-navigation-menu@1.2.5": + "@radix-ui/react-navigation-menu", + "@radix-ui/react-menubar@1.1.6": "@radix-ui/react-menubar", + "@radix-ui/react-label@2.1.2": "@radix-ui/react-label", + "@radix-ui/react-hover-card@1.1.6": "@radix-ui/react-hover-card", + "@radix-ui/react-dropdown-menu@2.1.6": "@radix-ui/react-dropdown-menu", + "@radix-ui/react-dialog@1.1.6": "@radix-ui/react-dialog", + "@radix-ui/react-context-menu@2.2.6": "@radix-ui/react-context-menu", + "@radix-ui/react-collapsible@1.1.3": "@radix-ui/react-collapsible", + "@radix-ui/react-checkbox@1.1.4": "@radix-ui/react-checkbox", + "@radix-ui/react-avatar@1.1.3": "@radix-ui/react-avatar", + "@radix-ui/react-aspect-ratio@1.1.2": "@radix-ui/react-aspect-ratio", + "@radix-ui/react-alert-dialog@1.1.6": "@radix-ui/react-alert-dialog", + "@radix-ui/react-accordion@1.2.3": "@radix-ui/react-accordion", + "@": path.resolve(__dirname, "./src"), }, - build: { - target: 'esnext', - outDir: 'build', - }, - server: { - port: 4000, - open: true, - }, - }); \ No newline at end of file + }, + build: { + target: "esnext", + outDir: "build", + }, + server: { + port: 4000, + open: true, + }, +});