All implemaentation and 360deg tour
This commit is contained in:
73
package-lock.json
generated
73
package-lock.json
generated
@@ -58,6 +58,7 @@
|
|||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "*",
|
"tailwind-merge": "*",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
|
"three": "^0.183.2",
|
||||||
"vaul": "^1.1.2"
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.8",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/react-slick": "^0.23.13",
|
"@types/react-slick": "^0.23.13",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
@@ -387,6 +389,13 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
|
||||||
@@ -2023,6 +2032,13 @@
|
|||||||
"tailwindcss": "4.1.12"
|
"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": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2178,12 +2194,42 @@
|
|||||||
"@types/react": "*"
|
"@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": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"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==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.2.tgz",
|
||||||
@@ -2226,6 +2272,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/aria-hidden": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
"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": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.23.12",
|
"version": "12.23.12",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
||||||
@@ -2925,6 +2985,13 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@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": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -3579,6 +3646,12 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "*",
|
"tailwind-merge": "*",
|
||||||
"tailwindcss": "^4.1.12",
|
"tailwindcss": "^4.1.12",
|
||||||
|
"three": "^0.183.2",
|
||||||
"vaul": "^1.1.2"
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.8",
|
"@types/react-dom": "^19.1.8",
|
||||||
"@types/react-slick": "^0.23.13",
|
"@types/react-slick": "^0.23.13",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import HomePage from './pages/HomePage';
|
|||||||
import { AboutUs } from './components/AboutUs';
|
import { AboutUs } from './components/AboutUs';
|
||||||
import { Services } from './components/Services';
|
import { Services } from './components/Services';
|
||||||
import { LearningFacilityNew } from './components/LearningFacilityNew';
|
import { LearningFacilityNew } from './components/LearningFacilityNew';
|
||||||
import HomePageNew from './pages/HomePageNew';
|
|
||||||
import { FooterNew } from './components/FooterNew';
|
import { FooterNew } from './components/FooterNew';
|
||||||
import { Privacy } from "./pages/Privacy";
|
import { Privacy } from "./pages/Privacy";
|
||||||
import { TermsCondition } from "./pages/TermsCondition";
|
import { TermsCondition } from "./pages/TermsCondition";
|
||||||
@@ -108,8 +107,8 @@ export default function App() {
|
|||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
|
|
||||||
{/* Dynamic Routes */}
|
{/* Dynamic Routes */}
|
||||||
<Route path="/learning/articles/:slug" element={<BlogDetail />} />
|
<Route path="/learning/articles/:slugAndId" element={<BlogDetail />} />
|
||||||
<Route path="/learning/blogs/:slug" element={<BlogDetail />} />
|
{/* <Route path="/learning/blogs/:slug" element={<BlogDetail />} /> */}
|
||||||
{/* <Route path="/learning/webcast/:slug" element={<WebinarDetail />} />
|
{/* <Route path="/learning/webcast/:slug" element={<WebinarDetail />} />
|
||||||
<Route path="/webinar/:slug" element={<WebinarDetail />} /> */}
|
<Route path="/webinar/:slug" element={<WebinarDetail />} /> */}
|
||||||
<Route path="/course/:slug" element={<ProgrammeDetail />} />
|
<Route path="/course/:slug" element={<ProgrammeDetail />} />
|
||||||
|
|||||||
BIN
src/assets/panoramas/cayley_interior.jpg
Normal file
BIN
src/assets/panoramas/cayley_interior.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 778 KiB |
BIN
src/assets/panoramas/cedar_bridge_sunset.jpg
Normal file
BIN
src/assets/panoramas/cedar_bridge_sunset.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
@@ -35,6 +35,7 @@ import Balaji from '../assets/Balaji-Chandrakumar.jpeg';
|
|||||||
import Ramesh from '../assets/Ramesh-Padmanabhan.jpeg';
|
import Ramesh from '../assets/Ramesh-Padmanabhan.jpeg';
|
||||||
import Diju from '../assets/Diju.jpeg';
|
import Diju from '../assets/Diju.jpeg';
|
||||||
import svgPaths from '../imports/svg-kw7r0ellyk';
|
import svgPaths from '../imports/svg-kw7r0ellyk';
|
||||||
|
import { useGetAboutUsQuery } from '../redux/services/aboutUsApi';
|
||||||
|
|
||||||
// Leadership Orientations Data
|
// Leadership Orientations Data
|
||||||
const leadershipOrientations = [
|
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'
|
'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
|
// Team Members Data with Full Profiles (Static - can be kept or also fetched from API if needed)
|
||||||
const teamMembers = [
|
const staticTeamMembers = [
|
||||||
{
|
{
|
||||||
name: 'Mr. K Ramkumar',
|
name: 'Mr. K Ramkumar',
|
||||||
role: 'Managing Director',
|
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 = () => (
|
||||||
|
<div className="animate-pulse">
|
||||||
|
{/* Hero Section Skeleton */}
|
||||||
|
<section className="relative min-h-[85vh] flex flex-col bg-gray-200">
|
||||||
|
<div className="absolute inset-0 bg-gray-300"></div>
|
||||||
|
<div className="relative z-10 flex-1 flex items-center">
|
||||||
|
<div className="w-full section-margin-x">
|
||||||
|
<div className="max-w-6xl">
|
||||||
|
<div className="h-16 bg-gray-400 rounded-lg w-3/4 mb-8"></div>
|
||||||
|
<div className="h-24 bg-gray-400 rounded-lg w-2/3 mb-8"></div>
|
||||||
|
<div className="h-12 bg-gray-400 rounded-lg w-48"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Add more skeleton sections as needed */}
|
||||||
|
<div className="py-24 text-center text-gray-500">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export function AboutUs() {
|
export function AboutUs() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [expandedValue, setExpandedValue] = useState<string | null>('context');
|
const [expandedValue, setExpandedValue] = useState<string | null>('context');
|
||||||
const [selectedMember, setSelectedMember] = useState<typeof teamMembers[0] | null>(null);
|
const [selectedMember, setSelectedMember] = useState<typeof staticTeamMembers[0] | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
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);
|
setSelectedMember(member);
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
@@ -296,7 +321,6 @@ export function AboutUs() {
|
|||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
};
|
};
|
||||||
@@ -305,9 +329,8 @@ export function AboutUs() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
|
|
||||||
// Timeline fill animation on scroll
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const timelineSection = document.querySelector('#timeline-fill-line');
|
const timelineSection = document.querySelector('#timeline-fill-line') as HTMLElement | null;
|
||||||
const timelineContainer = timelineSection?.parentElement;
|
const timelineContainer = timelineSection?.parentElement;
|
||||||
|
|
||||||
if (!timelineSection || !timelineContainer) return;
|
if (!timelineSection || !timelineContainer) return;
|
||||||
@@ -315,47 +338,56 @@ export function AboutUs() {
|
|||||||
const rect = timelineContainer.getBoundingClientRect();
|
const rect = timelineContainer.getBoundingClientRect();
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
// Calculate how much of the timeline is visible
|
|
||||||
const sectionTop = rect.top;
|
const sectionTop = rect.top;
|
||||||
const sectionHeight = rect.height;
|
const sectionHeight = rect.height;
|
||||||
const visibleTop = Math.max(0, windowHeight - sectionTop);
|
const visibleTop = Math.max(0, windowHeight - sectionTop);
|
||||||
const visibleHeight = Math.min(visibleTop, sectionHeight);
|
const visibleHeight = Math.min(visibleTop, sectionHeight);
|
||||||
const scrollProgress = Math.max(0, Math.min(1, visibleHeight / 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)';
|
||||||
const maxFillHeight = 'calc(100% - 1rem)'; // Match the background line limit
|
|
||||||
|
|
||||||
// Apply progressive fill that respects the maximum height constraint
|
|
||||||
if (scrollProgress >= 0.9) {
|
if (scrollProgress >= 0.9) {
|
||||||
// When nearly complete, set to exact end position
|
|
||||||
timelineSection.style.height = maxFillHeight;
|
timelineSection.style.height = maxFillHeight;
|
||||||
} else {
|
} else {
|
||||||
// Progressive fill up to 90% of the way
|
|
||||||
timelineSection.style.height = `${scrollProgress * 90}%`;
|
timelineSection.style.height = `${scrollProgress * 90}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add scroll listener
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
// Initial call to set the initial state
|
|
||||||
handleScroll();
|
handleScroll();
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Show loading skeleton while fetching data
|
||||||
|
if (isLoading) {
|
||||||
|
return <AboutUsSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state if API call fails
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-bold text-red-600 mb-4">Error Loading Page</h2>
|
||||||
|
<p className="text-gray-600 mb-4">Failed to load About Us content. Please try again later.</p>
|
||||||
|
<Button onClick={() => window.location.reload()}>Retry</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#FFFFFF', fontFamily: 'var(--font-family-base)' }}>
|
<div style={{ backgroundColor: '#FFFFFF', fontFamily: 'var(--font-family-base)' }}>
|
||||||
{/* Hero Section - Our Vision Page Style */}
|
{/* Hero Section - Dynamic from API */}
|
||||||
<section className="relative min-h-[85vh] flex flex-col">
|
<section className="relative min-h-[85vh] flex flex-col">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<div
|
<div
|
||||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url('https://images.unsplash.com/photo-1552664730-d307ca884978?w=1920&h=1080&fit=crop')`
|
backgroundImage: `url('${aboutUsData?.hero_section?.background_image_url || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1920&h=1080&fit=crop'}')`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-black/85 via-black/75 to-black/65"></div>
|
<div className="absolute inset-0 bg-gradient-to-r from-black/85 via-black/75 to-black/65"></div>
|
||||||
@@ -364,32 +396,22 @@ export function AboutUs() {
|
|||||||
<div className="relative z-10 flex-1 flex items-center">
|
<div className="relative z-10 flex-1 flex items-center">
|
||||||
<div className="w-full section-margin-x">
|
<div className="w-full section-margin-x">
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
{/* Back Navigation */}
|
|
||||||
{/* <div className="mb-8">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => navigateTo('/services')}
|
|
||||||
className="text-white hover:text-white hover:bg-white/10 p-2 -ml-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
||||||
Back to Services
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-h1-white">
|
<h1 className="text-h1-white">
|
||||||
Advancing Leadership Through Insight
|
{aboutUsData?.hero_section?.headline || "Advancing Leadership Through Insight"}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-body-lg-white mb-8 max-w-3xl">
|
<p className="text-body-lg-white mb-8 max-w-3xl">
|
||||||
<strong>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.</strong>
|
<strong>
|
||||||
|
{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."}
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="Talk to Us"
|
text={aboutUsData?.hero_section?.cta_text || "Talk to Us"}
|
||||||
onClick={() => navigateTo('/contact?topic=management-development')}
|
onClick={() => navigateTo(aboutUsData?.hero_section?.cta_destination || '/contact?topic=management-development')}
|
||||||
ariaLabel="Talk to us about management development"
|
ariaLabel="Talk to us about management development"
|
||||||
className="primary-cta-button-blue cta-text-white"
|
className="primary-cta-button-blue cta-text-white"
|
||||||
/>
|
/>
|
||||||
@@ -399,7 +421,7 @@ export function AboutUs() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 1: Our Promise */}
|
{/* Section 1: Our Promise - Dynamic from API */}
|
||||||
<section className="py-24 lg:py-32" style={{ backgroundColor: '#FFFFFF' }}>
|
<section className="py-24 lg:py-32" style={{ backgroundColor: '#FFFFFF' }}>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -410,7 +432,7 @@ export function AboutUs() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center"
|
className="text-center"
|
||||||
>
|
>
|
||||||
<BrandedTag text="Our Promise" />
|
<BrandedTag text={aboutUsData?.our_promise_title || "Our Promise"} />
|
||||||
<h2 className="text-h1 mb-8" style={{
|
<h2 className="text-h1 mb-8" style={{
|
||||||
fontSize: 'clamp(2.5rem, 5vw, 4rem)',
|
fontSize: 'clamp(2.5rem, 5vw, 4rem)',
|
||||||
lineHeight: '1.1',
|
lineHeight: '1.1',
|
||||||
@@ -423,7 +445,7 @@ export function AboutUs() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 2: How We Work */}
|
{/* Section 2: How We Work - Dynamic from API */}
|
||||||
<section className="py-24 lg:py-32" style={{ backgroundColor: '#F9F9F9' }}>
|
<section className="py-24 lg:py-32" style={{ backgroundColor: '#F9F9F9' }}>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -434,109 +456,143 @@ export function AboutUs() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<BrandedTag text="How We Work" />
|
<BrandedTag text={aboutUsData?.how_we_work_title || "How We Work"} />
|
||||||
<h2 className="text-h2 mb-8">How We Work</h2>
|
<h2 className="text-h2 mb-8">{aboutUsData?.how_we_work_title || "How We Work"}</h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Four Key Points Grid */}
|
{/* Four Key Points Grid - Using API data if available, otherwise fallback to static */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12">
|
||||||
<motion.div
|
{(aboutUsData?.how_we_work && aboutUsData.how_we_work.length > 0) ? (
|
||||||
initial={{ opacity: 0, y: 30 }}
|
aboutUsData.how_we_work.map((item, index) => (
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}
|
key={item.id}
|
||||||
viewport={{ once: true }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
>
|
transition={{ duration: 0.6, delay: 0.1 * (index + 1) }}
|
||||||
<div className="flex items-start gap-4">
|
viewport={{ once: true }}
|
||||||
<div
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
|
||||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
|
||||||
>
|
>
|
||||||
<Puzzle className="w-6 h-6 text-white" />
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<div
|
||||||
<div>
|
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
<h3 className="text-h4 mb-4">Co-created interventions</h3>
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
<p className="text-body text-muted leading-relaxed">
|
>
|
||||||
We collaborate with you to design solutions that fit your unique organizational context and strategic objectives.
|
{index === 0 && <Puzzle className="w-6 h-6 text-white" />}
|
||||||
</p>
|
{index === 1 && <Target className="w-6 h-6 text-white" />}
|
||||||
</div>
|
{index === 2 && <BookOpen className="w-6 h-6 text-white" />}
|
||||||
</div>
|
{index === 3 && <Zap className="w-6 h-6 text-white" />}
|
||||||
</motion.div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-h4 mb-4">{item.title}</h3>
|
||||||
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// Fallback to static data if API data is not available
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
|
>
|
||||||
|
<Puzzle className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-h4 mb-4">Co-created interventions</h3>
|
||||||
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
We collaborate with you to design solutions that fit your unique organizational context and strategic objectives.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
|
||||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
|
||||||
>
|
>
|
||||||
<Target className="w-6 h-6 text-white" />
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<div
|
||||||
<div>
|
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
<h3 className="text-h4 mb-4">Grounded in business context</h3>
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
<p className="text-body text-muted leading-relaxed">
|
>
|
||||||
Every solution is tailored to your specific business environment, challenges, and growth objectives.
|
<Target className="w-6 h-6 text-white" />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<h3 className="text-h4 mb-4">Grounded in business context</h3>
|
||||||
</motion.div>
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
Every solution is tailored to your specific business environment, challenges, and growth objectives.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.3 }}
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
|
||||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
|
||||||
>
|
>
|
||||||
<BookOpen className="w-6 h-6 text-white" />
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<div
|
||||||
<div>
|
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
<h3 className="text-h4 mb-4">Research-backed, behaviour-anchored</h3>
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
<p className="text-body text-muted leading-relaxed">
|
>
|
||||||
Our methodologies are rooted in rigorous research and focused on sustainable behavioral transformation.
|
<BookOpen className="w-6 h-6 text-white" />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<h3 className="text-h4 mb-4">Research-backed, behaviour-anchored</h3>
|
||||||
</motion.div>
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
Our methodologies are rooted in rigorous research and focused on sustainable behavioral transformation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.4 }}
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
className="bg-white p-8 rounded-2xl shadow-lg border border-gray-100"
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div
|
|
||||||
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
|
||||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
|
||||||
>
|
>
|
||||||
<Zap className="w-6 h-6 text-white" />
|
<div className="flex items-start gap-4">
|
||||||
</div>
|
<div
|
||||||
<div>
|
className="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
<h3 className="text-h4 mb-4">Delivered through immersive formats</h3>
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
<p className="text-body text-muted leading-relaxed">
|
>
|
||||||
Interactive, experiential learning approaches that engage participants and drive lasting impact.
|
<Zap className="w-6 h-6 text-white" />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<h3 className="text-h4 mb-4">Delivered through immersive formats</h3>
|
||||||
</motion.div>
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
Interactive, experiential learning approaches that engage participants and drive lasting impact.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 3: Who We Are - Updated Statistics */}
|
{/* Section 3: Who We Are - Updated Statistics - Dynamic from API */}
|
||||||
<section className="py-24 lg:py-32" style={{ backgroundColor: '#FFFFFF' }}>
|
<section className="py-24 lg:py-32" style={{ backgroundColor: '#FFFFFF' }}>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -552,7 +608,7 @@ export function AboutUs() {
|
|||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="branded-tag-system">
|
<div className="branded-tag-system">
|
||||||
<div className="dot"></div>
|
<div className="dot"></div>
|
||||||
<span className="text">Who we are</span>
|
<span className="text">{aboutUsData?.who_we_are_title || "Who we are"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -568,96 +624,122 @@ export function AboutUs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Updated Statistics Grid */}
|
{/* Updated Statistics Grid - Dynamic from API */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 lg:gap-12 pt-12 border-t border-gray-200">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 lg:gap-12 pt-12 border-t border-gray-200">
|
||||||
<motion.div
|
{(aboutUsData?.stat_section && aboutUsData.stat_section.length > 0) ? (
|
||||||
initial={{ opacity: 0, y: 20 }}
|
aboutUsData.stat_section.map((stat, index) => (
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<motion.div
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}
|
key={stat.id}
|
||||||
viewport={{ once: true }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
className="text-center lg:text-left"
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
>
|
transition={{ duration: 0.6, delay: 0.1 * (index + 1) }}
|
||||||
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
viewport={{ once: true }}
|
||||||
fontFamily: 'var(--font-family-base)',
|
className="text-center lg:text-left"
|
||||||
lineHeight: '1',
|
>
|
||||||
color: 'var(--color-primary)'
|
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
||||||
}}>
|
fontFamily: 'var(--font-family-base)',
|
||||||
150+
|
lineHeight: '1',
|
||||||
</div>
|
color: 'var(--color-primary)'
|
||||||
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
}}>
|
||||||
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
{stat.number}{stat.suffix}
|
||||||
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>CORPORATES</span>
|
</div>
|
||||||
</div>
|
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
||||||
</motion.div>
|
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
||||||
|
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>{stat.label.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// Fallback to static statistics if API data is not available
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-center lg:text-left"
|
||||||
|
>
|
||||||
|
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
lineHeight: '1',
|
||||||
|
color: 'var(--color-primary)'
|
||||||
|
}}>
|
||||||
|
150+
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
||||||
|
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
||||||
|
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>CORPORATES</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center lg:text-left"
|
className="text-center lg:text-left"
|
||||||
>
|
>
|
||||||
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
lineHeight: '1',
|
lineHeight: '1',
|
||||||
color: 'var(--color-primary)'
|
color: 'var(--color-primary)'
|
||||||
}}>
|
}}>
|
||||||
27,000+
|
27,000+
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
||||||
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
||||||
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>LEADERS</span>
|
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>LEADERS</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.3 }}
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center lg:text-left"
|
className="text-center lg:text-left"
|
||||||
>
|
>
|
||||||
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
<div className="text-5xl lg:text-6xl font-medium mb-2" style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
lineHeight: '1',
|
lineHeight: '1',
|
||||||
color: 'var(--color-primary)'
|
color: 'var(--color-primary)'
|
||||||
}}>
|
}}>
|
||||||
5,000+
|
5,000+
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
||||||
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
||||||
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>ROOM NIGHTS</span>
|
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>ROOM NIGHTS</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.4 }}
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center lg:text-left"
|
className="text-center lg:text-left"
|
||||||
>
|
>
|
||||||
<div className="text-3xl lg:text-4xl font-medium mb-2" style={{
|
<div className="text-3xl lg:text-4xl font-medium mb-2" style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
lineHeight: '1',
|
lineHeight: '1',
|
||||||
color: 'var(--color-primary)'
|
color: 'var(--color-primary)'
|
||||||
}}>
|
}}>
|
||||||
India & APAC
|
India & APAC
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
<div className="flex items-center justify-center lg:justify-start gap-2 text-body text-muted" style={{ fontFamily: 'var(--font-family-base)' }}>
|
||||||
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
<div className="w-2 h-2 rounded-sm" style={{ backgroundColor: 'var(--color-accent)' }}></div>
|
||||||
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>PRESENCE</span>
|
<span style={{ color: 'var(--color-black)', fontWeight: '500' }}>PRESENCE</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Section 4: Our Team - Dynamic from API */}
|
||||||
|
|
||||||
{/* Section 4: Our Team */}
|
|
||||||
<section className="py-24 lg:py-32" style={{ backgroundColor: '#F9F9F9' }}>
|
<section className="py-24 lg:py-32" style={{ backgroundColor: '#F9F9F9' }}>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -669,8 +751,8 @@ export function AboutUs() {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-16"
|
className="text-center mb-16"
|
||||||
>
|
>
|
||||||
<BrandedTag text="Our Team" />
|
<BrandedTag text={aboutUsData?.our_team_title || "Our Team"} />
|
||||||
<h2 className="text-h2 mb-8">Our Team</h2>
|
<h2 className="text-h2 mb-8">{aboutUsData?.our_team_title || "Our Team"}</h2>
|
||||||
<div className="max-w-4xl mx-auto text-center space-y-6">
|
<div className="max-w-4xl mx-auto text-center space-y-6">
|
||||||
<p className="text-body-lg text-muted leading-relaxed">
|
<p className="text-body-lg text-muted leading-relaxed">
|
||||||
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
|
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() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Team Members Grid */}
|
{/* Team Members Grid - Using static team members with full profiles */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
|
||||||
{teamMembers.map((member, index) => (
|
{staticTeamMembers.map((member, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={member.name}
|
key={member.name}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
@@ -725,11 +807,60 @@ export function AboutUs() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Alternative: Use API team data if needed */}
|
||||||
|
{/* {aboutUsData?.our_team && aboutUsData.our_team.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
|
||||||
|
{aboutUsData.our_team.map((member, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={member.id}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-left cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="relative mb-6 group">
|
||||||
|
<div className="aspect-square rounded-2xl overflow-hidden bg-gray-100 shadow-lg group-hover:shadow-xl transition-all duration-300">
|
||||||
|
<img
|
||||||
|
src={member.photo_url}
|
||||||
|
alt={member.alt_text}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-all duration-300 rounded-2xl flex items-center justify-center">
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
|
<div
|
||||||
|
className="px-4 py-2 rounded-lg text-white text-small"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#04045B',
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Profile
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-h4 text-black group-hover:text-primary transition-colors duration-300">
|
||||||
|
{member.name_role.split(' - ')[0]}
|
||||||
|
</h3>
|
||||||
|
<p className="text-body text-muted leading-relaxed">
|
||||||
|
{member.name_role.split(' - ')[1] || ''}
|
||||||
|
</p>
|
||||||
|
<p className="text-small text-muted">{member.bio}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Section 5: Our Methodology */}
|
{/* Section 5: Our Methodology (Static - unchanged) */}
|
||||||
<section className="py-16 lg:py-20" style={{ backgroundColor: '#FFFFFF' }}>
|
<section className="py-16 lg:py-20" style={{ backgroundColor: '#FFFFFF' }}>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
@@ -750,19 +881,19 @@ export function AboutUs() {
|
|||||||
<div
|
<div
|
||||||
className="absolute left-4 top-0 w-0.5 bg-gray-300"
|
className="absolute left-4 top-0 w-0.5 bg-gray-300"
|
||||||
style={{
|
style={{
|
||||||
height: 'calc(100% - 1rem)', // Adjusted to end exactly at Phase 3 dot
|
height: 'calc(100% - 1rem)',
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{/* Vertical Line Fill - Blue - Animated on Scroll - Ends exactly at Phase 3 dot */}
|
{/* Vertical Line Fill - Blue - Animated on Scroll */}
|
||||||
<div
|
<div
|
||||||
id="timeline-fill-line"
|
id="timeline-fill-line"
|
||||||
className="absolute left-4 top-0 w-0.5 transition-all duration-1000 ease-out"
|
className="absolute left-4 top-0 w-0.5 transition-all duration-1000 ease-out"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-primary)',
|
backgroundColor: 'var(--color-primary)',
|
||||||
height: '0%',
|
height: '0%',
|
||||||
maxHeight: 'calc(100% - 1rem)', // Limit to Phase 3 dot position
|
maxHeight: 'calc(100% - 1rem)',
|
||||||
zIndex: 2
|
zIndex: 2
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
@@ -1168,8 +1299,6 @@ export function AboutUs() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
{/* Testimonials Section */}
|
||||||
<TestimonialsSection
|
<TestimonialsSection
|
||||||
title="What Our Clients Say About Us"
|
title="What Our Clients Say About Us"
|
||||||
|
|||||||
@@ -1,113 +1,127 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
||||||
import { Badge } from './ui/badge';
|
|
||||||
import { Input } from './ui/input';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
||||||
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
|
||||||
import { navigateTo } from './Router';
|
|
||||||
import {
|
import {
|
||||||
Search,
|
|
||||||
Calendar,
|
|
||||||
User,
|
|
||||||
ArrowRight,
|
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
Filter,
|
Filter,
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
SortAsc,
|
Search,
|
||||||
Clock,
|
|
||||||
Eye,
|
|
||||||
Star,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
X
|
X
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { articlesData } from '../data/articlesData';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
import { BlogItem, useGetBlogsQuery } from '../redux/services/blogApi';
|
||||||
|
import { useGetFaqCategoriesQuery } from '../redux/services/faqApi';
|
||||||
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
|
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
||||||
|
import { navigateTo } from './Router';
|
||||||
|
import { Badge } from './ui/badge';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Card, CardContent } from './ui/card';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
|
import { FullScreenLoader } from './FullScreenLoader';
|
||||||
|
import { getSlugWithId } from '../utils/urlHelpers';
|
||||||
|
|
||||||
|
// Define category type with ID and name
|
||||||
|
interface CategoryOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateRange =
|
||||||
|
| 'all_time'
|
||||||
|
| 'last_7_days'
|
||||||
|
| 'last_30_days'
|
||||||
|
| 'last_3_months'
|
||||||
|
| 'last_6_months';
|
||||||
|
|
||||||
|
type SortBy =
|
||||||
|
| 'most_recent'
|
||||||
|
| 'oldest_first'
|
||||||
|
| 'title_az';
|
||||||
|
|
||||||
export function Articles() {
|
export function Articles() {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
const [selectedCategory, setSelectedCategory] = useState<CategoryOption>({ id: 'all', name: 'All Categories' });
|
||||||
const [selectedAuthor, setSelectedAuthor] = useState('All Authors');
|
|
||||||
const [selectedReadTime, setSelectedReadTime] = useState('All Read Times');
|
const [selectedReadTime, setSelectedReadTime] = useState('All Read Times');
|
||||||
const [selectedDateRange, setSelectedDateRange] = useState('All Time');
|
const [selectedDateRange, setSelectedDateRange] = useState<DateRange>('all_time');
|
||||||
const [selectedTopic, setSelectedTopic] = useState('All Topics');
|
const [selectedTopic, setSelectedTopic] = useState<{
|
||||||
const [sortBy, setSortBy] = useState('Most Recent');
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>({
|
||||||
|
id: 'all',
|
||||||
|
name: 'All Topics'
|
||||||
|
});
|
||||||
|
const [sortBy, setSortBy] = useState<SortBy>('most_recent');
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const articlesPerPage = 4;
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
|
const articlesPerPage = 6;
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [allTags, setAllTags] = useState<{ id: string; name: string }[]>([]);
|
||||||
|
|
||||||
// Use articlesData instead of the old articles variable
|
// Fetch categories for filter dropdown
|
||||||
const articles = articlesData;
|
const {
|
||||||
|
data: categoriesData,
|
||||||
// Get unique values for filters - FIXED: using articlesData
|
isLoading: isLoadingCategories
|
||||||
const categories = ['All Categories', ...Array.from(new Set(articlesData.map(article => article.category)))];
|
} = useGetFaqCategoriesQuery({
|
||||||
const authors = ['All Authors', ...Array.from(new Set(articlesData.map(article => article.author)))];
|
limit: 100,
|
||||||
const readTimes = ['All Read Times', 'Under 5 min', '5-10 min', 'Over 10 min'];
|
offset: 0
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Paginate results
|
// Filter categories to only those for blog and create options with id and name
|
||||||
const totalPages = Math.ceil(filteredArticles.length / articlesPerPage);
|
const categories: CategoryOption[] = [
|
||||||
const startIndex = (currentPage - 1) * articlesPerPage;
|
{ id: 'all', name: 'All Categories' },
|
||||||
const currentArticles = filteredArticles.slice(startIndex, startIndex + articlesPerPage);
|
...(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) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -116,25 +130,94 @@ export function Articles() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
// Calculate read time based on content length (approx)
|
||||||
setSearchTerm('');
|
const calculateReadTime = (content: string): string => {
|
||||||
setSelectedCategory('All Categories');
|
const wordsPerMinute = 200;
|
||||||
setSelectedAuthor('All Authors');
|
const wordCount = content.split(/\s+/).length;
|
||||||
setSelectedReadTime('All Read Times');
|
const minutes = Math.ceil(wordCount / wordsPerMinute);
|
||||||
setSelectedDateRange('All Time');
|
return `${minutes} min read`;
|
||||||
setSelectedTopic('All Topics');
|
|
||||||
setSortBy('Most Recent');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = searchTerm ||
|
// Filter articles by read time (client-side only - API doesn't support this)
|
||||||
selectedCategory !== 'All Categories' ||
|
const getReadTimeFilteredArticles = () => {
|
||||||
selectedAuthor !== 'All Authors' ||
|
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' ||
|
selectedReadTime !== 'All Read Times' ||
|
||||||
selectedDateRange !== 'All Time' ||
|
selectedDateRange !== 'all_time' ||
|
||||||
selectedTopic !== 'All Topics';
|
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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||||
|
<FullScreenLoader text="Loading articles..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error state
|
||||||
|
if (isBlogsError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<X className="w-8 h-8 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-h3 mb-2">Failed to load articles</h2>
|
||||||
|
<p className="text-gray-600 mb-4">Please try again later</p>
|
||||||
|
<Button onClick={() => refetchBlogs()}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#FFFFFF' }}>
|
<div style={{ backgroundColor: '#FFFFFF' }} ref={containerRef}>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative py-28 overflow-hidden">
|
<section className="relative py-28 overflow-hidden">
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
@@ -169,18 +252,16 @@ export function Articles() {
|
|||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="grid grid-cols-3 gap-8 text-center">
|
<div className="grid grid-cols-3 gap-8 text-center">
|
||||||
<div>
|
<div>
|
||||||
{/* FIXED: Using articlesData.length */}
|
<div className="text-h2-white mb-2">{blogsData?.data?.pagination?.total || 0}+</div>
|
||||||
<div className="text-h2-white mb-2">{articlesData.length}+</div>
|
|
||||||
<div className="text-small-white">Expert Articles</div>
|
<div className="text-small-white">Expert Articles</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{/* FIXED: Using categories from articlesData */}
|
|
||||||
<div className="text-h2-white mb-2">{categories.length - 1}</div>
|
<div className="text-h2-white mb-2">{categories.length - 1}</div>
|
||||||
<div className="text-small-white">Categories</div>
|
<div className="text-small-white">Categories</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-h2-white mb-2">25,400</div>
|
<div className="text-h2-white mb-2">{allTags.length}+</div>
|
||||||
<div className="text-small-white">Total Reads</div>
|
<div className="text-small-white">Topics</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +278,7 @@ export function Articles() {
|
|||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search articles, authors, topics..."
|
placeholder="Search articles..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => 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"
|
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() {
|
|||||||
<label className="block text-small mb-2 font-medium text-gray-700">
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
||||||
Category
|
Category
|
||||||
</label>
|
</label>
|
||||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
<Select
|
||||||
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors" style={{ '&:focus': { borderColor: 'var(--color-primary)' } }}>
|
value={selectedCategory.id}
|
||||||
<SelectValue placeholder="All Categories" />
|
onValueChange={(value: string) => {
|
||||||
|
const category = categories.find(c => c.id === value);
|
||||||
|
if (category) {
|
||||||
|
setSelectedCategory(category);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
||||||
|
<SelectValue placeholder="All Categories">
|
||||||
|
{selectedCategory.name}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<SelectItem key={category} value={category} className="text-small">
|
<SelectItem key={category.id} value={category.id} className="text-small">
|
||||||
{category}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Author Filter */}
|
{/* Read Time Filter - Client-side only */}
|
||||||
<div className="filter-section">
|
|
||||||
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
||||||
Author
|
|
||||||
</label>
|
|
||||||
<Select value={selectedAuthor} onValueChange={setSelectedAuthor}>
|
|
||||||
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors" style={{ '&:focus': { borderColor: 'var(--color-primary)' } }}>
|
|
||||||
<SelectValue placeholder="All Authors" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{authors.map((author) => (
|
|
||||||
<SelectItem key={author} value={author} className="text-small">
|
|
||||||
{author}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Read Time Filter */}
|
|
||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
<label className="block text-small mb-2 font-medium text-gray-700">
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
||||||
Read Time
|
Read Time
|
||||||
</label>
|
</label>
|
||||||
<Select value={selectedReadTime} onValueChange={setSelectedReadTime}>
|
<Select value={selectedReadTime} onValueChange={setSelectedReadTime}>
|
||||||
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors" style={{ '&:focus': { borderColor: 'var(--color-primary)' } }}>
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
||||||
<SelectValue placeholder="All Read Times" />
|
<SelectValue placeholder="All Read Times" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -356,13 +428,13 @@ export function Articles() {
|
|||||||
Date Range
|
Date Range
|
||||||
</label>
|
</label>
|
||||||
<Select value={selectedDateRange} onValueChange={setSelectedDateRange}>
|
<Select value={selectedDateRange} onValueChange={setSelectedDateRange}>
|
||||||
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors" style={{ '&:focus': { borderColor: 'var(--color-primary)' } }}>
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
||||||
<SelectValue placeholder="All Time" />
|
<SelectValue placeholder="All Time" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{dateRanges.map((dateRange) => (
|
{dateRanges.map((range) => (
|
||||||
<SelectItem key={dateRange} value={dateRange} className="text-small">
|
<SelectItem key={range.value} value={range.value}>
|
||||||
{dateRange}
|
{range.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -374,17 +446,28 @@ export function Articles() {
|
|||||||
<label className="block text-small mb-2 font-medium text-gray-700">
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
||||||
Topic
|
Topic
|
||||||
</label>
|
</label>
|
||||||
<Select value={selectedTopic} onValueChange={setSelectedTopic}>
|
<Select
|
||||||
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors" style={{ '&:focus': { borderColor: 'var(--color-primary)' } }}>
|
value={selectedTopic.id}
|
||||||
|
onValueChange={(value: string) => {
|
||||||
|
if (value === 'all') {
|
||||||
|
setSelectedTopic({ id: 'all', name: 'All Topics' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const topic = allTags.find(t => t.id === value);
|
||||||
|
if (topic) setSelectedTopic(topic);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
||||||
<SelectValue placeholder="All Topics" />
|
<SelectValue placeholder="All Topics" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="All Topics" className="text-small">
|
<SelectItem value="all">
|
||||||
All Topics
|
All Topics
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
{allTags.map((tag) => (
|
{allTags.map((tag) => (
|
||||||
<SelectItem key={tag} value={tag} className="text-small">
|
<SelectItem key={tag.id} value={tag.id}>
|
||||||
{tag}
|
{tag.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -399,73 +482,85 @@ export function Articles() {
|
|||||||
{/* Right Content Area - Scrollable Articles */}
|
{/* Right Content Area - Scrollable Articles */}
|
||||||
<div className="col-span-12 lg:col-span-9">
|
<div className="col-span-12 lg:col-span-9">
|
||||||
<div className="mb-4 text-small text-muted">
|
<div className="mb-4 text-small text-muted">
|
||||||
Showing {currentArticles.length} of {filteredArticles.length} articles
|
Showing {finalFilteredArticles.length} of {blogsData?.data?.pagination?.total || 0} articles
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Articles Results */}
|
{/* Articles Results */}
|
||||||
{currentArticles.length === 0 ? (
|
{finalFilteredArticles.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
|
<BookOpen className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
<p className="text-body-lg text-muted">
|
<p className="text-body-lg text-muted">
|
||||||
No articles found matching your criteria.
|
No articles found matching your criteria.
|
||||||
</p>
|
</p>
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Grid View */}
|
{/* Grid View */}
|
||||||
{viewMode === 'grid' && (
|
{viewMode === 'grid' && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{currentArticles.map((article) => (
|
{finalFilteredArticles.map((article: BlogItem) => (
|
||||||
<Card
|
<Card
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
||||||
onClick={() => 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}`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="aspect-video w-full bg-gray-100 overflow-hidden relative">
|
<div className="aspect-video w-full bg-gray-100 overflow-hidden relative">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={article.thumbnail}
|
src={article.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnRpY2xlJTIwYmxvZyUyMGNvbnRlbnQlMjBrbm93bGVkZ2V8ZW58MXx8fHwxNzU1ODU0Mjg2fDA&ixlib=rb-4.1.0&q=80&w=1080'}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
{article.featured && (
|
|
||||||
<div className="absolute top-4 right-4">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-yellow-100 text-yellow-800 border-yellow-200"
|
|
||||||
>
|
|
||||||
Featured
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Badge variant="outline" className="text-small">
|
<Badge variant="outline" className="text-small">
|
||||||
{article.category}
|
{article.content_category}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-small text-muted">{article.readTime}</span>
|
<span className="text-small text-muted">
|
||||||
|
{calculateReadTime(article.content)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-h4 mb-3 group-hover:text-blue-600 transition-colors line-clamp-2">
|
<h3 className="text-h4 mb-3 group-hover:text-[#04045B] transition-colors line-clamp-2">
|
||||||
{article.title}
|
{article.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-small text-muted mb-4 line-clamp-3">
|
<p className="text-small text-muted mb-4 line-clamp-3">
|
||||||
{article.excerpt}
|
{article.short_description || article.content.substring(0, 150) + '...'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={article.authorAvatar}
|
|
||||||
alt={article.author}
|
|
||||||
className="w-6 h-6 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
<span className="text-small">{article.author}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-small text-muted">
|
<div className="text-small text-muted">
|
||||||
{formatDate(article.date)}
|
{formatDate(article.updated_at)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Display tags if available */}
|
||||||
|
{article.blog_tags && article.blog_tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-3 pt-3 border-t border-gray-100">
|
||||||
|
{article.blog_tags.slice(0, 3).map((tag, idx) => (
|
||||||
|
<Badge key={idx} variant="secondary" className="text-xs">
|
||||||
|
{tag.tag_name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{article.blog_tags.length > 3 && (
|
||||||
|
<span className="text-xs text-gray-500">+{article.blog_tags.length - 3}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -475,29 +570,23 @@ export function Articles() {
|
|||||||
{/* List View */}
|
{/* List View */}
|
||||||
{viewMode === 'list' && (
|
{viewMode === 'list' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{currentArticles.map((article) => (
|
{finalFilteredArticles.map((article: BlogItem) => (
|
||||||
<Card
|
<Card
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
className="overflow-hidden hover:shadow-lg transition-all duration-300 cursor-pointer group"
|
||||||
onClick={() => 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}`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col md:flex-row">
|
<div className="flex flex-col md:flex-row">
|
||||||
<div className="md:w-80 h-48 md:h-auto bg-gray-100 overflow-hidden relative flex-shrink-0">
|
<div className="md:w-80 h-48 md:h-auto bg-gray-100 overflow-hidden relative flex-shrink-0">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={article.thumbnail}
|
src={article.banner_img || 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnRpY2xlJTIwYmxvZyUyMGNvbnRlbnQlMjBrbm93bGVkZ2V8ZW58MXx8fHwxNzU1ODU0Mjg2fDA&ixlib=rb-4.1.0&q=80&w=1080'}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
{article.featured && (
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-yellow-100 text-yellow-800 border-yellow-200 text-xs"
|
|
||||||
>
|
|
||||||
Featured
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 p-6">
|
<div className="flex-1 p-6">
|
||||||
@@ -505,38 +594,39 @@ export function Articles() {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Badge variant="outline" className="text-small">
|
<Badge variant="outline" className="text-small">
|
||||||
{article.category}
|
{article.content_category}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-small text-muted">{article.readTime}</span>
|
<span className="text-small text-muted">
|
||||||
<div className="flex items-center gap-1 text-small text-muted">
|
{calculateReadTime(article.content)}
|
||||||
<Eye className="w-3 h-3" />
|
</span>
|
||||||
<span>{article.views}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-h4 mb-2 group-hover:text-blue-600 transition-colors">
|
<h3 className="text-h4 mb-2 group-hover:text-[#04045B] transition-colors">
|
||||||
{article.title}
|
{article.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-body text-muted mb-3">
|
<p className="text-body text-muted mb-3">
|
||||||
{article.excerpt}
|
{article.short_description || article.content.substring(0, 200) + '...'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ImageWithFallback
|
|
||||||
src={article.authorAvatar}
|
|
||||||
alt={article.author}
|
|
||||||
className="w-6 h-6 rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<span className="text-small font-medium">{article.author}</span>
|
|
||||||
<span className="text-small text-muted ml-1">• {article.authorTitle}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-small text-muted">
|
<div className="text-small text-muted">
|
||||||
{formatDate(article.date)}
|
{formatDate(article.updated_at)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Display tags if available */}
|
||||||
|
{article.blog_tags && article.blog_tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{article.blog_tags.slice(0, 2).map((tag, idx) => (
|
||||||
|
<Badge key={idx} variant="secondary" className="text-xs">
|
||||||
|
{tag.tag_name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{article.blog_tags.length > 2 && (
|
||||||
|
<span className="text-xs text-gray-500">+{article.blog_tags.length - 2}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -592,7 +682,7 @@ export function Articles() {
|
|||||||
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}}
|
}}
|
||||||
className={`min-w-10 ${currentPage === page
|
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'
|
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Card, CardContent } from './ui/card';
|
import { Card, CardContent } from './ui/card';
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
import { navigateTo } from './Router';
|
|
||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
import { CTABannerSection } from './CTABannerSection';
|
import { CTABannerSection } from './CTABannerSection';
|
||||||
import { useCart } from './CartContext';
|
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 {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
ArrowRight,
|
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Share2,
|
|
||||||
Bookmark,
|
Bookmark,
|
||||||
Twitter,
|
Twitter,
|
||||||
Facebook,
|
Facebook,
|
||||||
@@ -24,202 +19,91 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
Eye,
|
Eye,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
ArrowLeft,
|
ArrowLeft
|
||||||
Star,
|
|
||||||
MessageCircle,
|
|
||||||
Users,
|
|
||||||
TrendingUp,
|
|
||||||
Award
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { articlesData } from '../data/articlesData';
|
import { useGetBlogByIDQuery, useGetBlogsQuery } from '../redux/services/blogApi';
|
||||||
import { useParams } from 'react-router-dom';
|
import { FullScreenLoader } from './FullScreenLoader';
|
||||||
|
import { extractIdFromSlug, extractSlugFromSlugAndId, getSlugWithId } from '../utils/urlHelpers';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BlogDetailProps {
|
interface BlogDetailProps {
|
||||||
params?: {
|
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: `
|
|
||||||
<p>We're sorry, but the article you're looking for could not be found. It may have been moved or removed.</p>
|
|
||||||
<p>Available articles:</p>
|
|
||||||
<ul>
|
|
||||||
${articlesData.slice(0, 6).map(article =>
|
|
||||||
`<li><a href="/learning/articles/${article.slug}">${article.title}</a></li>`
|
|
||||||
).join('')}
|
|
||||||
</ul>
|
|
||||||
`,
|
|
||||||
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) {
|
export function BlogDetail({ params }: BlogDetailProps) {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slugAndId } = useParams<{ slugAndId: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [scrollProgress, setScrollProgress] = useState(0);
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
const [isBookmarked, setIsBookmarked] = useState(false);
|
const [isBookmarked, setIsBookmarked] = useState(false);
|
||||||
const { addToCart } = useCart();
|
const { addToCart } = useCart();
|
||||||
|
|
||||||
// Get blog post data based on slug
|
// Extract full ID from URL using the new function
|
||||||
const blogPost = getBlogPostBySlug(slug || "");
|
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(() => {
|
useEffect(() => {
|
||||||
document.title = `${blogPost.title} | KLC Blog`;
|
if (blogPost?.title) {
|
||||||
|
document.title = `${blogPost.title} | KLC Blog`;
|
||||||
|
}
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
|
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
|
||||||
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
||||||
const scrolled = (winScroll / height) * 100;
|
const scrolled = (winScroll / height) * 100;
|
||||||
|
|
||||||
setScrollProgress(scrolled);
|
setScrollProgress(scrolled);
|
||||||
setShowBackToTop(winScroll > 300);
|
setShowBackToTop(winScroll > 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
}, [blogPost.title]);
|
}, [blogPost?.title]);
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('en-US', {
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
@@ -231,8 +115,8 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
|
|
||||||
const handleShare = (platform: string) => {
|
const handleShare = (platform: string) => {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
const title = blogPost.title;
|
const title = blogPost?.title || '';
|
||||||
|
|
||||||
const shareUrls = {
|
const shareUrls = {
|
||||||
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
|
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
|
||||||
facebook: `https://www.facebook.com/sharer/sharer.php?u=${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' });
|
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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||||
|
<FullScreenLoader text="Loading article..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error state
|
||||||
|
if (isBlogError || !blogPost) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white">
|
||||||
|
<div className="text-center max-w-md px-4">
|
||||||
|
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<BookOpen className="w-8 h-8 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-h3 mb-2">Article Not Found</h2>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
The article you're looking for could not be found. It may have been moved or removed.
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => navigate('/learning/articles')}>
|
||||||
|
Back to Articles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#FFFFFF' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#FFFFFF' }}>
|
||||||
{/* Scroll Progress Bar */}
|
{/* Scroll Progress Bar */}
|
||||||
<div className="fixed top-0 left-0 w-full h-1 z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.1)' }}>
|
<div className="fixed top-0 left-0 w-full h-1 z-50" style={{ backgroundColor: 'rgba(0, 0, 0, 0.1)' }}>
|
||||||
<div
|
<div
|
||||||
className="h-full transition-all duration-150"
|
className="h-full transition-all duration-150"
|
||||||
style={{
|
style={{
|
||||||
width: `${scrollProgress}%`,
|
width: `${scrollProgress}%`,
|
||||||
backgroundColor: '#04045B'
|
backgroundColor: '#04045B'
|
||||||
}}
|
}}
|
||||||
@@ -287,7 +229,7 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigateTo('/learning/articles')}
|
onClick={() => navigate('/learning/articles')}
|
||||||
className="p-0 h-auto font-medium hover:bg-transparent transition-colors"
|
className="p-0 h-auto font-medium hover:bg-transparent transition-colors"
|
||||||
style={{ color: '#6F6F6F' }}
|
style={{ color: '#6F6F6F' }}
|
||||||
>
|
>
|
||||||
@@ -295,7 +237,7 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
Back to Articles
|
Back to Articles
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-[#E5E7EB]">•</span>
|
<span className="text-[#E5E7EB]">•</span>
|
||||||
<span>{blogPost.category}</span>
|
<span>{blogPost.content_category || 'Article'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -307,64 +249,64 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
<header className="mb-16">
|
<header className="mb-16">
|
||||||
{/* Category Badge */}
|
{/* Category Badge */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Badge
|
<Badge
|
||||||
className="mb-6 text-small px-4 py-2 font-medium border-none"
|
className="mb-6 text-small px-4 py-2 font-medium border-none"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(4, 4, 91, 0.1)',
|
backgroundColor: 'rgba(4, 4, 91, 0.1)',
|
||||||
color: '#04045B'
|
color: '#04045B'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{blogPost.category}
|
{blogPost.content_category || 'Article'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
{/* Improved Typography Hierarchy */}
|
{/* Improved Typography Hierarchy */}
|
||||||
<h1 className="text-h1 mb-6 leading-tight" style={{ color: '#26231A' }}>
|
<h1 className="text-h1 mb-6 leading-tight" style={{ color: '#26231A' }}>
|
||||||
{blogPost.title}
|
{blogPost.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Constrained Width Excerpt for Better Readability */}
|
{/* Constrained Width Excerpt for Better Readability */}
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<p className="text-body-lg leading-relaxed" style={{ color: '#6F6F6F' }}>
|
<p className="text-body-lg leading-relaxed" style={{ color: '#6F6F6F' }}>
|
||||||
{blogPost.excerpt}
|
{blogPost.short_description || blogPost.content.substring(0, 200) + '...'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Enhanced Meta Bar with Cleaner Spacing */}
|
{/* Enhanced Meta Bar with Cleaner Spacing */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 p-6 rounded-xl border"
|
className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 p-6 rounded-xl border"
|
||||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', borderColor: 'rgba(0, 0, 0, 0.08)' }}
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)', borderColor: 'rgba(0, 0, 0, 0.08)' }}
|
||||||
>
|
>
|
||||||
{/* Author Info with Improved Layout */}
|
{/* Author Info with Improved Layout */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="w-14 h-14 ring-2 ring-white shadow-md">
|
<Avatar className="w-14 h-14 ring-2 ring-white shadow-md">
|
||||||
<AvatarImage src={blogPost.authorImage} alt={blogPost.author} />
|
<AvatarImage src="https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop" alt="Author" />
|
||||||
<AvatarFallback className="text-subhead font-medium">{blogPost.author.split(' ').map(n => n[0]).join('')}</AvatarFallback>
|
<AvatarFallback className="text-subhead font-medium">KLC</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-subhead font-medium mb-1" style={{ color: '#26231A' }}>
|
<div className="text-subhead font-medium mb-1" style={{ color: '#26231A' }}>
|
||||||
{blogPost.author}
|
KLC Team
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cleaner Meta Information with Subtle Dividers */}
|
{/* Cleaner Meta Information with Subtle Dividers */}
|
||||||
<div className="flex items-center gap-4 text-small" style={{ color: '#6F6F6F' }}>
|
<div className="flex items-center gap-4 text-small" style={{ color: '#6F6F6F' }}>
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<Calendar className="w-4 h-4" />
|
<Calendar className="w-4 h-4" />
|
||||||
{formatDate(blogPost.publishedDate)}
|
{formatDate(blogPost.updated_at || new Date().toISOString())}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
|
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
|
||||||
|
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
{blogPost.readTime}
|
{calculateReadTime(blogPost.content)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
|
<div className="w-1 h-1 rounded-full" style={{ backgroundColor: '#E5E7EB' }}></div>
|
||||||
|
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
{blogPost.views.toLocaleString()}
|
0
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,9 +321,9 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
className={`transition-colors ${isLiked ? 'text-red-500' : 'text-[#6F6F6F]'}`}
|
className={`transition-colors ${isLiked ? 'text-red-500' : 'text-[#6F6F6F]'}`}
|
||||||
>
|
>
|
||||||
<Heart className={`w-4 h-4 mr-2 ${isLiked ? 'fill-current' : ''}`} />
|
<Heart className={`w-4 h-4 mr-2 ${isLiked ? 'fill-current' : ''}`} />
|
||||||
{blogPost.likes + (isLiked ? 1 : 0)}
|
0
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -390,7 +332,7 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
>
|
>
|
||||||
<Bookmark className={`w-4 h-4 ${isBookmarked ? 'fill-current' : ''}`} />
|
<Bookmark className={`w-4 h-4 ${isBookmarked ? 'fill-current' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Share Options */}
|
{/* Share Options */}
|
||||||
<div className="flex items-center gap-1 ml-2 pl-2 border-l border-[#E5E7EB]">
|
<div className="flex items-center gap-1 ml-2 pl-2 border-l border-[#E5E7EB]">
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleShare('twitter')} className="text-[#6F6F6F] hover:text-[#04045B]">
|
<Button variant="ghost" size="sm" onClick={() => handleShare('twitter')} className="text-[#6F6F6F] hover:text-[#04045B]">
|
||||||
@@ -409,7 +351,7 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
{/* Featured Image with Better Aspect Ratio */}
|
{/* Featured Image with Better Aspect Ratio */}
|
||||||
<div className="aspect-[16/9] rounded-xl overflow-hidden mt-8 shadow-lg">
|
<div className="aspect-[16/9] rounded-xl overflow-hidden mt-8 shadow-lg">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src={blogPost.image}
|
src={blogPost.banner_img || 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=1200&h=600&fit=crop'}
|
||||||
alt={blogPost.title}
|
alt={blogPost.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -420,7 +362,7 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
<article className="mb-20">
|
<article className="mb-20">
|
||||||
{/* Full Width Container - Uses complete available width */}
|
{/* Full Width Container - Uses complete available width */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className="prose prose-xl max-w-none blog-article-content w-full"
|
className="prose prose-xl max-w-none blog-article-content w-full"
|
||||||
style={{
|
style={{
|
||||||
/* Enhanced Typography Hierarchy using Design System */
|
/* Enhanced Typography Hierarchy using Design System */
|
||||||
@@ -447,48 +389,50 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
color: '#26231A',
|
color: '#26231A',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
}}
|
} as React.CSSProperties}
|
||||||
dangerouslySetInnerHTML={{ __html: blogPost.content }}
|
dangerouslySetInnerHTML={{ __html: blogPost.content }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{/* Enhanced Tag Pills with Hover States */}
|
{/* Enhanced Tag Pills with Hover States */}
|
||||||
<div className="mb-16">
|
{blogPost.blog_tags && blogPost.blog_tags.length > 0 && (
|
||||||
<h3 className="text-subhead mb-6 font-medium" style={{ color: '#26231A' }}>
|
<div className="mb-16">
|
||||||
Topics covered in this article
|
<h3 className="text-subhead mb-6 font-medium" style={{ color: '#26231A' }}>
|
||||||
</h3>
|
Topics covered in this article
|
||||||
<div className="flex flex-wrap gap-3">
|
</h3>
|
||||||
{blogPost.tags.map((tag) => (
|
<div className="flex flex-wrap gap-3">
|
||||||
<Badge
|
{blogPost.blog_tags.map((tag: any) => (
|
||||||
key={tag}
|
<Badge
|
||||||
className="transition-all duration-200 text-body px-4 py-2 font-medium"
|
key={tag.tag_name}
|
||||||
style={{
|
className="transition-all duration-200 text-body px-4 py-2 font-medium"
|
||||||
backgroundColor: 'rgba(4, 4, 91, 0.08)',
|
style={{
|
||||||
color: '#04045B',
|
backgroundColor: 'rgba(4, 4, 91, 0.08)',
|
||||||
border: '1px solid rgba(4, 4, 91, 0.15)'
|
color: '#04045B',
|
||||||
}}
|
border: '1px solid rgba(4, 4, 91, 0.15)'
|
||||||
>
|
}}
|
||||||
{tag}
|
>
|
||||||
</Badge>
|
{tag.tag_name}
|
||||||
))}
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Enhanced Author Bio Card */}
|
{/* Enhanced Author Bio Card */}
|
||||||
<Card className="mb-16 shadow-md border-0" style={{ backgroundColor: '#FFFFFF' }}>
|
<Card className="mb-16 shadow-md border-0" style={{ backgroundColor: '#FFFFFF' }}>
|
||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex items-start gap-6">
|
<div className="flex items-start gap-6">
|
||||||
<Avatar className="w-20 h-20 ring-4 ring-white shadow-lg">
|
<Avatar className="w-20 h-20 ring-4 ring-white shadow-lg">
|
||||||
<AvatarImage src={blogPost.authorImage} alt={blogPost.author} />
|
<AvatarImage src="https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop" alt="Author" />
|
||||||
<AvatarFallback className="text-lg font-medium">{blogPost.author.split(' ').map(n => n[0]).join('')}</AvatarFallback>
|
<AvatarFallback className="text-lg font-medium">KLC</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="text-h4 mb-3 font-semibold" style={{ color: '#26231A' }}>
|
<h4 className="text-h4 mb-3 font-semibold" style={{ color: '#26231A' }}>
|
||||||
About {blogPost.author}
|
About KLC Team
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-body leading-relaxed mb-6" style={{ color: '#6F6F6F' }}>
|
<p className="text-body leading-relaxed mb-6" style={{ color: '#6F6F6F' }}>
|
||||||
{blogPost.authorBio}
|
The Kautilya Leadership Center team is dedicated to providing cutting-edge insights and research on leadership development, management strategies, and organizational excellence.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -498,95 +442,101 @@ export function BlogDetail({ params }: BlogDetailProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Related Articles Section with Balanced Grid Layout */}
|
{/* Related Articles Section with Balanced Grid Layout */}
|
||||||
<section className="py-20" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
|
{relatedPosts.length > 0 && (
|
||||||
<div className="section-margin-x">
|
<section className="py-20" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="section-margin-x">
|
||||||
<div className="text-center mb-16">
|
<div className="max-w-6xl mx-auto">
|
||||||
<div className="branded-tag-system mb-6">
|
<div className="text-center mb-16">
|
||||||
<div className="dot"></div>
|
<div className="branded-tag-system mb-6">
|
||||||
<span className="text">Continue Learning</span>
|
<div className="dot"></div>
|
||||||
|
<span className="text">Continue Learning</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-h2 mb-6 font-bold" style={{ color: '#26231A' }}>
|
||||||
|
Explore More Leadership Insights
|
||||||
|
</h2>
|
||||||
|
<p className="text-body-lg max-w-2xl mx-auto" style={{ color: '#6F6F6F' }}>
|
||||||
|
Discover additional strategies and frameworks to enhance your leadership effectiveness
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-h2 mb-6 font-bold" style={{ color: '#26231A' }}>
|
|
||||||
Explore More Leadership Insights
|
|
||||||
</h2>
|
|
||||||
<p className="text-body-lg max-w-2xl mx-auto" style={{ color: '#6F6F6F' }}>
|
|
||||||
Discover additional strategies and frameworks to enhance your leadership effectiveness
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Balanced Card Grid with Equal Spacing */}
|
{/* Balanced Card Grid with Equal Spacing */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{relatedPosts.map((post) => (
|
{relatedPosts.map((post: any) => (
|
||||||
<Card
|
<Card
|
||||||
key={post.id}
|
key={post.id}
|
||||||
className="overflow-hidden hover:shadow-xl transition-all duration-300 cursor-pointer group border-0"
|
className="overflow-hidden hover:shadow-xl transition-all duration-300 cursor-pointer group border-0"
|
||||||
onClick={() => navigateTo(`/learning/articles/${post.slug}`)}
|
onClick={() => {
|
||||||
style={{ backgroundColor: '#FFFFFF' }}
|
// Use the same pattern as the main articles with full UUID
|
||||||
>
|
const url = getSlugWithId(post.slug, post.id);
|
||||||
<div className="aspect-[16/10] w-full bg-gray-100 overflow-hidden relative">
|
navigate(`/learning/articles/${url}`);
|
||||||
<ImageWithFallback
|
}}
|
||||||
src={post.image}
|
style={{ backgroundColor: '#FFFFFF' }}
|
||||||
alt={post.title}
|
>
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
<div className="aspect-[16/10] w-full bg-gray-100 overflow-hidden relative">
|
||||||
/>
|
<ImageWithFallback
|
||||||
</div>
|
src={post.image}
|
||||||
<CardContent className="p-6">
|
alt={post.title}
|
||||||
<div className="flex items-center justify-between mb-3">
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
<Badge
|
/>
|
||||||
variant="outline"
|
</div>
|
||||||
className="text-small border-none"
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-small border-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(4, 4, 91, 0.1)',
|
||||||
|
color: '#04045B'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{post.category}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
||||||
|
{post.readTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
className="mb-3 group-hover:text-blue-600 transition-colors line-clamp-2"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(4, 4, 91, 0.1)',
|
fontSize: 'var(--font-h4)',
|
||||||
color: '#04045B'
|
fontWeight: 'var(--font-weight-h4)',
|
||||||
|
lineHeight: '1.3',
|
||||||
|
color: '#26231A',
|
||||||
|
fontFamily: 'var(--font-family-base)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{post.category}
|
{post.title}
|
||||||
</Badge>
|
</h3>
|
||||||
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
|
||||||
{post.readTime}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3
|
|
||||||
className="mb-3 group-hover:text-blue-600 transition-colors line-clamp-2"
|
|
||||||
style={{
|
|
||||||
fontSize: 'var(--font-h4)',
|
|
||||||
fontWeight: 'var(--font-weight-h4)',
|
|
||||||
lineHeight: '1.3',
|
|
||||||
color: '#26231A',
|
|
||||||
fontFamily: 'var(--font-family-base)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{post.title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className="mb-4 line-clamp-3"
|
|
||||||
style={{
|
|
||||||
fontSize: 'var(--font-body)',
|
|
||||||
lineHeight: '1.5',
|
|
||||||
color: '#6F6F6F',
|
|
||||||
fontFamily: 'var(--font-family-base)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{post.excerpt}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-4 border-t" style={{ borderColor: 'rgba(0, 0, 0, 0.05)' }}>
|
<p
|
||||||
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
className="mb-4 line-clamp-3"
|
||||||
{post.author}
|
style={{
|
||||||
</span>
|
fontSize: 'var(--font-body)',
|
||||||
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
lineHeight: '1.5',
|
||||||
{formatDate(post.publishedDate)}
|
color: '#6F6F6F',
|
||||||
</span>
|
fontFamily: 'var(--font-family-base)'
|
||||||
</div>
|
}}
|
||||||
</CardContent>
|
>
|
||||||
</Card>
|
{post.excerpt}
|
||||||
))}
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t" style={{ borderColor: 'rgba(0, 0, 0, 0.05)' }}>
|
||||||
|
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
||||||
|
{post.author}
|
||||||
|
</span>
|
||||||
|
<span className="text-small" style={{ color: '#6F6F6F' }}>
|
||||||
|
{formatDate(post.publishedDate)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
)}
|
||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<CTABannerSection />
|
<CTABannerSection />
|
||||||
|
|||||||
@@ -4,14 +4,45 @@ import { BrandedTag } from "./about/BrandedTag";
|
|||||||
import { PrimaryCTAButton } from "./PrimaryCTAButton";
|
import { PrimaryCTAButton } from "./PrimaryCTAButton";
|
||||||
import { navigateTo } from "./Router";
|
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 (
|
||||||
|
<section className="relative h-[700px] overflow-hidden bg-gray-100 animate-pulse">
|
||||||
|
<div className="absolute inset-0 bg-gray-200" />
|
||||||
|
<div className="relative h-full flex items-center justify-end section-margin-x">
|
||||||
|
<div className="bg-gray-300 rounded-lg p-16 max-w-2xl w-full h-96" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no CTA band is available, don't render anything
|
||||||
|
if (!ctaBand) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative h-[700px] overflow-hidden">
|
<section className="relative h-[700px] overflow-hidden">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<ImageWithFallback
|
<ImageWithFallback
|
||||||
src="https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80"
|
src={ctaBand.background_image_url || "https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2940&q=80"}
|
||||||
alt="Professional team collaborating in modern office"
|
alt={ctaBand.background_image_alt_text || "Professional team collaborating in modern office"}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -34,11 +65,11 @@ export function CTABannerSection() {
|
|||||||
{/* Branded Tag */}
|
{/* Branded Tag */}
|
||||||
<BrandedTag text="Next Steps" variant="white" />
|
<BrandedTag text="Next Steps" variant="white" />
|
||||||
|
|
||||||
{/* Main Headline */}
|
{/* Main Headline - Use API text or fallback */}
|
||||||
<h2
|
<h2
|
||||||
className="text-h2-white mb-8"
|
className="text-h2-white mb-8"
|
||||||
>
|
>
|
||||||
Ready to transform your leadership?
|
{ctaBand.text || "Ready to transform your leadership?"}
|
||||||
<span
|
<span
|
||||||
className="italic"
|
className="italic"
|
||||||
style={{ color: 'var(--color-brand-accent)' }}
|
style={{ color: 'var(--color-brand-accent)' }}
|
||||||
@@ -48,10 +79,10 @@ export function CTABannerSection() {
|
|||||||
to start your development journey now.
|
to start your development journey now.
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* CTA Button - Updated to redirect to contact page */}
|
{/* CTA Button */}
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="Schedule a Consultation"
|
text={ctaBand.cta_text || "Schedule a Consultation"}
|
||||||
onClick={() => navigateTo('/contact?topic=consulting')}
|
onClick={() => navigateTo(ctaBand.cta_destination || '/contact?topic=consulting')}
|
||||||
ariaLabel="Schedule a consultation with our leadership experts"
|
ariaLabel="Schedule a consultation with our leadership experts"
|
||||||
className="cta-banner-yellow"
|
className="cta-banner-yellow"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { PrimaryCTAButton } from './PrimaryCTAButton';
|
|||||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||||
import { navigateTo } from './Router';
|
import { navigateTo } from './Router';
|
||||||
import kautilyabg from '../assets/Kautilya.png';
|
import kautilyabg from '../assets/Kautilya.png';
|
||||||
|
import { useCreateLeadMutation, useGetLeadCategoriesQuery } from '../redux/services/contactUsApi';
|
||||||
|
|
||||||
interface ContactProps {
|
interface ContactProps {
|
||||||
topic?: string;
|
topic?: string;
|
||||||
@@ -19,13 +20,23 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
name: '',
|
name: '',
|
||||||
mobileNumber: '',
|
mobileNumber: '',
|
||||||
emailId: '',
|
emailId: '',
|
||||||
interestedIn: topic || '',
|
interestedIn: '',
|
||||||
message: ''
|
message: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = 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(() => {
|
useEffect(() => {
|
||||||
console.log('Contact component mounted with topic:', topic);
|
console.log('Contact component mounted with topic:', topic);
|
||||||
// Set default interested in based on topic parameter
|
// Set default interested in based on topic parameter
|
||||||
@@ -65,10 +76,22 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
// Simulate form submission
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await createLead({
|
||||||
|
full_name: formData.name,
|
||||||
setIsSubmitted(true);
|
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);
|
setIsSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,16 +103,21 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<CheckCircle2 className="w-10 h-10 text-green-600" />
|
<CheckCircle2 className="w-10 h-10 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-h2 mb-4">Thank You!</h1>
|
<h1 className="text-h2 mb-4">Thank You!</h1>
|
||||||
|
|
||||||
<p className="text-body-lg text-muted mb-8">
|
<p className="text-body-lg text-muted mb-8">
|
||||||
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.
|
Our leadership development experts are excited to help transform your organization.
|
||||||
</p>
|
</p>
|
||||||
<PrimaryCTAButton
|
|
||||||
text="Return to Home"
|
<div className="flex justify-center">
|
||||||
onClick={() => navigateTo('/')}
|
<PrimaryCTAButton
|
||||||
ariaLabel="Return to homepage"
|
text="Return to Home"
|
||||||
/>
|
onClick={() => navigateTo('/')}
|
||||||
|
ariaLabel="Return to homepage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,7 +135,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
alt="Google Maps showing office location and navigation"
|
alt="Google Maps showing office location and navigation"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dark overlay for better text readability */}
|
{/* Dark overlay for better text readability */}
|
||||||
<div className="absolute inset-0 bg-black/60" />
|
<div className="absolute inset-0 bg-black/60" />
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +168,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="bg-white rounded-xl shadow-lg border border-gray-100 h-fit overflow-hidden">
|
<div className="bg-white rounded-xl shadow-lg border border-gray-100 h-fit overflow-hidden">
|
||||||
{/* Contact Info Header */}
|
{/* Contact Info Header */}
|
||||||
<div
|
<div
|
||||||
className="p-8 text-white"
|
className="p-8 text-white"
|
||||||
style={{ backgroundColor: 'var(--color-brand-primary)' }}
|
style={{ backgroundColor: 'var(--color-brand-primary)' }}
|
||||||
>
|
>
|
||||||
@@ -154,7 +182,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
<div className="p-8 space-y-8">
|
<div className="p-8 space-y-8">
|
||||||
{/* Corporate Office */}
|
{/* Corporate Office */}
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||||
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
||||||
>
|
>
|
||||||
@@ -173,7 +201,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
|
|
||||||
{/* Registered Office */}
|
{/* Registered Office */}
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||||
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
||||||
>
|
>
|
||||||
@@ -192,7 +220,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||||
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
||||||
>
|
>
|
||||||
@@ -209,7 +237,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
|
|
||||||
{/* Phone */}
|
{/* Phone */}
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
className="w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||||
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
||||||
>
|
>
|
||||||
@@ -226,7 +254,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div
|
<div
|
||||||
className="p-8 border-t"
|
className="p-8 border-t"
|
||||||
style={{ borderColor: 'var(--color-border)' }}
|
style={{ borderColor: 'var(--color-border)' }}
|
||||||
>
|
>
|
||||||
@@ -236,7 +264,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300"
|
className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300"
|
||||||
onClick={() => navigateTo('/services/learning-facility')}
|
onClick={() => navigateTo('/services/learning-facility')}
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-brand-primary)',
|
borderColor: 'var(--color-brand-primary)',
|
||||||
color: 'var(--color-brand-primary)'
|
color: 'var(--color-brand-primary)'
|
||||||
}}
|
}}
|
||||||
@@ -248,7 +276,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300"
|
className="w-full justify-start hover:bg-blue-50 hover:border-blue-200 transition-all duration-300"
|
||||||
onClick={() => navigateTo('/leadership-journey')}
|
onClick={() => navigateTo('/leadership-journey')}
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-brand-primary)',
|
borderColor: 'var(--color-brand-primary)',
|
||||||
color: 'var(--color-brand-primary)'
|
color: 'var(--color-brand-primary)'
|
||||||
}}
|
}}
|
||||||
@@ -267,7 +295,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
{/* Form Header */}
|
{/* Form Header */}
|
||||||
<div className="p-8 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
<div className="p-8 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||||||
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
style={{ backgroundColor: 'rgba(248, 195, 1, 0.1)' }}
|
||||||
>
|
>
|
||||||
@@ -279,7 +307,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
Fill out the form below and we'll get back to you within 24 hours.
|
Fill out the form below and we'll get back to you within 24 hours.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Content */}
|
{/* Form Content */}
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -294,7 +322,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
placeholder="Enter your full name"
|
placeholder="Enter your full name"
|
||||||
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
fontSize: 'var(--font-body)'
|
fontSize: 'var(--font-body)'
|
||||||
}}
|
}}
|
||||||
@@ -312,7 +340,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
onChange={(e) => handleInputChange('mobileNumber', e.target.value)}
|
||||||
placeholder="+91 98765 43210"
|
placeholder="+91 98765 43210"
|
||||||
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
fontSize: 'var(--font-body)'
|
fontSize: 'var(--font-body)'
|
||||||
}}
|
}}
|
||||||
@@ -330,7 +358,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
onChange={(e) => handleInputChange('emailId', e.target.value)}
|
onChange={(e) => handleInputChange('emailId', e.target.value)}
|
||||||
placeholder="example@company.com"
|
placeholder="example@company.com"
|
||||||
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
fontSize: 'var(--font-body)'
|
fontSize: 'var(--font-body)'
|
||||||
}}
|
}}
|
||||||
@@ -345,9 +373,9 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
value={formData.interestedIn}
|
value={formData.interestedIn}
|
||||||
onValueChange={(value: string) => handleInputChange('interestedIn', value)}
|
onValueChange={(value: string) => handleInputChange('interestedIn', value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
className="w-full h-12 border-gray-200 focus:border-blue-500 focus:ring-blue-500"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
fontSize: 'var(--font-body)'
|
fontSize: 'var(--font-body)'
|
||||||
}}
|
}}
|
||||||
@@ -355,14 +383,13 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
<SelectValue placeholder="Select your area of interest" />
|
<SelectValue placeholder="Select your area of interest" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="Leadership Development">Leadership Development</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="Executive Coaching">Executive Coaching</SelectItem>
|
{categories.map((cat: any) => (
|
||||||
<SelectItem value="Management Development">Management Development</SelectItem>
|
<SelectItem key={cat.id} value={cat.id}>
|
||||||
<SelectItem value="Culture Competence">Culture Competence</SelectItem>
|
{cat.category_type}
|
||||||
<SelectItem value="Consulting Services">Consulting Services</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="Learning Facility">Learning Facility</SelectItem>
|
))}
|
||||||
<SelectItem value="Online Courses">Online Courses</SelectItem>
|
</SelectContent>
|
||||||
<SelectItem value="General Inquiry">General Inquiry</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,7 +405,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
placeholder="Please enter your message or inquiry..."
|
placeholder="Please enter your message or inquiry..."
|
||||||
rows={6}
|
rows={6}
|
||||||
className="w-full border-gray-200 focus:border-blue-500 focus:ring-blue-500 resize-none"
|
className="w-full border-gray-200 focus:border-blue-500 focus:ring-blue-500 resize-none"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'var(--font-family-base)',
|
fontFamily: 'var(--font-family-base)',
|
||||||
fontSize: 'var(--font-body)'
|
fontSize: 'var(--font-body)'
|
||||||
}}
|
}}
|
||||||
@@ -392,7 +419,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
</div>
|
</div>
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text={isSubmitting ? "Sending Message..." : "Send Message"}
|
text={isSubmitting ? "Sending Message..." : "Send Message"}
|
||||||
onClick={() => {}}
|
onClick={() => { handleSubmit }}
|
||||||
ariaLabel="Send contact message"
|
ariaLabel="Send contact message"
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
@@ -415,19 +442,19 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
alt="Professional team collaborating in modern office"
|
alt="Professional team collaborating in modern office"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Subtle dark overlay for overall image */}
|
{/* Subtle dark overlay for overall image */}
|
||||||
<div className="absolute inset-0 bg-black/30" />
|
<div className="absolute inset-0 bg-black/30" />
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<div className="relative h-full flex items-center justify-end section-margin-x">
|
<div className="relative h-full flex items-center justify-end section-margin-x">
|
||||||
{/* CTA Content Block */}
|
{/* CTA Content Block */}
|
||||||
<div
|
<div
|
||||||
className="bg-opacity-95 backdrop-blur-sm rounded-lg p-16 max-w-2xl"
|
className="bg-opacity-95 backdrop-blur-sm rounded-lg p-16 max-w-2xl"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-brand-primary)'
|
backgroundColor: 'var(--color-brand-primary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -436,8 +463,8 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
|
|
||||||
{/* Main Headline */}
|
{/* Main Headline */}
|
||||||
<h2 className="text-h2-white mb-8">
|
<h2 className="text-h2-white mb-8">
|
||||||
Ready to transform your leadership?
|
Ready to transform your leadership?
|
||||||
<span
|
<span
|
||||||
className="italic"
|
className="italic"
|
||||||
style={{ color: 'var(--color-brand-accent)' }}
|
style={{ color: 'var(--color-brand-accent)' }}
|
||||||
>
|
>
|
||||||
@@ -446,7 +473,7 @@ export function Contact({ topic }: ContactProps) {
|
|||||||
to start your development journey now.
|
to start your development journey now.
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="Schedule a Consultation"
|
text="Schedule a Consultation"
|
||||||
onClick={() => navigateTo('/contact?topic=consulting')}
|
onClick={() => navigateTo('/contact?topic=consulting')}
|
||||||
ariaLabel="Schedule a consultation with our leadership experts"
|
ariaLabel="Schedule a consultation with our leadership experts"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function FooterNew() {
|
|||||||
About Us
|
About Us
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateTo('/learning/blogs')}
|
onClick={() => navigateTo('/learning/articles')}
|
||||||
className="block text-small-white transition-all duration-300 text-left"
|
className="block text-small-white transition-all duration-300 text-left"
|
||||||
style={{
|
style={{
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
|||||||
22
src/components/FullScreenLoader.tsx
Normal file
22
src/components/FullScreenLoader.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Loader } from "./Loader";
|
||||||
|
|
||||||
|
interface FullScreenLoaderProps {
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullScreenLoader: React.FC<FullScreenLoaderProps> = ({
|
||||||
|
text = "Loading...",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 flex flex-col items-center justify-center bg-white/100 z-[9999]">
|
||||||
|
<Loader />
|
||||||
|
|
||||||
|
{text && (
|
||||||
|
<p className="mt-6 text-lg text-gray-600">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { navigateTo } from './Router';
|
import { navigateTo } from "./Router";
|
||||||
import svgPaths from "../imports/svg-i1joeov37f";
|
import PrimaryCTAButton from "./PrimaryCTAButton";
|
||||||
import PrimaryCTAButton from './PrimaryCTAButton';
|
|
||||||
|
interface HeroSectionItem {
|
||||||
|
id: string;
|
||||||
|
headline: string;
|
||||||
|
subtext: string;
|
||||||
|
background_image_url: string;
|
||||||
|
cta_text: string;
|
||||||
|
cta_destination: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SlideData {
|
interface SlideData {
|
||||||
id: number;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
shortTitle: string;
|
shortTitle: string;
|
||||||
@@ -13,68 +21,42 @@ interface SlideData {
|
|||||||
route: string;
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HeroSection() {
|
interface HeroSectionProps {
|
||||||
const [currentSlide, setCurrentSlide] = useState(0);
|
heroSections: HeroSectionItem[];
|
||||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
isLoading: boolean;
|
||||||
const [progressValues, setProgressValues] = useState([0, 0, 0, 0, 0, 0]);
|
}
|
||||||
|
|
||||||
const slides: SlideData[] = [
|
export default function HeroSection({
|
||||||
{
|
heroSections,
|
||||||
id: 1,
|
isLoading,
|
||||||
title: "Know Your Leaders. Strengthen Your Pipeline.",
|
}: HeroSectionProps) {
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1697059361419-349e924ed363?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidXNpbmVzcyUyMGxlYWRlcnMlMjBzdHJhdGVneSUyMG1lZXRpbmd8ZW58MXx8fHwxNzU2ODA3Mjc0fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
const slides: SlideData[] = heroSections.map((item) => ({
|
||||||
shortTitle: "Leadership Pipeline Development",
|
id: item.id,
|
||||||
ctaText: "Discover Our Assessment Solutions",
|
title: item.headline,
|
||||||
route: '/services/leadership-pipeline-development'
|
backgroundImage: item.background_image_url,
|
||||||
},
|
shortTitle: item.subtext,
|
||||||
{
|
ctaText: item.cta_text,
|
||||||
id: 2,
|
route: item.cta_destination,
|
||||||
title: "Build Leaders Who Drive Business Growth",
|
}));
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1705234384669-c6d31c61b789?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxleGVjdXRpdmUlMjBsZWFkZXJzaGlwJTIwZGV2ZWxvcG1lbnQlMjB0cmFpbmluZ3xlbnwxfHx8fDE3NTY4MDcyNjJ8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
shortTitle: "Leadership Development",
|
|
||||||
ctaText: "Explore Leadership Journeys",
|
|
||||||
route: '/services/leadership-development'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Strengthen the Backbone: Your Managers",
|
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1565688527174-775059ac429c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtYW5hZ2VtZW50JTIwdGVhbSUyMGRpc2N1c3Npb24lMjBvZmZpY2V8ZW58MXx8fHwxNzU2ODA2ODg1fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
shortTitle: "Management Development",
|
|
||||||
ctaText: "Strengthen your Managerial Calibre",
|
|
||||||
route: '/services/management-development'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Shape Cultures That Accelerate Performance",
|
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1531535807748-218331acbcb4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjb3Jwb3JhdGUlMjBjdWx0dXJlJTIwdGVhbSUyMGNvbGxhYm9yYXRpb258ZW58MXx8fHwxNzU2ODA2ODg5fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
shortTitle: "Culture & Competence Consulting",
|
|
||||||
ctaText: "Learn how we facilitate Change",
|
|
||||||
route: '/services/culture-competence'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Unlock Leadership Potential",
|
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1714974528833-a10e19a8f951?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxleGVjdXRpdmUlMjBjb2FjaGluZyUyMG1lbnRvciUyMGNvbnZlcnNhdGlvbnxlbnwxfHx8fDE3NTY4MDY4OTR8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
shortTitle: "Coaching & Mentoring",
|
|
||||||
ctaText: "Start a Coaching Conversation",
|
|
||||||
route: '/services/executive-coaching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Experience Learning in a World-Class Environment",
|
|
||||||
backgroundImage: "https://images.unsplash.com/photo-1582213782179-e0d53f98f2ca?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjb25mZXJlbmNlJTIwY2VudGVyJTIwbGVhcm5pbmclMjBmYWNpbGl0eXxlbnwxfHx8fDE3NTY4MDc0MDB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
|
|
||||||
shortTitle: "Learning Facility",
|
|
||||||
ctaText: "Explore Our Learning Facility",
|
|
||||||
route: '/services/learning-facility'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const totalSlides = slides.length;
|
const totalSlides = slides.length;
|
||||||
const slideDuration = 5000; // 5 seconds per slide
|
|
||||||
|
|
||||||
// Auto-advance slides
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||||
|
const [progressValues, setProgressValues] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const slideDuration = 5000;
|
||||||
|
|
||||||
|
/* Initialize progress array when slides load */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAutoPlaying) return;
|
if (totalSlides > 0) {
|
||||||
|
setProgressValues(new Array(totalSlides).fill(0));
|
||||||
|
}
|
||||||
|
}, [totalSlides]);
|
||||||
|
|
||||||
|
/* Auto slide */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAutoPlaying || totalSlides === 0) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setCurrentSlide((prev) => (prev + 1) % totalSlides);
|
setCurrentSlide((prev) => (prev + 1) % totalSlides);
|
||||||
@@ -83,48 +65,50 @@ export default function HeroSection() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [isAutoPlaying, totalSlides]);
|
}, [isAutoPlaying, totalSlides]);
|
||||||
|
|
||||||
// Progress bar animation
|
/* Progress animation */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAutoPlaying) return;
|
if (!isAutoPlaying || totalSlides === 0) return;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setProgressValues(prev => {
|
setProgressValues((prev) => {
|
||||||
const newProgress = [...prev];
|
const newProgress = [...prev];
|
||||||
newProgress[currentSlide] = Math.min(newProgress[currentSlide] + (100 / (slideDuration / 100)), 100);
|
|
||||||
// Reset progress when slide changes
|
newProgress[currentSlide] = Math.min(
|
||||||
|
newProgress[currentSlide] + 100 / (slideDuration / 100),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
if (newProgress[currentSlide] >= 100) {
|
if (newProgress[currentSlide] >= 100) {
|
||||||
newProgress[currentSlide] = 0;
|
newProgress[currentSlide] = 0;
|
||||||
// Reset other slides
|
|
||||||
newProgress.forEach((_, index) => {
|
newProgress.forEach((_, index) => {
|
||||||
if (index !== currentSlide) {
|
if (index !== currentSlide) newProgress[index] = 0;
|
||||||
newProgress[index] = 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newProgress;
|
return newProgress;
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [currentSlide, isAutoPlaying]);
|
}, [currentSlide, isAutoPlaying, totalSlides]);
|
||||||
|
|
||||||
// Reset progress when manually changing slides
|
/* Reset progress when slide changes */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProgressValues(prev => {
|
setProgressValues(new Array(totalSlides).fill(0));
|
||||||
const newProgress = [0, 0, 0, 0, 0, 0];
|
}, [currentSlide, totalSlides]);
|
||||||
newProgress[currentSlide] = 0;
|
|
||||||
return newProgress;
|
|
||||||
});
|
|
||||||
}, [currentSlide]);
|
|
||||||
|
|
||||||
const goToSlide = useCallback((slideIndex: number) => {
|
const goToSlide = useCallback(
|
||||||
if (slideIndex !== currentSlide) {
|
(slideIndex: number) => {
|
||||||
setCurrentSlide(slideIndex);
|
if (slideIndex !== currentSlide) {
|
||||||
setIsAutoPlaying(false);
|
setCurrentSlide(slideIndex);
|
||||||
// Resume auto-play after manual interaction
|
setIsAutoPlaying(false);
|
||||||
setTimeout(() => setIsAutoPlaying(true), 3000);
|
|
||||||
}
|
setTimeout(() => setIsAutoPlaying(true), 3000);
|
||||||
}, [currentSlide]);
|
}
|
||||||
|
},
|
||||||
|
[currentSlide]
|
||||||
|
);
|
||||||
|
|
||||||
const nextSlide = useCallback(() => {
|
const nextSlide = useCallback(() => {
|
||||||
const next = (currentSlide + 1) % totalSlides;
|
const next = (currentSlide + 1) % totalSlides;
|
||||||
@@ -136,10 +120,11 @@ export default function HeroSection() {
|
|||||||
goToSlide(prev);
|
goToSlide(prev);
|
||||||
}, [currentSlide, totalSlides, goToSlide]);
|
}, [currentSlide, totalSlides, goToSlide]);
|
||||||
|
|
||||||
// Pause auto-play on hover
|
|
||||||
const handleMouseEnter = () => setIsAutoPlaying(false);
|
const handleMouseEnter = () => setIsAutoPlaying(false);
|
||||||
const handleMouseLeave = () => setIsAutoPlaying(true);
|
const handleMouseLeave = () => setIsAutoPlaying(true);
|
||||||
|
|
||||||
|
if (isLoading || slides.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="hero-section"
|
className="hero-section"
|
||||||
@@ -150,9 +135,9 @@ export default function HeroSection() {
|
|||||||
{slides.map((slide, index) => (
|
{slides.map((slide, index) => (
|
||||||
<div
|
<div
|
||||||
key={slide.id}
|
key={slide.id}
|
||||||
className={`hero-slide ${index === currentSlide ? 'active' : ''}`}
|
className={`hero-slide ${index === currentSlide ? "active" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url('${slide.backgroundImage}')`
|
backgroundImage: `url('${slide.backgroundImage}')`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="hero-overlay" />
|
<div className="hero-overlay" />
|
||||||
@@ -162,13 +147,10 @@ export default function HeroSection() {
|
|||||||
{/* Hero Content */}
|
{/* Hero Content */}
|
||||||
<div className="hero-content">
|
<div className="hero-content">
|
||||||
<div className="hero-text-section">
|
<div className="hero-text-section">
|
||||||
{/* Title */}
|
<h1 className="hero-title" style={{ whiteSpace: "pre-line" }}>
|
||||||
<h1 className="hero-title" style={{ whiteSpace: 'pre-line' }}>
|
|
||||||
{slides[currentSlide].title}
|
{slides[currentSlide].title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Dynamic CTA Button - Enhanced with Proper Navigation */}
|
|
||||||
|
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text={slides[currentSlide].ctaText}
|
text={slides[currentSlide].ctaText}
|
||||||
onClick={() => navigateTo(slides[currentSlide].route)}
|
onClick={() => navigateTo(slides[currentSlide].route)}
|
||||||
@@ -177,10 +159,8 @@ export default function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Bottom Navigation */}
|
{/* Bottom Navigation */}
|
||||||
<div className="hero-navigation">
|
<div className="hero-navigation">
|
||||||
{/* Progress Section */}
|
|
||||||
<div className="hero-progress-container">
|
<div className="hero-progress-container">
|
||||||
{slides.map((slide, index) => (
|
{slides.map((slide, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -188,35 +168,41 @@ export default function HeroSection() {
|
|||||||
className="hero-progress-item"
|
className="hero-progress-item"
|
||||||
onClick={() => goToSlide(index)}
|
onClick={() => goToSlide(index)}
|
||||||
>
|
>
|
||||||
{/* Progress Bar */}
|
|
||||||
<div
|
<div
|
||||||
key={slide.id}
|
className={`hero-progress-segment ${
|
||||||
className="hero-progress-item"
|
index === currentSlide ? "active" : ""
|
||||||
onClick={() => navigateTo(slide.route)}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Progress Bar */}
|
<div
|
||||||
<div className={`hero-progress-segment ${index === currentSlide ? 'active' : ''}`}>
|
className="hero-progress-fill"
|
||||||
<div
|
style={{
|
||||||
className="hero-progress-fill"
|
width:
|
||||||
style={{ width: index === currentSlide ? `${progressValues[index]}%` : '0%' }}
|
index === currentSlide
|
||||||
/>
|
? `${progressValues[index] ?? 0}%`
|
||||||
</div>
|
: "0%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Number */}
|
<div
|
||||||
<div className={`hero-progress-number ${index === currentSlide ? 'active' : ''}`}>
|
className={`hero-progress-number ${
|
||||||
{String(index + 1).padStart(2, '0')}
|
index === currentSlide ? "active" : ""
|
||||||
</div>
|
}`}
|
||||||
|
>
|
||||||
|
{String(index + 1).padStart(2, "0")}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Text */}
|
<div
|
||||||
<div className={`hero-progress-text ${index === currentSlide ? 'active' : ''}`}>
|
className={`hero-progress-text ${
|
||||||
{slide.shortTitle}
|
index === currentSlide ? "active" : ""
|
||||||
</div>
|
}`}
|
||||||
|
>
|
||||||
|
{slide.shortTitle}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Arrows */}
|
|
||||||
<div className="hero-controls">
|
<div className="hero-controls">
|
||||||
<button
|
<button
|
||||||
className="hero-nav-button"
|
className="hero-nav-button"
|
||||||
@@ -225,6 +211,7 @@ export default function HeroSection() {
|
|||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="hero-nav-button"
|
className="hero-nav-button"
|
||||||
onClick={nextSlide}
|
onClick={nextSlide}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import privateSpaceImage from '../assets/private-space.jpg';
|
|||||||
import executiveBoardroomImage from '../assets/exe-boardroom.jpg';
|
import executiveBoardroomImage from '../assets/exe-boardroom.jpg';
|
||||||
import morningReflectionImage from '../assets/morning.jpg';
|
import morningReflectionImage from '../assets/morning.jpg';
|
||||||
import campusArialViewImage from '../assets/campus-arial-view.jpg';
|
import campusArialViewImage from '../assets/campus-arial-view.jpg';
|
||||||
|
import { VirtualTour360 } from './VirtualTour360';
|
||||||
|
|
||||||
const facilityFeatures = [
|
const facilityFeatures = [
|
||||||
{
|
{
|
||||||
@@ -345,6 +346,7 @@ export function LearningFacilityNew() {
|
|||||||
const maxSlide = Math.max(0, facilityFeatures.length - cardsPerView);
|
const maxSlide = Math.max(0, facilityFeatures.length - cardsPerView);
|
||||||
const maxTourSlide = Math.max(0, virtualTourStops.length - tourCardsPerView);
|
const maxTourSlide = Math.max(0, virtualTourStops.length - tourCardsPerView);
|
||||||
|
|
||||||
|
|
||||||
// Handle window resize
|
// Handle window resize
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -419,11 +421,6 @@ export function LearningFacilityNew() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [heroBackgroundImages.length]);
|
}, [heroBackgroundImages.length]);
|
||||||
|
|
||||||
const handleStartTour = () => {
|
|
||||||
setIsVirtualTourActive(true);
|
|
||||||
setCurrentTourStop(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextStop = () => {
|
const handleNextStop = () => {
|
||||||
if (currentTourStop < virtualTourStops.length - 1) {
|
if (currentTourStop < virtualTourStops.length - 1) {
|
||||||
setCurrentTourStop(currentTourStop + 1);
|
setCurrentTourStop(currentTourStop + 1);
|
||||||
@@ -451,6 +448,11 @@ export function LearningFacilityNew() {
|
|||||||
setSelectedFacility(null);
|
setSelectedFacility(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isVirtualTourOpen, setIsVirtualTourOpen] = useState(false);
|
||||||
|
const handleStartTour = () => {
|
||||||
|
setIsVirtualTourOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: '#F7F7FD' }}>
|
<div className="min-h-screen" style={{ backgroundColor: '#F7F7FD' }}>
|
||||||
|
|
||||||
@@ -1766,6 +1768,16 @@ export function LearningFacilityNew() {
|
|||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Virtual Tour Modal */}
|
||||||
|
<VirtualTour360
|
||||||
|
isOpen={isVirtualTourOpen}
|
||||||
|
onClose={() => setIsVirtualTourOpen(false)}
|
||||||
|
onBookNow={() => {
|
||||||
|
// Optional: auto-open booking modal after tour
|
||||||
|
handleBookNow();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* CTA Section - Using standardized home page CTA */}
|
{/* CTA Section - Using standardized home page CTA */}
|
||||||
<CTABannerSection />
|
<CTABannerSection />
|
||||||
|
|
||||||
|
|||||||
175
src/components/Loader.tsx
Normal file
175
src/components/Loader.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface LoaderProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loader: React.FC<LoaderProps> = ({ className = "" }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
.pl {
|
||||||
|
width: 8em;
|
||||||
|
height: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl circle {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl__ring2 {
|
||||||
|
animation: ring2_ 4s 0.04s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl__ring4 {
|
||||||
|
animation: ring4_ 4s 0.12s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl__ring6 {
|
||||||
|
animation: ring6_ 4s 0.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring2_ {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: -329.207488554;
|
||||||
|
transform: rotate(-0.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
23% {
|
||||||
|
stroke-dashoffset: -82.46680575;
|
||||||
|
transform: rotate(1turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
46%, 50% {
|
||||||
|
stroke-dashoffset: -329.207488554;
|
||||||
|
transform: rotate(2.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
73% {
|
||||||
|
stroke-dashoffset: -82.46680575;
|
||||||
|
transform: rotate(3.5turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
96%, to {
|
||||||
|
stroke-dashoffset: -329.207488554;
|
||||||
|
transform: rotate(4.75turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring4_ {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: -253.9600625988;
|
||||||
|
transform: rotate(-0.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
23% {
|
||||||
|
stroke-dashoffset: -63.61725015;
|
||||||
|
transform: rotate(1turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
46%, 50% {
|
||||||
|
stroke-dashoffset: -253.9600625988;
|
||||||
|
transform: rotate(2.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
73% {
|
||||||
|
stroke-dashoffset: -63.61725015;
|
||||||
|
transform: rotate(3.5turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
96%, to {
|
||||||
|
stroke-dashoffset: -253.9600625988;
|
||||||
|
transform: rotate(4.75turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ring6_ {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: -203.795111962;
|
||||||
|
transform: rotate(-0.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
23% {
|
||||||
|
stroke-dashoffset: -51.05087975;
|
||||||
|
transform: rotate(1turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
46%, 50% {
|
||||||
|
stroke-dashoffset: -203.795111962;
|
||||||
|
transform: rotate(2.25turn);
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
}
|
||||||
|
73% {
|
||||||
|
stroke-dashoffset: -51.05087975;
|
||||||
|
transform: rotate(3.5turn);
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
}
|
||||||
|
96%, to {
|
||||||
|
stroke-dashoffset: -203.795111962;
|
||||||
|
transform: rotate(4.75turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<div className={`loader-center ${className}`}>
|
||||||
|
<svg
|
||||||
|
className="pl"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
{/* Ring 2 */}
|
||||||
|
<circle
|
||||||
|
className="pl__ring2"
|
||||||
|
cx="64"
|
||||||
|
cy="64"
|
||||||
|
r="52.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="hsl(240,92%,19%)"
|
||||||
|
strokeWidth="12"
|
||||||
|
transform="rotate(-90,64,64)"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="329.9 329.9"
|
||||||
|
strokeDashoffset="-329.3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Ring 4 */}
|
||||||
|
<circle
|
||||||
|
className="pl__ring4"
|
||||||
|
cx="64"
|
||||||
|
cy="64"
|
||||||
|
r="37.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="hsl(13,90%,55%)"
|
||||||
|
strokeWidth="9"
|
||||||
|
transform="rotate(-90,64,64)"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="254.5 254.5"
|
||||||
|
strokeDashoffset="-254"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Ring 6 */}
|
||||||
|
<circle
|
||||||
|
className="pl__ring6"
|
||||||
|
cx="64"
|
||||||
|
cy="64"
|
||||||
|
r="22.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="hsl(47,99%,49%)"
|
||||||
|
strokeWidth="9"
|
||||||
|
transform="rotate(-90,64,64)"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="204.2 204.2"
|
||||||
|
strokeDashoffset="-203.9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
Settings,
|
Settings,
|
||||||
User,
|
User,
|
||||||
Globe,
|
Globe,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Building,
|
Building,
|
||||||
@@ -14,58 +14,36 @@ import {
|
|||||||
import { BrandedTag } from "./about/BrandedTag";
|
import { BrandedTag } from "./about/BrandedTag";
|
||||||
import { StandardCTAButton } from "./StandardCTAButton";
|
import { StandardCTAButton } from "./StandardCTAButton";
|
||||||
import { navigateTo } from "./Router";
|
import { navigateTo } from "./Router";
|
||||||
|
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||||
|
|
||||||
// Services data
|
interface HighlightCard {
|
||||||
const recognitionItems = [
|
card_title: string;
|
||||||
{
|
icon_url: string;
|
||||||
id: 1,
|
accessible_label: string;
|
||||||
title: "Leadership Pipeline Development",
|
body_text: string;
|
||||||
description: "Build a strong foundation for sustainable leadership succession. Identify, assess, and develop high-potential talent through structured pathways that ensure organizational continuity and growth.",
|
display_order: number;
|
||||||
icon: <TrendingUp size={28} />,
|
}
|
||||||
route: '/services/leadership-pipeline-development'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Leadership Development",
|
|
||||||
description: "Comprehensive programs designed to cultivate strategic thinking and emotional intelligence. Develop capabilities that drive organizational success through authentic leadership practices.",
|
|
||||||
icon: <Users size={28} />,
|
|
||||||
route: '/services/leadership-development'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Management Development",
|
|
||||||
description: "Essential skills training for first-time and experienced managers seeking growth. Focus on communication, delegation, and performance management excellence.",
|
|
||||||
icon: <Settings size={28} />,
|
|
||||||
route: '/services/management-development'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Culture & Competence Consulting",
|
|
||||||
description: "Transform organizational culture and build inclusive practices that enhance team collaboration. Navigate cultural differences with confidence and create environments where everyone thrives.",
|
|
||||||
icon: <Globe size={28} />,
|
|
||||||
route: '/services/culture-competence'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Coaching & Mentoring",
|
|
||||||
description: "One-on-one personalized development for senior leaders and high-potential talent. Strategic guidance for complex leadership challenges, career advancement, and personal growth.",
|
|
||||||
icon: <MessageSquare size={28} />,
|
|
||||||
route: '/services/executive-coaching'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Leadership Campus (Facility)",
|
|
||||||
description: "Experience learning in a world-class environment designed for transformation. Our state-of-the-art facility provides the perfect setting for immersive leadership development programs.",
|
|
||||||
icon: <Building size={28} />,
|
|
||||||
route: '/services/learning-facility'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export function ServicesSection() {
|
interface ServicesSectionProps {
|
||||||
|
highlightCards?: HighlightCard[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServicesSection({ highlightCards = [], isLoading = false }: ServicesSectionProps) {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
const sectionRef = useRef<HTMLDivElement>(null);
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Create service items from highlightCards data
|
||||||
|
const serviceItems = highlightCards.map((card, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
title: card.card_title,
|
||||||
|
description: card.body_text,
|
||||||
|
iconUrl: card.icon_url,
|
||||||
|
accessibleLabel: card.accessible_label,
|
||||||
|
route: '/services' // You might want to map to specific routes based on title
|
||||||
|
}));
|
||||||
|
|
||||||
// Add card refs helper
|
// Add card refs helper
|
||||||
const addCardRef = (el: HTMLDivElement | null, index: number) => {
|
const addCardRef = (el: HTMLDivElement | null, index: number) => {
|
||||||
cardRefs.current[index] = el;
|
cardRefs.current[index] = el;
|
||||||
@@ -102,11 +80,33 @@ export function ServicesSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading skeleton if isLoading is true
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<section ref={sectionRef} className="py-16 lg:py-20" style={{ backgroundColor: '#fff' }}>
|
||||||
|
<div className="section-margin-x">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||||||
|
<div className="h-12 bg-gray-200 rounded w-2/3 mb-4"></div>
|
||||||
|
<div className="h-24 bg-gray-200 rounded w-full mb-8"></div>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div key={i} className="h-48 bg-gray-200 rounded"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
className="py-16 lg:py-20"
|
className="py-16 lg:py-20"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
fontFamily: 'var(--font-family-brand)'
|
fontFamily: 'var(--font-family-brand)'
|
||||||
}}
|
}}
|
||||||
@@ -119,11 +119,11 @@ export function ServicesSection() {
|
|||||||
{/* Left Side - Sticky Content */}
|
{/* Left Side - Sticky Content */}
|
||||||
<div className="col-span-5 sticky top-24 self-start">
|
<div className="col-span-5 sticky top-24 self-start">
|
||||||
<div className="recognition-header pr-8">
|
<div className="recognition-header pr-8">
|
||||||
<BrandedTag
|
<BrandedTag
|
||||||
text="Our Services"
|
text="Our Services"
|
||||||
/>
|
/>
|
||||||
<h2
|
<h2
|
||||||
id="recognition-section-heading"
|
id="recognition-section-heading"
|
||||||
className="text-h2 mb-6"
|
className="text-h2 mb-6"
|
||||||
>
|
>
|
||||||
Shaping Leaders, Cultures, and Institutions
|
Shaping Leaders, Cultures, and Institutions
|
||||||
@@ -133,7 +133,7 @@ export function ServicesSection() {
|
|||||||
</p>
|
</p>
|
||||||
{/* CTA Button - Left aligned */}
|
{/* CTA Button - Left aligned */}
|
||||||
<div className="primary-cta-container-left cta-left-locked">
|
<div className="primary-cta-container-left cta-left-locked">
|
||||||
<StandardCTAButton
|
<StandardCTAButton
|
||||||
text="Services Page"
|
text="Services Page"
|
||||||
onClick={() => navigateTo('/services')}
|
onClick={() => navigateTo('/services')}
|
||||||
ariaLabel="Explore our services"
|
ariaLabel="Explore our services"
|
||||||
@@ -144,12 +144,12 @@ export function ServicesSection() {
|
|||||||
|
|
||||||
{/* Right Side - Scrolling Cards */}
|
{/* Right Side - Scrolling Cards */}
|
||||||
<div className="col-span-7">
|
<div className="col-span-7">
|
||||||
<div
|
<div
|
||||||
className="recognition-cards space-y-6"
|
className="recognition-cards space-y-6"
|
||||||
role="list"
|
role="list"
|
||||||
aria-label="Leadership development services"
|
aria-label="Leadership development services"
|
||||||
>
|
>
|
||||||
{recognitionItems.map((item, index) => (
|
{serviceItems.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
ref={(el) => addCardRef(el, index)}
|
ref={(el) => addCardRef(el, index)}
|
||||||
@@ -159,41 +159,50 @@ export function ServicesSection() {
|
|||||||
aria-describedby={`recognition-desc-${item.id}`}
|
aria-describedby={`recognition-desc-${item.id}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => handleKeyDown(e, index)}
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
||||||
style={{
|
style={{
|
||||||
transitionDelay: `${(index + 1) * 150}ms`,
|
transitionDelay: `${(index + 1) * 150}ms`,
|
||||||
opacity: isVisible ? 1 : 0
|
opacity: isVisible ? 1 : 0
|
||||||
}}
|
}}
|
||||||
onClick={() => navigateTo(item.route)}
|
onClick={() => navigateTo(item.route)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="p-8 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white"
|
className="p-8 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white"
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-border)',
|
borderColor: 'var(--color-border)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
fontFamily: 'var(--font-family-brand)'
|
fontFamily: 'var(--font-family-brand)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start mb-6">
|
<div className="flex items-start mb-6">
|
||||||
<div
|
<div
|
||||||
className="w-14 h-14 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
className="w-14 h-14 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-brand-primary)',
|
backgroundColor: 'var(--color-brand-primary)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
color: 'white'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{/* Image icon from icon_url */}
|
||||||
|
<img
|
||||||
|
src={item.iconUrl}
|
||||||
|
alt={item.accessibleLabel || item.title}
|
||||||
|
className="w-8 h-8 object-contain filter brightness-0 invert" // Makes white icon on colored background
|
||||||
|
onError={(e) => {
|
||||||
|
// Fallback if image fails to load
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
// You could add a fallback icon here if needed
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="recognition-card-content">
|
<div className="recognition-card-content">
|
||||||
<h3
|
<h3
|
||||||
id={`recognition-title-${item.id}`}
|
id={`recognition-title-${item.id}`}
|
||||||
className="text-h4 mb-4"
|
className="text-h4 mb-4"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
id={`recognition-desc-${item.id}`}
|
id={`recognition-desc-${item.id}`}
|
||||||
className="text-small text-muted leading-relaxed"
|
className="text-small text-muted leading-relaxed"
|
||||||
>
|
>
|
||||||
@@ -211,11 +220,11 @@ export function ServicesSection() {
|
|||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<BrandedTag
|
<BrandedTag
|
||||||
text="Our Services"
|
text="Our Services"
|
||||||
/>
|
/>
|
||||||
<h2
|
<h2
|
||||||
id="recognition-section-heading-mobile"
|
id="recognition-section-heading-mobile"
|
||||||
className="text-h2 mb-6"
|
className="text-h2 mb-6"
|
||||||
>
|
>
|
||||||
Shaping Leaders, Cultures, and Institutions
|
Shaping Leaders, Cultures, and Institutions
|
||||||
@@ -225,7 +234,7 @@ export function ServicesSection() {
|
|||||||
</p>
|
</p>
|
||||||
{/* CTA Button - Left aligned for mobile */}
|
{/* CTA Button - Left aligned for mobile */}
|
||||||
<div className="primary-cta-container-left cta-left-locked">
|
<div className="primary-cta-container-left cta-left-locked">
|
||||||
<StandardCTAButton
|
<StandardCTAButton
|
||||||
text="Services Page"
|
text="Services Page"
|
||||||
onClick={() => navigateTo('/services')}
|
onClick={() => navigateTo('/services')}
|
||||||
ariaLabel="Explore our services"
|
ariaLabel="Explore our services"
|
||||||
@@ -235,16 +244,16 @@ export function ServicesSection() {
|
|||||||
|
|
||||||
{/* Mobile Horizontal Scrollable Cards */}
|
{/* Mobile Horizontal Scrollable Cards */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className="flex gap-6 overflow-x-auto scrollbar-hide pb-4"
|
className="flex gap-6 overflow-x-auto scrollbar-hide pb-4"
|
||||||
style={{
|
style={{
|
||||||
scrollSnapType: 'x mandatory',
|
scrollSnapType: 'x mandatory',
|
||||||
WebkitOverflowScrolling: 'touch'
|
WebkitOverflowScrolling: 'touch'
|
||||||
}}
|
}}
|
||||||
role="list"
|
role="list"
|
||||||
aria-label="Leadership development services"
|
aria-label="Leadership development services"
|
||||||
>
|
>
|
||||||
{recognitionItems.map((item, index) => (
|
{serviceItems.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`recognition-card-mobile group focus-ring flex-shrink-0 ${isVisible ? 'animate-in' : ''}`}
|
className={`recognition-card-mobile group focus-ring flex-shrink-0 ${isVisible ? 'animate-in' : ''}`}
|
||||||
@@ -253,42 +262,45 @@ export function ServicesSection() {
|
|||||||
aria-describedby={`recognition-desc-mobile-${item.id}`}
|
aria-describedby={`recognition-desc-mobile-${item.id}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => handleKeyDown(e, index)}
|
onKeyDown={(e) => handleKeyDown(e, index)}
|
||||||
style={{
|
style={{
|
||||||
scrollSnapAlign: 'start',
|
scrollSnapAlign: 'start',
|
||||||
width: '320px',
|
width: '320px',
|
||||||
transitionDelay: `${(index + 1) * 150}ms`,
|
transitionDelay: `${(index + 1) * 150}ms`,
|
||||||
opacity: isVisible ? 1 : 0
|
opacity: isVisible ? 1 : 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="p-6 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white h-full"
|
className="p-6 transition-all duration-300 hover:shadow-xl hover:-translate-y-1 border bg-white h-full"
|
||||||
style={{
|
style={{
|
||||||
borderColor: 'var(--color-border)',
|
borderColor: 'var(--color-border)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
fontFamily: 'var(--font-family-brand)'
|
fontFamily: 'var(--font-family-brand)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start mb-6">
|
<div className="flex items-start mb-6">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
className="w-12 h-12 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-brand-primary)',
|
backgroundColor: 'var(--color-brand-primary)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
color: 'white'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
<ImageWithFallback
|
||||||
|
src={item.iconUrl}
|
||||||
|
alt={item.accessibleLabel || item.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="recognition-card-content">
|
<div className="recognition-card-content">
|
||||||
<h3
|
<h3
|
||||||
id={`recognition-title-mobile-${item.id}`}
|
id={`recognition-title-mobile-${item.id}`}
|
||||||
className="text-h4 mb-4"
|
className="text-h4 mb-4"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
id={`recognition-desc-mobile-${item.id}`}
|
id={`recognition-desc-mobile-${item.id}`}
|
||||||
className="text-small text-muted leading-relaxed"
|
className="text-small text-muted leading-relaxed"
|
||||||
>
|
>
|
||||||
@@ -301,8 +313,6 @@ export function ServicesSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Button } from "./ui/button";
|
|
||||||
import { useAnimatedCounter } from "./hooks/useAnimatedCounter";
|
|
||||||
import { ArrowUpRight } from "lucide-react";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { BrandedTag } from "./about/BrandedTag";
|
import { BrandedTag } from "./about/BrandedTag";
|
||||||
import { PrimaryCTAButton } from "./PrimaryCTAButton";
|
import { PrimaryCTAButton } from "./PrimaryCTAButton";
|
||||||
|
import { useAnimatedCounter } from "../redux/hooks/useAnimatedCounter";
|
||||||
|
|
||||||
|
interface Stat {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
suffix: string;
|
||||||
|
label: string;
|
||||||
|
display_order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatsSectionProps {
|
||||||
|
stats: Stat[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface StatItemProps {
|
interface StatItemProps {
|
||||||
end: number;
|
end: number;
|
||||||
@@ -17,123 +27,104 @@ function StatItem({ end, suffix, label, duration = 2000 }: StatItemProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-0">
|
<div className="mb-0">
|
||||||
{/* Top line */}
|
<div
|
||||||
<div
|
|
||||||
className="w-full h-[1px] mb-4"
|
className="w-full h-[1px] mb-4"
|
||||||
style={{ backgroundColor: 'var(--color-brand-gray-muted)' }}
|
style={{ backgroundColor: "var(--color-brand-gray-muted)" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Large number */}
|
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="stats-number mb-3 leading-none"
|
className="stats-number mb-3 leading-none"
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--color-brand-primary)',
|
color: "var(--color-brand-primary)",
|
||||||
fontFamily: 'var(--font-family-base)'
|
fontFamily: "var(--font-family-base)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Yellow square and label */}
|
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 mr-3 flex-shrink-0"
|
className="w-2 h-2 mr-3 flex-shrink-0"
|
||||||
style={{ backgroundColor: 'var(--color-brand-accent)' }}
|
style={{ backgroundColor: "var(--color-brand-accent)" }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-eyebrow"
|
className="text-eyebrow"
|
||||||
style={{ color: 'var(--color-brand-black)' }}
|
style={{ color: "var(--color-brand-black)" }}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom line */}
|
<div
|
||||||
<div
|
|
||||||
className="w-full h-[1px]"
|
className="w-full h-[1px]"
|
||||||
style={{ backgroundColor: 'var(--color-brand-gray-muted)' }}
|
style={{ backgroundColor: "var(--color-brand-gray-muted)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatsSection() {
|
export function StatsSection({ stats = [], isLoading }: StatsSectionProps) {
|
||||||
|
|
||||||
|
const sortedStats = [...stats].sort(
|
||||||
|
(a, b) => a.display_order - b.display_order
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="py-20"
|
className="py-20"
|
||||||
style={{ backgroundColor: 'var(--color-brand-bg-light)' }}
|
style={{ backgroundColor: "var(--color-brand-bg-light)" }}
|
||||||
>
|
>
|
||||||
<div className="section-margin-x">
|
<div className="section-margin-x">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="mb-12 lg:mb-16 md:mb-12 sm:mb-8">
|
<div className="mb-12 lg:mb-16 md:mb-12 sm:mb-8">
|
||||||
{/* Branded Tag */}
|
|
||||||
<BrandedTag
|
|
||||||
text="Serving Leaders Across Industries"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Main Heading */}
|
<BrandedTag text="Serving Leaders Across Industries" />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-start">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-12 items-start">
|
||||||
<div className="lg:col-span-6 md:col-span-8 sm:col-span-12">
|
<div className="lg:col-span-6 md:col-span-8 sm:col-span-12">
|
||||||
<h2 className="text-h2 mb-8">
|
<h2 className="text-h2 mb-8">
|
||||||
Your Partner in Leadership, Culture, and Capability Building
|
Your Partner in Leadership, Culture, and Capability Building
|
||||||
</h2>
|
</h2>
|
||||||
{/* CTA Button */}
|
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="About Us"
|
text="About Us"
|
||||||
onClick={() => console.log('About us clicked')}
|
onClick={() => console.log("About us clicked")}
|
||||||
ariaLabel="Learn more about KLC"
|
ariaLabel="Learn more about KLC"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Statistics */}
|
{/* Desktop */}
|
||||||
<div className="hidden lg:block lg:col-start-9 lg:col-end-13">
|
<div className="hidden lg:block lg:col-start-9 lg:col-end-13">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<StatItem
|
{sortedStats.map((stat) => (
|
||||||
end={27000}
|
<StatItem
|
||||||
suffix="+"
|
key={stat.id}
|
||||||
label="LEADERS DEVELOPED"
|
end={stat.number}
|
||||||
duration={2500}
|
suffix={stat.suffix}
|
||||||
/>
|
label={stat.label}
|
||||||
<StatItem
|
/>
|
||||||
end={150}
|
))}
|
||||||
suffix="+"
|
|
||||||
label="CORPORATES"
|
|
||||||
duration={2000}
|
|
||||||
/>
|
|
||||||
<StatItem
|
|
||||||
end={5000}
|
|
||||||
suffix="+"
|
|
||||||
label="ROOM NIGHTS UTILISED"
|
|
||||||
duration={1800}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Statistics - Show below content on mobile/tablet */}
|
{/* Mobile */}
|
||||||
<div className="block lg:hidden mt-12">
|
<div className="block lg:hidden mt-12">
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 sm:gap-8">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 sm:gap-8">
|
||||||
<StatItem
|
{sortedStats.map((stat) => (
|
||||||
end={27000}
|
<StatItem
|
||||||
suffix="+"
|
key={stat.id}
|
||||||
label="LEADERS DEVELOPED"
|
end={stat.number}
|
||||||
duration={2500}
|
suffix={stat.suffix}
|
||||||
/>
|
label={stat.label}
|
||||||
<StatItem
|
/>
|
||||||
end={150}
|
))}
|
||||||
suffix="+"
|
|
||||||
label="CORPORATE CLIENTS"
|
|
||||||
duration={2000}
|
|
||||||
/>
|
|
||||||
<StatItem
|
|
||||||
end={20}
|
|
||||||
suffix="+"
|
|
||||||
label="COUNTRIES SERVED"
|
|
||||||
duration={1800}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
355
src/components/VirtualTour360.tsx
Normal file
355
src/components/VirtualTour360.tsx
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
import panormaImage from '../assets/panoramas/cedar_bridge_sunset.jpg';
|
||||||
|
import panormaImage2 from '../assets/panoramas/cayley_interior.jpg';
|
||||||
|
|
||||||
|
interface VirtualTour360Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onBookNow?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Your 360° panorama images
|
||||||
|
const tourScenes = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
title: "Cedar Bridge Sunset",
|
||||||
|
capacity: "Outdoor Learning Space",
|
||||||
|
description: "A serene outdoor setting with stunning sunset views, perfect for meditation and reflection sessions at Kautilya Leadership Centre.",
|
||||||
|
features: ["Natural environment", "Sunset views", "Peaceful atmosphere", "Meditation area", "Reflection space"],
|
||||||
|
panoramaUrl: panormaImage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Training Amphitheater",
|
||||||
|
capacity: "80-120 attendees",
|
||||||
|
description: "State-of-the-art amphitheater with tiered seating, advanced acoustics, and immersive presentation technology.",
|
||||||
|
features: ["Tiered seating", "4K projection", "Live streaming", "Acoustic panels"],
|
||||||
|
panoramaUrl: panormaImage2
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function VirtualTour360({ isOpen, onClose, onBookNow }: VirtualTour360Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [currentSceneIndex, setCurrentSceneIndex] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// Three.js refs
|
||||||
|
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||||
|
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||||
|
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||||
|
const controlsRef = useRef<OrbitControls | null>(null);
|
||||||
|
const currentMeshRef = useRef<THREE.Mesh | null>(null);
|
||||||
|
const currentTextureRef = useRef<THREE.Texture | null>(null);
|
||||||
|
const animationRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Initialize Three.js scene
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !containerRef.current) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
|
||||||
|
// Setup scene
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x050510);
|
||||||
|
sceneRef.current = scene;
|
||||||
|
|
||||||
|
// Setup camera
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
|
camera.position.set(0, 0, 0.1);
|
||||||
|
cameraRef.current = camera;
|
||||||
|
|
||||||
|
// Setup renderer
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
renderer.domElement.style.touchAction = "none"; // Important for touch events
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
rendererRef.current = renderer;
|
||||||
|
|
||||||
|
// Setup controls - ONLY ROTATION (no built-in zoom)
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableZoom = false; // Disable built-in zoom, we'll handle custom zoom
|
||||||
|
controls.enablePan = false;
|
||||||
|
controls.enableRotate = true;
|
||||||
|
controls.rotateSpeed = 1.2;
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.05;
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
controlsRef.current = controls;
|
||||||
|
|
||||||
|
// 🔥 CUSTOM ZOOM - Mouse wheel (desktop)
|
||||||
|
const handleWheel = (event: WheelEvent) => {
|
||||||
|
if (!cameraRef.current) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const zoomSpeed = 0.05;
|
||||||
|
cameraRef.current.fov += event.deltaY * zoomSpeed;
|
||||||
|
|
||||||
|
// Limit zoom range
|
||||||
|
cameraRef.current.fov = Math.max(40, Math.min(90, cameraRef.current.fov));
|
||||||
|
cameraRef.current.updateProjectionMatrix();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
|
||||||
|
// 🔥 CUSTOM ZOOM - Mobile pinch gesture
|
||||||
|
let lastDistance = 0;
|
||||||
|
|
||||||
|
const getDistance = (touches: TouchList) => {
|
||||||
|
if (touches.length < 2) return 0;
|
||||||
|
const dx = touches[0].clientX - touches[1].clientX;
|
||||||
|
const dy = touches[0].clientY - touches[1].clientY;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (!cameraRef.current) return;
|
||||||
|
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const distance = getDistance(e.touches);
|
||||||
|
|
||||||
|
if (lastDistance > 0) {
|
||||||
|
const delta = distance - lastDistance;
|
||||||
|
cameraRef.current.fov -= delta * 0.05;
|
||||||
|
cameraRef.current.fov = Math.max(40, Math.min(90, cameraRef.current.fov));
|
||||||
|
cameraRef.current.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDistance = distance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
lastDistance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||||
|
renderer.domElement.addEventListener("touchend", handleTouchEnd);
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const animate = () => {
|
||||||
|
animationRef.current = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
if (controlsRef.current) controlsRef.current.update();
|
||||||
|
if (rendererRef.current && sceneRef.current && cameraRef.current) {
|
||||||
|
rendererRef.current.render(sceneRef.current, cameraRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Load first panorama
|
||||||
|
loadPanorama(tourScenes[0].panoramaUrl);
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
const handleResize = () => {
|
||||||
|
if (cameraRef.current && rendererRef.current) {
|
||||||
|
cameraRef.current.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
cameraRef.current.updateProjectionMatrix();
|
||||||
|
rendererRef.current.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (animationRef.current) cancelAnimationFrame(animationRef.current);
|
||||||
|
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
renderer.domElement.removeEventListener("wheel", handleWheel);
|
||||||
|
renderer.domElement.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
renderer.domElement.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
|
||||||
|
if (controlsRef.current) controlsRef.current.dispose();
|
||||||
|
if (rendererRef.current) rendererRef.current.dispose();
|
||||||
|
if (currentTextureRef.current) currentTextureRef.current.dispose();
|
||||||
|
|
||||||
|
if (container && renderer.domElement && container.contains(renderer.domElement)) {
|
||||||
|
container.removeChild(renderer.domElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Load panorama image
|
||||||
|
const loadPanorama = (imageUrl: string) => {
|
||||||
|
if (!sceneRef.current) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const textureLoader = new THREE.TextureLoader();
|
||||||
|
textureLoader.load(
|
||||||
|
imageUrl,
|
||||||
|
(texture) => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping;
|
||||||
|
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||||
|
texture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
|
// Remove old mesh
|
||||||
|
if (currentMeshRef.current) sceneRef.current?.remove(currentMeshRef.current);
|
||||||
|
if (currentTextureRef.current) currentTextureRef.current.dispose();
|
||||||
|
|
||||||
|
// Create new sphere with texture
|
||||||
|
const geometry = new THREE.SphereGeometry(500, 64, 64);
|
||||||
|
const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.BackSide });
|
||||||
|
const sphere = new THREE.Mesh(geometry, material);
|
||||||
|
sceneRef.current?.add(sphere);
|
||||||
|
|
||||||
|
currentMeshRef.current = sphere;
|
||||||
|
currentTextureRef.current = texture;
|
||||||
|
|
||||||
|
// Reset camera FOV when loading new scene
|
||||||
|
if (cameraRef.current) {
|
||||||
|
cameraRef.current.fov = 75;
|
||||||
|
cameraRef.current.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(error) => {
|
||||||
|
console.error('Error loading panorama:', error);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to next scene
|
||||||
|
const nextScene = () => {
|
||||||
|
const nextIndex = (currentSceneIndex + 1) % tourScenes.length;
|
||||||
|
setCurrentSceneIndex(nextIndex);
|
||||||
|
loadPanorama(tourScenes[nextIndex].panoramaUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to previous scene
|
||||||
|
const prevScene = () => {
|
||||||
|
const prevIndex = (currentSceneIndex - 1 + tourScenes.length) % tourScenes.length;
|
||||||
|
setCurrentSceneIndex(prevIndex);
|
||||||
|
loadPanorama(tourScenes[prevIndex].panoramaUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Jump to specific scene
|
||||||
|
const goToScene = (index: number) => {
|
||||||
|
setCurrentSceneIndex(index);
|
||||||
|
loadPanorama(tourScenes[index].panoramaUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentScene = tourScenes[currentSceneIndex];
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[100] bg-black">
|
||||||
|
{/* Canvas container for Three.js */}
|
||||||
|
<div ref={containerRef} className="absolute inset-0" />
|
||||||
|
|
||||||
|
{/* UI Overlay */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
{/* Top Bar */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 bg-gradient-to-r from-[#04045B]/95 to-[#04045B]/80 backdrop-blur-md border-b border-yellow-400/30 pointer-events-auto z-30">
|
||||||
|
<div className="flex justify-between items-center px-4 md:px-6 py-3 md:py-4">
|
||||||
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
|
<div className="w-8 h-8 md:w-10 md:h-10 bg-yellow-400 rounded-xl flex items-center justify-center font-bold text-[#04045B] text-base md:text-xl">
|
||||||
|
KL
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<h1 className="text-white font-semibold text-sm md:text-lg">Kautilya Leadership Centre</h1>
|
||||||
|
<p className="text-white/70 text-xs">360° Immersive Experience</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
|
<button
|
||||||
|
onClick={prevScene}
|
||||||
|
className="w-8 h-8 md:w-9 md:h-9 rounded-full bg-white/20 hover:bg-yellow-400 hover:text-[#04045B] transition-all flex items-center justify-center text-white text-sm md:text-base touch-manipulation"
|
||||||
|
aria-label="Previous scene"
|
||||||
|
>
|
||||||
|
◀
|
||||||
|
</button>
|
||||||
|
<div className="bg-black/50 backdrop-blur-sm px-3 py-1.5 md:px-4 rounded-full text-xs md:text-sm font-medium">
|
||||||
|
{currentScene.title}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={nextScene}
|
||||||
|
className="w-8 h-8 md:w-9 md:h-9 rounded-full bg-white/20 hover:bg-yellow-400 hover:text-[#04045B] transition-all flex items-center justify-center text-white text-sm md:text-base touch-manipulation"
|
||||||
|
aria-label="Next scene"
|
||||||
|
>
|
||||||
|
▶
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-8 h-8 md:w-9 md:h-9 rounded-full bg-white/20 hover:bg-red-500 transition-all flex items-center justify-center text-white text-sm md:text-base touch-manipulation"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Panel - Bottom Left */}
|
||||||
|
<div className="absolute bottom-4 md:bottom-6 left-4 md:left-6 max-w-[280px] md:max-w-[320px] bg-[#04045B]/90 backdrop-blur-md rounded-xl md:rounded-2xl p-4 md:p-5 border border-yellow-400/40 pointer-events-auto z-25">
|
||||||
|
<h2 className="text-yellow-400 text-lg md:text-2xl font-bold mb-1">{currentScene.title}</h2>
|
||||||
|
<div className="text-white/80 text-xs md:text-sm mb-2 md:mb-3 flex items-center gap-2">
|
||||||
|
🎯 Capacity: {currentScene.capacity}
|
||||||
|
</div>
|
||||||
|
<p className="text-white/90 text-xs md:text-sm mb-3 md:mb-4 leading-relaxed">
|
||||||
|
{currentScene.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5 md:gap-2 mb-3 md:mb-4">
|
||||||
|
{currentScene.features.map((feature, idx) => (
|
||||||
|
<span key={idx} className="bg-yellow-400/20 text-yellow-400 text-[10px] md:text-xs px-2 py-1 md:px-3 rounded-full">
|
||||||
|
✓ {feature}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onBookNow?.();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="w-full bg-yellow-400 hover:bg-yellow-500 text-[#04045B] font-bold py-2 md:py-2.5 rounded-full transition-all flex items-center justify-center gap-2 text-sm md:text-base touch-manipulation"
|
||||||
|
>
|
||||||
|
📅 Book This Space →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scene Thumbnails - Bottom Right */}
|
||||||
|
<div className="absolute bottom-4 md:bottom-6 right-4 md:right-6 flex flex-col gap-2 pointer-events-auto z-25">
|
||||||
|
{tourScenes.map((scene, idx) => (
|
||||||
|
<button
|
||||||
|
key={scene.id}
|
||||||
|
onClick={() => goToScene(idx)}
|
||||||
|
className={`bg-black/70 backdrop-blur-sm rounded-full px-3 py-1.5 md:px-4 md:py-2 flex items-center gap-2 md:gap-3 hover:bg-[#04045B]/90 transition-all touch-manipulation ${
|
||||||
|
idx === currentSceneIndex ? 'border-l-4 border-yellow-400' : 'border-l-4 border-yellow-400/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="w-1.5 h-1.5 md:w-2 md:h-2 bg-yellow-400 rounded-full" />
|
||||||
|
<span className="text-white text-[10px] md:text-xs font-medium truncate max-w-[120px] md:max-w-[150px]">
|
||||||
|
{scene.title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading Overlay */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 bg-[#04045B] flex items-center justify-center z-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-12 h-12 border-4 border-yellow-400/30 border-t-yellow-400 rounded-full animate-spin mx-auto mb-4"></div>
|
||||||
|
<p className="text-white text-sm">Loading 360° Experience...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Instruction Hint */}
|
||||||
|
<div className="absolute bottom-4 md:bottom-6 left-1/2 transform -translate-x-1/2 bg-black/60 backdrop-blur-sm rounded-full px-3 py-1.5 md:px-4 md:py-2 text-white text-[10px] md:text-xs pointer-events-auto whitespace-nowrap">
|
||||||
|
🖱️ Drag to look around • 👆 Pinch/Scroll to zoom in/out
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/global.d.ts
vendored
14
src/global.d.ts
vendored
@@ -1,5 +1,15 @@
|
|||||||
// declarations.d.ts
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
// Vite ENV typing
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image modules
|
||||||
declare module "*.png" {
|
declare module "*.png" {
|
||||||
const src: string;
|
const src: string;
|
||||||
export default src;
|
export default src;
|
||||||
@@ -23,4 +33,4 @@ declare module "*.svg" {
|
|||||||
export { ReactComponent };
|
export { ReactComponent };
|
||||||
const src: string;
|
const src: string;
|
||||||
export default src;
|
export default src;
|
||||||
}
|
}
|
||||||
13
src/main.tsx
13
src/main.tsx
@@ -4,9 +4,14 @@ import "../src/styles/globals.css";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import ScrollToTop from "./components/ScrollToTop";
|
import ScrollToTop from "./components/ScrollToTop";
|
||||||
|
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "./redux/store/Store";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<BrowserRouter>
|
<Provider store={store}>
|
||||||
<ScrollToTop />
|
<BrowserRouter>
|
||||||
<App />
|
<ScrollToTop />
|
||||||
</BrowserRouter>
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
@@ -1,16 +1,45 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, HelpCircle, Mail } from 'lucide-react';
|
import { Plus, Minus, HelpCircle, Mail } from 'lucide-react';
|
||||||
import { PrimaryCTAButton } from '../components/PrimaryCTAButton';
|
import { PrimaryCTAButton } from '../components/PrimaryCTAButton';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useGetFaqsQuery, useGetFaqCategoriesQuery } from '../redux/services/faqApi';
|
||||||
|
|
||||||
interface FAQItemProps {
|
interface FAQItemProps {
|
||||||
question: string;
|
question: string;
|
||||||
answer: string;
|
answer: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
|
tags?: Array<{ tag_name: string; display_order: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FAQItem: React.FC<FAQItemProps> = ({ 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<FAQItemProps> = ({ question, answer, isOpen, onToggle, tags }) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-4 transition-all duration-300 hover:shadow-md">
|
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-4 transition-all duration-300 hover:shadow-md">
|
||||||
<button
|
<button
|
||||||
@@ -22,109 +51,136 @@ const FAQItem: React.FC<FAQItemProps> = ({ question, answer, isOpen, onToggle })
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex-shrink-0 ml-4">
|
<div className="flex-shrink-0 ml-4">
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Minus
|
<Minus
|
||||||
className="w-5 h-5 transition-transform duration-200"
|
className="w-5 h-5 transition-transform duration-200"
|
||||||
style={{ color: 'var(--color-primary)' }}
|
style={{ color: 'var(--color-primary)' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Plus
|
<Plus
|
||||||
className="w-5 h-5 transition-transform duration-200"
|
className="w-5 h-5 transition-transform duration-200"
|
||||||
style={{ color: 'var(--color-primary)' }}
|
style={{ color: 'var(--color-primary)' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`overflow-hidden transition-all duration-300 ease-out ${
|
className={`overflow-hidden transition-all duration-300 ease-out ${isOpen ? 'max-h-96 opacity-100 mt-4' : 'max-h-0 opacity-0'
|
||||||
isOpen ? 'max-h-96 opacity-100 mt-4' : 'max-h-0 opacity-0'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<p className="text-body-lg leading-relaxed" style={{ color: 'var(--color-black)' }}>
|
<p className="text-body-lg leading-relaxed mb-4" style={{ color: 'var(--color-black)' }}>
|
||||||
{answer}
|
{answer}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Display tags if available */}
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{tags.map((tag, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-small"
|
||||||
|
>
|
||||||
|
{tag.tag_name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FAQ() {
|
export function FAQ() {
|
||||||
const [openItems, setOpenItems] = useState<number[]>([]);
|
const [openItems, setOpenItems] = useState<string[]>([]);
|
||||||
const [activeCategory, setActiveCategory] = useState<string>('all');
|
const [activeCategory, setActiveCategory] = useState<string>('all');
|
||||||
|
const [filteredFaqs, setFilteredFaqs] = useState<FAQItemData[]>([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const toggleItem = (index: number) => {
|
// Fetch FAQ categories
|
||||||
setOpenItems(prev =>
|
const {
|
||||||
prev.includes(index)
|
data: categoriesResponse,
|
||||||
? prev.filter(i => i !== index)
|
isLoading: isLoadingCategories
|
||||||
: [...prev, index]
|
} = 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 = [
|
// Handle loading state
|
||||||
{ id: 'all', label: 'All Questions' },
|
if (isLoadingFaqs || isLoadingCategories) {
|
||||||
{ id: 'getting-started', label: 'Getting Started' },
|
return (
|
||||||
{ id: 'membership', label: 'Members and Pricing' },
|
<div style={{ backgroundColor: '#FFFFFF' }} className="min-h-screen flex items-center justify-center">
|
||||||
{ id: 'requests', label: 'Book Requests and Recommendations' },
|
<div className="text-center">
|
||||||
{ id: 'account', label: 'Account & Technical Issues' }
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
||||||
];
|
<p className="mt-4 text-gray-600">Loading FAQs...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const faqData = [
|
// Handle error state
|
||||||
{
|
if (isError) {
|
||||||
question: "How do I sign up for an account?",
|
return (
|
||||||
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.",
|
<div style={{ backgroundColor: '#FFFFFF' }} className="min-h-screen flex items-center justify-center">
|
||||||
category: 'getting-started'
|
<div className="text-center">
|
||||||
},
|
<HelpCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
||||||
{
|
<h2 className="text-h3 mb-2">Failed to load FAQs</h2>
|
||||||
question: "What are the membership club packages?",
|
<p className="text-gray-600 mb-4">Please try again later</p>
|
||||||
answer: "We offer subscription packages to our learners which allow you to avail of the different products/services available at a discounted rate.",
|
<PrimaryCTAButton
|
||||||
category: 'membership'
|
text="Retry"
|
||||||
},
|
onClick={() => refetch()}
|
||||||
{
|
className="cta-text-black"
|
||||||
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.",
|
</div>
|
||||||
category: 'membership'
|
</div>
|
||||||
},
|
);
|
||||||
{
|
}
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: '#FFFFFF' }} className="min-h-screen">
|
<div style={{ backgroundColor: '#FFFFFF' }} className="min-h-screen">
|
||||||
@@ -135,49 +191,63 @@ export function FAQ() {
|
|||||||
<h1 className="text-h1 mb-6" style={{ color: 'var(--color-black)' }}>
|
<h1 className="text-h1 mb-6" style={{ color: 'var(--color-black)' }}>
|
||||||
FAQs
|
FAQs
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-body-lg mb-8 leading-relaxed" style={{ color: 'var(--color-gray-muted)' }}>
|
<p className="text-body-lg mb-8 leading-relaxed" style={{ color: 'var(--color-gray-muted)' }}>
|
||||||
Everything you need to know about features, membership, and troubleshooting.
|
Everything you need to know about features, membership, and troubleshooting.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Category Filter Tags */}
|
{/* Category Filter Tags - Only show if there are categories */}
|
||||||
<div className="flex flex-wrap gap-3 mb-8">
|
{categories.length > 1 && (
|
||||||
{categories.map((category) => (
|
<div className="flex flex-wrap gap-3 mb-8">
|
||||||
<button
|
{categories.map((category) => (
|
||||||
key={category.id}
|
<button
|
||||||
onClick={() => setActiveCategory(category.id)}
|
key={category.id}
|
||||||
className={`px-4 py-2 rounded-full text-small font-medium transition-all duration-200 ${
|
onClick={() => setActiveCategory(category.id)}
|
||||||
activeCategory === category.id
|
className={`px-4 py-2 rounded-full text-small font-medium transition-all duration-200 ${activeCategory === category.id
|
||||||
? 'text-white'
|
? 'text-white'
|
||||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: activeCategory === category.id ? 'var(--color-primary)' : undefined,
|
backgroundColor: activeCategory === category.id ? 'var(--color-primary)' : undefined,
|
||||||
color: activeCategory === category.id ? 'white' : undefined
|
color: activeCategory === category.id ? 'white' : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{category.label}
|
{category.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<section className="pb-20 section-margin-x">
|
<section className="pb-20 section-margin-x">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="space-y-0">
|
{filteredFaqs.length > 0 ? (
|
||||||
{filteredFAQs.map((faq, index) => (
|
<div className="space-y-0">
|
||||||
<FAQItem
|
{filteredFaqs.map((faq) => (
|
||||||
key={`${activeCategory}-${index}`}
|
<FAQItem
|
||||||
question={faq.question}
|
key={faq.id}
|
||||||
answer={faq.answer}
|
question={faq.question}
|
||||||
isOpen={openItems.includes(index)}
|
answer={faq.answer}
|
||||||
onToggle={() => toggleItem(index)}
|
// tags={faq.faq_tags}
|
||||||
/>
|
isOpen={openItems.includes(faq.id)}
|
||||||
))}
|
onToggle={() => toggleItem(faq.id)}
|
||||||
</div>
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<HelpCircle className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-h4 mb-2">No FAQs found</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{activeCategory === 'all'
|
||||||
|
? 'No published FAQs available at the moment.'
|
||||||
|
: 'No FAQs available in this category.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -188,24 +258,24 @@ export function FAQ() {
|
|||||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--color-primary)' }}>
|
<div className="w-16 h-16 mx-auto mb-6 rounded-full flex items-center justify-center" style={{ backgroundColor: 'var(--color-primary)' }}>
|
||||||
<HelpCircle className="w-8 h-8" style={{ color: 'white' }} />
|
<HelpCircle className="w-8 h-8" style={{ color: 'white' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-h3 mb-4" style={{ color: 'var(--color-black)' }}>
|
<h2 className="text-h3 mb-4" style={{ color: 'var(--color-black)' }}>
|
||||||
Still have questions?
|
Still have questions?
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="text-body-lg mb-8 max-w-2xl mx-auto" style={{ color: 'var(--color-gray-muted)' }}>
|
<p className="text-body-lg mb-8 max-w-2xl mx-auto" style={{ color: 'var(--color-gray-muted)' }}>
|
||||||
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.
|
about our leadership development programs and services.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="Contact Support"
|
text="Contact Support"
|
||||||
onClick={() => navigate('/contact')}
|
onClick={() => navigate('/contact')}
|
||||||
className="cta-text-black"
|
className="cta-text-black"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="mailto:connect@leadershipcentre.in"
|
href="mailto:connect@leadershipcentre.in"
|
||||||
className="flex items-center gap-2 px-6 py-3 text-body-lg transition-colors duration-200 hover:bg-gray-50 rounded-lg"
|
className="flex items-center gap-2 px-6 py-3 text-body-lg transition-colors duration-200 hover:bg-gray-50 rounded-lg"
|
||||||
style={{ color: 'var(--color-primary)' }}
|
style={{ color: 'var(--color-primary)' }}
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import HeroSection from '../components/HeroSection';
|
import HeroSection from "../components/HeroSection";
|
||||||
import { StatsSection } from '../components/StatsSection';
|
import { StatsSection } from "../components/StatsSection";
|
||||||
import { LogosSection } from '../components/LogosSection';
|
import { LogosSection } from "../components/LogosSection";
|
||||||
import { ServicesSection } from '../components/ServicesSection';
|
import { ServicesSection } from "../components/ServicesSection";
|
||||||
import { VirtualSpaceSection } from '../components/VirtualSpaceSection';
|
import { VirtualSpaceSection } from "../components/VirtualSpaceSection";
|
||||||
import { TestimonialsSection } from '../components/TestimonialsSection';
|
import { TestimonialsSection } from "../components/TestimonialsSection";
|
||||||
import { UpcomingWebinarsSection } from '../components/UpcomingWebinarsSection';
|
import { InsightsSection } from "../components/InsightsSection";
|
||||||
import { InsightsSection } from '../components/InsightsSection';
|
import { CTABannerSection } from "../components/CTABannerSection";
|
||||||
import { WhitepapersSection } from '../components/WhitepapersSection';
|
|
||||||
import { CTABannerSection } from '../components/CTABannerSection';
|
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { PrimaryCTAButton } from '../components/PrimaryCTAButton';
|
import { PrimaryCTAButton } from "../components/PrimaryCTAButton";
|
||||||
import { BrandedTag } from '../components/about/BrandedTag';
|
import { BrandedTag } from "../components/about/BrandedTag";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useGetHomepageQuery } from "../redux/services/homepageApi";
|
||||||
|
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeroSection />
|
<HeroSection heroSections={heroSections} isLoading={isLoading} />
|
||||||
<StatsSection />
|
|
||||||
|
{/* Stats Section */}
|
||||||
|
<StatsSection stats={stats} isLoading={isLoading} />
|
||||||
|
|
||||||
<LogosSection />
|
<LogosSection />
|
||||||
<ServicesSection />
|
<ServicesSection highlightCards={highlightCards} isLoading={isLoading} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto text-center py-16 px-4 bg-gradient-to-r from-blue-900 via-gray-400 to-black exp-our-head-tab-sec" >
|
<div className="mx-auto text-center py-16 px-4 bg-gradient-to-r from-blue-900 via-gray-400 to-black exp-our-head-tab-sec">
|
||||||
{/* Branded Tag */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
@@ -40,7 +51,6 @@ const HomePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Main Heading */}
|
|
||||||
<motion.h2
|
<motion.h2
|
||||||
className="text-5xl font-bold leading-tight mb-4 max-lg:text-4xl max-md:text-3xl text-white"
|
className="text-5xl font-bold leading-tight mb-4 max-lg:text-4xl max-md:text-3xl text-white"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
@@ -51,7 +61,6 @@ const HomePage: React.FC = () => {
|
|||||||
Experience Our Space Virtually
|
Experience Our Space Virtually
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
|
|
||||||
{/* Subheading */}
|
|
||||||
<motion.p
|
<motion.p
|
||||||
className="text-lg leading-relaxed max-w-2xl mx-auto max-lg:text-base mb-6 text-white/90"
|
className="text-lg leading-relaxed max-w-2xl mx-auto max-lg:text-base mb-6 text-white/90"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -59,10 +68,10 @@ const HomePage: React.FC = () => {
|
|||||||
transition={{ duration: 0.8, delay: 0.4 }}
|
transition={{ duration: 0.8, delay: 0.4 }}
|
||||||
viewport={{ once: true }}
|
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.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Main CTA Button - Explore Our Space */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex justify-center"
|
className="flex justify-center"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -73,19 +82,19 @@ const HomePage: React.FC = () => {
|
|||||||
<div className="hero-slide-button">
|
<div className="hero-slide-button">
|
||||||
<PrimaryCTAButton
|
<PrimaryCTAButton
|
||||||
text="Explore Our Space"
|
text="Explore Our Space"
|
||||||
onClick={() => navigate('/services/learning-facility')}
|
onClick={() => navigate("/services/learning-facility")}
|
||||||
ariaLabel="Explore our virtual learning space and facilities"
|
ariaLabel="Explore our virtual learning space and facilities"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VirtualSpaceSection />
|
<VirtualSpaceSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TestimonialsSection />
|
<TestimonialsSection />
|
||||||
{/* <UpcomingWebinarsSection /> */}
|
|
||||||
<InsightsSection />
|
<InsightsSection />
|
||||||
{/* <WhitepapersSection /> */}
|
<CTABannerSection ctaBands={ctaBands} isLoading={isLoading} />
|
||||||
<CTABannerSection />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<>
|
|
||||||
<HeroSection />
|
|
||||||
<StatsSection />
|
|
||||||
<LogosSection />
|
|
||||||
<ServicesSectionNew />
|
|
||||||
{/* <LearningEnvionment /> */}
|
|
||||||
<div>
|
|
||||||
<div className="mx-auto text-center py-16 px-4 bg-gradient-to-r from-blue-900 via-gray-400 to-black exp-our-head-tab-sec" >
|
|
||||||
{/* Branded Tag */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
<BrandedTag
|
|
||||||
text="Virtual Learning Environment"
|
|
||||||
className="justify-center"
|
|
||||||
variant="white"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Main Heading */}
|
|
||||||
<motion.h2
|
|
||||||
className="text-5xl font-bold leading-tight mb-4 max-lg:text-4xl max-md:text-3xl text-white"
|
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
Experience Our Space Virtually
|
|
||||||
</motion.h2>
|
|
||||||
|
|
||||||
{/* Subheading */}
|
|
||||||
<motion.p
|
|
||||||
className="text-lg leading-relaxed max-w-2xl mx-auto max-lg:text-base mb-6 text-white/90"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
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.
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
{/* Main CTA Button - Explore Our Space */}
|
|
||||||
<motion.div
|
|
||||||
className="flex justify-center"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.6 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
<div className="hero-slide-button">
|
|
||||||
<PrimaryCTAButton
|
|
||||||
text="Explore Our Space"
|
|
||||||
onClick={() => navigate('/services/learning-facility')}
|
|
||||||
ariaLabel="Explore our virtual learning space and facilities"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
<VirtualSpaceSection />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
82
src/redux/services/aboutUsApi.ts
Normal file
82
src/redux/services/aboutUsApi.ts
Normal file
@@ -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<AboutUsData, void>({
|
||||||
|
query: () => ({
|
||||||
|
url: "/admin/about-us",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 🔥 extract only useful data
|
||||||
|
transformResponse: (response: AboutUsResponse) => response.data,
|
||||||
|
|
||||||
|
providesTags: ["AboutUs"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetAboutUsQuery,
|
||||||
|
} = aboutUsApi;
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
const rawBaseQuery = fetchBaseQuery({
|
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 baseQueryWithReauth = async (args: any, api: any, extraOptions: any) => {
|
||||||
const result = await rawBaseQuery(args, api, extraOptions);
|
const result = await rawBaseQuery(args, api, extraOptions);
|
||||||
|
|
||||||
// Optional: reauthentication logic if result.error?.status === 401
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
126
src/redux/services/blogApi.ts
Normal file
126
src/redux/services/blogApi.ts
Normal file
@@ -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<BlogListResponse, BlogListParams>({
|
||||||
|
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<string, any> = {
|
||||||
|
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<BlogItem, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
url: `/admin/blogs/list/${id}`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
transformResponse: (response: BlogByIdResponse) => response.data,
|
||||||
|
providesTags: ["blog"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetBlogsQuery, useGetBlogByIDQuery } = blogApi;
|
||||||
39
src/redux/services/contactUsApi.ts
Normal file
39
src/redux/services/contactUsApi.ts
Normal file
@@ -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;
|
||||||
44
src/redux/services/faqApi.ts
Normal file
44
src/redux/services/faqApi.ts
Normal file
@@ -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;
|
||||||
92
src/redux/services/homepageApi.ts
Normal file
92
src/redux/services/homepageApi.ts
Normal file
@@ -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;
|
||||||
27
src/redux/store/Store.tsx
Normal file
27
src/redux/store/Store.tsx
Normal file
@@ -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<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
@@ -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<void, FormData>({
|
|
||||||
query: (formData) => ({
|
|
||||||
url: "/api/store-contact-us-data",
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { useStoreSwitchToDashboardMutation } = storeSwitchToDashboard;
|
|
||||||
@@ -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;
|
|
||||||
@@ -1092,6 +1092,10 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive modal styles - No Scroll Version */
|
/* Responsive modal styles - No Scroll Version */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.virtual-space-modal-container {
|
.virtual-space-modal-container {
|
||||||
|
|||||||
14
src/utils/getReadingTime.ts
Normal file
14
src/utils/getReadingTime.ts
Normal file
@@ -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`;
|
||||||
|
};
|
||||||
52
src/utils/urlHelpers.ts
Normal file
52
src/utils/urlHelpers.ts
Normal file
@@ -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;
|
||||||
|
};
|
||||||
@@ -16,6 +16,13 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"src",
|
||||||
}
|
"src/redux/hooks",
|
||||||
|
"src/redux/store",
|
||||||
|
"src/redux/services"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
156
vite.config.ts
156
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';
|
export default defineConfig({
|
||||||
import react from '@vitejs/plugin-react-swc';
|
plugins: [react()],
|
||||||
import * as path from 'path';
|
resolve: {
|
||||||
|
extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
|
||||||
export default defineConfig({
|
alias: {
|
||||||
plugins: [react()],
|
"vaul@1.1.2": "vaul",
|
||||||
resolve: {
|
"sonner@2.0.3": "sonner",
|
||||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
"recharts@2.15.2": "recharts",
|
||||||
alias: {
|
"react-resizable-panels@2.1.7": "react-resizable-panels",
|
||||||
'vaul@1.1.2': 'vaul',
|
"react-hook-form@7.55.0": "react-hook-form",
|
||||||
'sonner@2.0.3': 'sonner',
|
"react-day-picker@8.10.1": "react-day-picker",
|
||||||
'recharts@2.15.2': 'recharts',
|
"next-themes@0.4.6": "next-themes",
|
||||||
'react-resizable-panels@2.1.7': 'react-resizable-panels',
|
"lucide-react@0.487.0": "lucide-react",
|
||||||
'react-hook-form@7.55.0': 'react-hook-form',
|
"input-otp@1.4.2": "input-otp",
|
||||||
'react-day-picker@8.10.1': 'react-day-picker',
|
"figma:asset/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png": path.resolve(
|
||||||
'next-themes@0.4.6': 'next-themes',
|
__dirname,
|
||||||
'lucide-react@0.487.0': 'lucide-react',
|
"./src/assets/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png",
|
||||||
'input-otp@1.4.2': 'input-otp',
|
),
|
||||||
'figma:asset/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png': path.resolve(__dirname, './src/assets/e98caa8afd8d11246bbff1dde75bbaae6f6a0894.png'),
|
"figma:asset/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png": path.resolve(
|
||||||
'figma:asset/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png': path.resolve(__dirname, './src/assets/e8fad960112d5eba554c3969d08891ebe4d4b9c7.png'),
|
__dirname,
|
||||||
'figma:asset/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png': path.resolve(__dirname, './src/assets/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png'),
|
"./src/assets/e8fad960112d5eba554c3969d08891ebe4d4b9c7.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/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png": path.resolve(
|
||||||
'figma:asset/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png': path.resolve(__dirname, './src/assets/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png'),
|
__dirname,
|
||||||
'figma:asset/4833274f0a593cd31fdefe553b70bb016de281af.png': path.resolve(__dirname, './src/assets/4833274f0a593cd31fdefe553b70bb016de281af.png'),
|
"./src/assets/d5bab6ea4f3d8cef3b0425c45cfee7faea19fdbc.png",
|
||||||
'figma:asset/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png': path.resolve(__dirname, './src/assets/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png'),
|
),
|
||||||
'embla-carousel-react@8.6.0': 'embla-carousel-react',
|
"figma:asset/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png": path.resolve(
|
||||||
'cmdk@1.1.1': 'cmdk',
|
__dirname,
|
||||||
'class-variance-authority@0.7.1': 'class-variance-authority',
|
"./src/assets/c57ec1f4466f68e607139a3cd6d52f7e2f372408.png",
|
||||||
'@radix-ui/react-tooltip@1.1.8': '@radix-ui/react-tooltip',
|
),
|
||||||
'@radix-ui/react-toggle@1.1.2': '@radix-ui/react-toggle',
|
"figma:asset/a28d79dd35b730f689b77dbb30452ca27bd25759.png": path.resolve(
|
||||||
'@radix-ui/react-toggle-group@1.1.2': '@radix-ui/react-toggle-group',
|
__dirname,
|
||||||
'@radix-ui/react-tabs@1.1.3': '@radix-ui/react-tabs',
|
"./src/assets/a28d79dd35b730f689b77dbb30452ca27bd25759.png",
|
||||||
'@radix-ui/react-switch@1.1.3': '@radix-ui/react-switch',
|
),
|
||||||
'@radix-ui/react-slot@1.1.2': '@radix-ui/react-slot',
|
"figma:asset/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png": path.resolve(
|
||||||
'@radix-ui/react-slider@1.2.3': '@radix-ui/react-slider',
|
__dirname,
|
||||||
'@radix-ui/react-separator@1.1.2': '@radix-ui/react-separator',
|
"./src/assets/6bdf8056f51bbdc6dd9dab9044a6579a254bd02c.png",
|
||||||
'@radix-ui/react-select@2.1.6': '@radix-ui/react-select',
|
),
|
||||||
'@radix-ui/react-scroll-area@1.2.3': '@radix-ui/react-scroll-area',
|
"figma:asset/4833274f0a593cd31fdefe553b70bb016de281af.png": path.resolve(
|
||||||
'@radix-ui/react-radio-group@1.2.3': '@radix-ui/react-radio-group',
|
__dirname,
|
||||||
'@radix-ui/react-progress@1.1.2': '@radix-ui/react-progress',
|
"./src/assets/4833274f0a593cd31fdefe553b70bb016de281af.png",
|
||||||
'@radix-ui/react-popover@1.1.6': '@radix-ui/react-popover',
|
),
|
||||||
'@radix-ui/react-navigation-menu@1.2.5': '@radix-ui/react-navigation-menu',
|
"figma:asset/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png": path.resolve(
|
||||||
'@radix-ui/react-menubar@1.1.6': '@radix-ui/react-menubar',
|
__dirname,
|
||||||
'@radix-ui/react-label@2.1.2': '@radix-ui/react-label',
|
"./src/assets/037c4659b7b0bf15b1dfdcd4868cb42e8257e838.png",
|
||||||
'@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',
|
"embla-carousel-react@8.6.0": "embla-carousel-react",
|
||||||
'@radix-ui/react-dialog@1.1.6': '@radix-ui/react-dialog',
|
"cmdk@1.1.1": "cmdk",
|
||||||
'@radix-ui/react-context-menu@2.2.6': '@radix-ui/react-context-menu',
|
"class-variance-authority@0.7.1": "class-variance-authority",
|
||||||
'@radix-ui/react-collapsible@1.1.3': '@radix-ui/react-collapsible',
|
"@radix-ui/react-tooltip@1.1.8": "@radix-ui/react-tooltip",
|
||||||
'@radix-ui/react-checkbox@1.1.4': '@radix-ui/react-checkbox',
|
"@radix-ui/react-toggle@1.1.2": "@radix-ui/react-toggle",
|
||||||
'@radix-ui/react-avatar@1.1.3': '@radix-ui/react-avatar',
|
"@radix-ui/react-toggle-group@1.1.2": "@radix-ui/react-toggle-group",
|
||||||
'@radix-ui/react-aspect-ratio@1.1.2': '@radix-ui/react-aspect-ratio',
|
"@radix-ui/react-tabs@1.1.3": "@radix-ui/react-tabs",
|
||||||
'@radix-ui/react-alert-dialog@1.1.6': '@radix-ui/react-alert-dialog',
|
"@radix-ui/react-switch@1.1.3": "@radix-ui/react-switch",
|
||||||
'@radix-ui/react-accordion@1.2.3': '@radix-ui/react-accordion',
|
"@radix-ui/react-slot@1.1.2": "@radix-ui/react-slot",
|
||||||
'@': path.resolve(__dirname, './src'),
|
"@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',
|
build: {
|
||||||
outDir: 'build',
|
target: "esnext",
|
||||||
},
|
outDir: "build",
|
||||||
server: {
|
},
|
||||||
port: 4000,
|
server: {
|
||||||
open: true,
|
port: 4000,
|
||||||
},
|
open: true,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user