update
This commit is contained in:
37
frontEnd/components/common/Counter.tsx
Normal file
37
frontEnd/components/common/Counter.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import "odometer/themes/odometer-theme-default.css";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
const Odometer = dynamic(() => import("react-odometerjs"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
interface CounterProps {
|
||||
value: number;
|
||||
}
|
||||
|
||||
const Counter = ({ value }: CounterProps) => {
|
||||
const [odometerValue, setOdometerValue] = useState(0);
|
||||
const [ref, inView] = useInView();
|
||||
|
||||
useEffect(() => {
|
||||
if (inView) {
|
||||
setTimeout(() => {
|
||||
setOdometerValue(value);
|
||||
}, 1000);
|
||||
}
|
||||
}, [inView, value]);
|
||||
|
||||
return (
|
||||
<span ref={ref}>
|
||||
{inView ? (
|
||||
<Odometer value={odometerValue} format="(,ddd)" theme="default" />
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Counter;
|
||||
241
frontEnd/components/pages/about/AboutBanner.tsx
Normal file
241
frontEnd/components/pages/about/AboutBanner.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useAbout } from "@/lib/hooks/useAbout";
|
||||
|
||||
const AboutBanner = () => {
|
||||
const { data, loading, error } = useAbout();
|
||||
const [currentMetric, setCurrentMetric] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-rotate metrics
|
||||
const interval = setInterval(() => {
|
||||
setCurrentMetric(prev => (prev + 1) % 3);
|
||||
}, 4000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="about-banner fix-top pb-120 position-relative overflow-x-clip">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3">Loading about us content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<section className="about-banner fix-top pb-120 position-relative overflow-x-clip">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Error Loading Content</h4>
|
||||
<p>{error}</p>
|
||||
<hr />
|
||||
<p className="mb-0">Please try refreshing the page or contact support if the problem persists.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Use API data or fallback to default content
|
||||
const bannerData = data?.banner;
|
||||
|
||||
const metrics = [
|
||||
{ value: "8", label: "Industry Verticals", icon: "fa-industry", color: "#3b82f6" },
|
||||
{ value: "99.9%", label: "Uptime SLA", icon: "fa-shield-halved", color: "#10b981" },
|
||||
{ value: "24/7", label: "Enterprise Support", icon: "fa-headset", color: "#f59e0b" },
|
||||
{ value: "5+", label: "Years of Operation", icon: "fa-award", color: "#8b5cf6" }
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="hero-banner">
|
||||
{/* Video-like Background */}
|
||||
<div className="hero-background">
|
||||
{/* Animated Code Lines */}
|
||||
<div className="code-animation">
|
||||
<div className="code-line line-1">
|
||||
<span className="code-keyword">const</span> enterprise = <span className="code-string">'mission-critical'</span>;
|
||||
</div>
|
||||
<div className="code-line line-2">
|
||||
<span className="code-keyword">if</span> (security.level === <span className="code-string">'enterprise'</span>) {
|
||||
</div>
|
||||
<div className="code-line line-3">
|
||||
deploy<span className="code-function">()</span>;
|
||||
</div>
|
||||
<div className="code-line line-4">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Tech Icons */}
|
||||
<div className="floating-tech">
|
||||
<div className="tech-icon icon-1">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div className="tech-icon icon-2">
|
||||
<i className="fa-solid fa-cloud"></i>
|
||||
</div>
|
||||
<div className="tech-icon icon-3">
|
||||
<i className="fa-solid fa-server"></i>
|
||||
</div>
|
||||
<div className="tech-icon icon-4">
|
||||
<i className="fa-solid fa-lock"></i>
|
||||
</div>
|
||||
<div className="tech-icon icon-5">
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="tech-icon icon-6">
|
||||
<i className="fa-solid fa-database"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Dashboard Elements */}
|
||||
<div className="dashboard-elements">
|
||||
<div className="dashboard-card card-1">
|
||||
<div className="card-header">
|
||||
<i className="fa-solid fa-shield-check"></i>
|
||||
<span>Security Status</span>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="status-indicator resolved"></div>
|
||||
<span>Enterprise Grade</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-card card-2">
|
||||
<div className="card-header">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
<span>SLA Response</span>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="metric-value">99.9%</div>
|
||||
<span>Uptime</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-card card-3">
|
||||
<div className="card-header">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
<span>Enterprise Clients</span>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<div className="metric-value">500+</div>
|
||||
<span>Fortune 500</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Connection Lines */}
|
||||
<div className="network-lines">
|
||||
<div className="connection-line line-1"></div>
|
||||
<div className="connection-line line-2"></div>
|
||||
<div className="connection-line line-3"></div>
|
||||
<div className="connection-line line-4"></div>
|
||||
</div>
|
||||
|
||||
{/* Data Flow Particles */}
|
||||
<div className="data-particles">
|
||||
<div className="particle particle-1"></div>
|
||||
<div className="particle particle-2"></div>
|
||||
<div className="particle particle-3"></div>
|
||||
<div className="particle particle-4"></div>
|
||||
<div className="particle particle-5"></div>
|
||||
<div className="particle particle-6"></div>
|
||||
</div>
|
||||
|
||||
{/* Background Gradient Overlay */}
|
||||
<div className="video-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="hero-content text-center">
|
||||
{/* Badge */}
|
||||
<div className="hero-badge">
|
||||
<div className="badge-icon">
|
||||
<i className="fa-solid fa-shield-check"></i>
|
||||
</div>
|
||||
<span>Enterprise Security Certified</span>
|
||||
</div>
|
||||
|
||||
{/* Main Title */}
|
||||
<h1 className="hero-title">
|
||||
{bannerData?.title || "Mission-Critical Software Solutions for Global Enterprises"}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="hero-description">
|
||||
{bannerData?.description || "GNX Soft Ltd delivers enterprise-grade software solutions for mission-critical industries with 99.9% uptime SLA and 24/7 dedicated support. Our platforms power digital transformation across Defense & Aerospace, Healthcare, Banking, Telecommunication, and other highly regulated sectors."}
|
||||
</p>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<div className="hero-metrics">
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`metric-item ${currentMetric === index ? 'active' : ''}`}
|
||||
onClick={() => setCurrentMetric(index)}
|
||||
>
|
||||
<div className="metric-icon" style={{ backgroundColor: metric.color }}>
|
||||
<i className={`fa-solid ${metric.icon}`}></i>
|
||||
</div>
|
||||
<div className="metric-content">
|
||||
<div className="metric-value">{metric.value}</div>
|
||||
<div className="metric-label">{metric.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="hero-actions">
|
||||
<Link href={bannerData?.cta_link || "services"} className="btn-primary">
|
||||
<span>View Enterprise Solutions</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
<Link href="contact-us" className="btn-secondary">
|
||||
<span>Schedule Consultation</span>
|
||||
<i className="fa-solid fa-calendar-check"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="social-links">
|
||||
<Link href="https://www.linkedin.com/company/gnxtech" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
<Link href="https://github.com/gnxtech" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutBanner;
|
||||
65
frontEnd/components/pages/about/AboutInitAnimations.tsx
Normal file
65
frontEnd/components/pages/about/AboutInitAnimations.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AboutInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutInitAnimations;
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const AboutScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutScrollProgressButton;
|
||||
566
frontEnd/components/pages/about/AboutService.tsx
Normal file
566
frontEnd/components/pages/about/AboutService.tsx
Normal file
@@ -0,0 +1,566 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useAbout } from "@/lib/hooks/useAbout";
|
||||
import { AboutService as AboutServiceType, AboutProcess } from "@/lib/api/aboutService";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import thumb from "@/public/images/service/two.png";
|
||||
import thumbTwo from "@/public/images/service/three.png";
|
||||
|
||||
const AboutServiceComponent = () => {
|
||||
const { data, loading, error } = useAbout();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="about-service-section">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3">Loading service content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="about-service-section">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Error Loading Content</h4>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const serviceData = data?.service as AboutServiceType | undefined;
|
||||
const processData = data?.process as AboutProcess | undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="about-service-section" suppressHydrationWarning>
|
||||
<div className="container">
|
||||
<div className="row g-5 align-items-start">
|
||||
{/* Image Column */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="about-image-wrapper">
|
||||
<div className="about-image-container">
|
||||
{serviceData?.image_url ? (
|
||||
<img
|
||||
src={getValidImageUrl(serviceData.image_url, FALLBACK_IMAGES.SERVICE)}
|
||||
className="about-image"
|
||||
alt="Enterprise Technology Solutions"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={thumb}
|
||||
className="about-image"
|
||||
alt="Enterprise Technology Solutions"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
)}
|
||||
<div className="image-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Column */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="about-content-wrapper">
|
||||
<div className="luxury-badge">
|
||||
<i className={serviceData?.badge_icon || "fa-solid fa-users"}></i>
|
||||
<span>{serviceData?.badge_text || "About Our Company"}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="luxury-title">
|
||||
{serviceData?.title || "GNX Soft Ltd. - Software Excellence"}
|
||||
</h2>
|
||||
|
||||
<p className="luxury-description">
|
||||
{serviceData?.description || "Founded in 2020, GNX Soft Ltd. has emerged as a premier enterprise software company, delivering mission-critical software solutions across various industries."}
|
||||
</p>
|
||||
|
||||
<div className="luxury-features-grid">
|
||||
{serviceData?.features && serviceData.features.length > 0 ? (
|
||||
serviceData.features.map((feature, index) => (
|
||||
<div key={index} className="luxury-feature-card">
|
||||
<div className="feature-icon-wrapper">
|
||||
<i className={feature.icon}></i>
|
||||
</div>
|
||||
<div className="feature-text">
|
||||
<h6 className="feature-title">{feature.title}</h6>
|
||||
<p className="feature-description">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<div className="luxury-feature-card">
|
||||
<div className="feature-icon-wrapper">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div className="feature-text">
|
||||
<h6 className="feature-title">Enterprise Security</h6>
|
||||
<p className="feature-description">Defense-Grade Protection</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="luxury-feature-card">
|
||||
<div className="feature-icon-wrapper">
|
||||
<i className="fa-solid fa-cloud"></i>
|
||||
</div>
|
||||
<div className="feature-text">
|
||||
<h6 className="feature-title">Cloud Native</h6>
|
||||
<p className="feature-description">AWS, Azure, GCP Partners</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="luxury-cta">
|
||||
<Link href={serviceData?.cta_link || "service-single"} className="luxury-btn">
|
||||
<span>{serviceData?.cta_text || "Explore Our Solutions"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-process-section">
|
||||
<div className="container">
|
||||
<div className="row g-5 align-items-start">
|
||||
{/* Content Column */}
|
||||
<div className="col-12 col-lg-6 order-2 order-lg-1">
|
||||
<div className="about-content-wrapper">
|
||||
<div className="luxury-badge">
|
||||
<i className={processData?.badge_icon || "fa-solid fa-cogs"}></i>
|
||||
<span>{processData?.badge_text || "Our Methodology"}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="luxury-title">
|
||||
{processData?.title || "Enterprise Development Process"}
|
||||
</h2>
|
||||
|
||||
<p className="luxury-description">
|
||||
{processData?.description || "Our proven enterprise development methodology combines agile practices with enterprise-grade security, scalability, and compliance requirements."}
|
||||
</p>
|
||||
|
||||
<div className="luxury-process-steps">
|
||||
{processData?.steps && processData.steps.length > 0 ? (
|
||||
processData.steps.map((step, index) => (
|
||||
<div key={index} className="luxury-process-step">
|
||||
<div className="step-number-badge">{step.step_number}</div>
|
||||
<div className="step-content-wrapper">
|
||||
<h6 className="step-title">{step.title}</h6>
|
||||
<p className="step-description">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<div className="luxury-process-step">
|
||||
<div className="step-number-badge">01</div>
|
||||
<div className="step-content-wrapper">
|
||||
<h6 className="step-title">Discovery & Planning</h6>
|
||||
<p className="step-description">Comprehensive analysis and architecture design</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="luxury-process-step">
|
||||
<div className="step-number-badge">02</div>
|
||||
<div className="step-content-wrapper">
|
||||
<h6 className="step-title">Development & Testing</h6>
|
||||
<p className="step-description">Agile development with continuous testing</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="luxury-cta">
|
||||
<Link href={processData?.cta_link || "service-single"} className="luxury-btn">
|
||||
<span>{processData?.cta_text || "View Our Services"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Column */}
|
||||
<div className="col-12 col-lg-6 order-1 order-lg-2">
|
||||
<div className="about-image-wrapper">
|
||||
<div className="about-image-container">
|
||||
{processData?.image_url ? (
|
||||
<img
|
||||
src={getValidImageUrl(processData.image_url, FALLBACK_IMAGES.SERVICE)}
|
||||
className="about-image"
|
||||
alt="Enterprise Development Process"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={thumbTwo}
|
||||
className="about-image"
|
||||
alt="Enterprise Development Process"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
)}
|
||||
<div className="image-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>{`
|
||||
/* Section Base Styles */
|
||||
.about-service-section,
|
||||
.about-process-section {
|
||||
padding: 120px 0;
|
||||
position: relative;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* Image Wrapper - Perfect Alignment */
|
||||
.about-image-wrapper {
|
||||
position: sticky;
|
||||
top: 120px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.about-image-container {
|
||||
position: relative;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.about-image-container:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.about-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 24px;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.about-image-container:hover .about-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
/* Content Wrapper - Perfect Alignment */
|
||||
.about-content-wrapper {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.about-content-wrapper {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Luxury Badge */
|
||||
.luxury-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
width: fit-content;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.luxury-badge i {
|
||||
font-size: 0.875rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Luxury Title */
|
||||
.luxury-title {
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.8) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Luxury Description */
|
||||
.luxury-description {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.8;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Features Grid */
|
||||
.luxury-features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.25rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.luxury-features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.luxury-feature-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.luxury-feature-card:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.feature-icon-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.feature-icon-wrapper i {
|
||||
font-size: 1.5rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Process Steps */
|
||||
.luxury-process-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.luxury-process-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.25rem;
|
||||
padding: 1.75rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 18px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.luxury-process-step:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateX(8px);
|
||||
}
|
||||
|
||||
.step-number-badge {
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 16px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.step-content-wrapper {
|
||||
flex: 1;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Luxury CTA Button */
|
||||
.luxury-cta {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.luxury-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 2rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.luxury-btn:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.luxury-btn i {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.luxury-btn:hover i {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 991px) {
|
||||
.about-service-section,
|
||||
.about-process-section {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.about-image-wrapper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.about-content-wrapper {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.luxury-features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.luxury-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.luxury-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.luxury-feature-card,
|
||||
.luxury-process-step {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutServiceComponent;
|
||||
149
frontEnd/components/pages/about/AboutStarter.tsx
Normal file
149
frontEnd/components/pages/about/AboutStarter.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useAbout } from "@/lib/hooks/useAbout";
|
||||
import { AboutJourney } from "@/lib/api/aboutService";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import thumb from "@/public/images/start-thumb.png";
|
||||
|
||||
const AboutStarter = () => {
|
||||
const { data, loading, error } = useAbout();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="about-journey-section">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-light" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3 text-white">Loading journey content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="about-journey-section">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Error Loading Content</h4>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const journeyData = data?.journey as AboutJourney | undefined;
|
||||
|
||||
return (
|
||||
<section className="about-journey-section" suppressHydrationWarning>
|
||||
<div className="container">
|
||||
<div className="row g-3 align-items-start">
|
||||
{/* Image Column */}
|
||||
<div className="col-12 col-lg-6 order-1 order-lg-2">
|
||||
<div className="about-image-wrapper">
|
||||
<div className="about-image-container">
|
||||
{journeyData?.image_url ? (
|
||||
<img
|
||||
src={getValidImageUrl(journeyData.image_url, FALLBACK_IMAGES.DEFAULT)}
|
||||
className="about-image"
|
||||
alt="Enterprise Journey"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={thumb}
|
||||
className="about-image"
|
||||
alt="Enterprise Journey"
|
||||
width={600}
|
||||
height={700}
|
||||
/>
|
||||
)}
|
||||
<div className="image-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Column */}
|
||||
<div className="col-12 col-lg-6 order-2 order-lg-1">
|
||||
<div className="about-content-wrapper">
|
||||
<div className="luxury-badge">
|
||||
<i className={journeyData?.badge_icon || "fa-solid fa-rocket"}></i>
|
||||
<span>{journeyData?.badge_text || "Our Journey"}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="luxury-title">
|
||||
{journeyData?.title || "Building Enterprise Excellence Since 2020"}
|
||||
</h2>
|
||||
|
||||
<p className="luxury-description">
|
||||
{journeyData?.description || "Founded in 2020 in Burgas, Bulgaria, GNX Soft Ltd was established with a clear mission: to deliver world-class enterprise software solutions for mission-critical industries."}
|
||||
</p>
|
||||
|
||||
<div className="luxury-milestones">
|
||||
{journeyData?.milestones && journeyData.milestones.length > 0 ? (
|
||||
journeyData.milestones.map((milestone, index) => (
|
||||
<div key={index} className="luxury-milestone-card">
|
||||
<div className="milestone-year-badge">
|
||||
<span>{milestone.year}</span>
|
||||
</div>
|
||||
<div className="milestone-content-wrapper">
|
||||
<h6 className="milestone-title">{milestone.title}</h6>
|
||||
<p className="milestone-description">{milestone.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<div className="luxury-milestone-card">
|
||||
<div className="milestone-year-badge">
|
||||
<span>2020</span>
|
||||
</div>
|
||||
<div className="milestone-content-wrapper">
|
||||
<h6 className="milestone-title">Company Founded</h6>
|
||||
<p className="milestone-description">GNX Soft Ltd established in Burgas, Bulgaria</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="luxury-milestone-card">
|
||||
<div className="milestone-year-badge">
|
||||
<span>2021</span>
|
||||
</div>
|
||||
<div className="milestone-content-wrapper">
|
||||
<h6 className="milestone-title">Industry Specialization</h6>
|
||||
<p className="milestone-description">Specialized in 8 mission-critical industries</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="luxury-cta">
|
||||
<Link href={journeyData?.cta_link || "services"} className="luxury-btn">
|
||||
<span>{journeyData?.cta_text || "Explore Solutions"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutStarter;
|
||||
65
frontEnd/components/pages/blog/BlogInitAnimations.tsx
Normal file
65
frontEnd/components/pages/blog/BlogInitAnimations.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const BlogInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogInitAnimations;
|
||||
150
frontEnd/components/pages/blog/BlogItems.tsx
Normal file
150
frontEnd/components/pages/blog/BlogItems.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import PostFilterItems from "./post-filter/PostFilterItems";
|
||||
|
||||
const BlogItems = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const postsPerPage = 6;
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
setCurrentPage(page);
|
||||
// Scroll to top of posts section
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTotalPagesChange = (total: number) => {
|
||||
setTotalPages(total);
|
||||
};
|
||||
|
||||
// Generate page numbers to display
|
||||
const getPageNumbers = () => {
|
||||
const pages: (number | string)[] = [];
|
||||
const maxPagesToShow = 5;
|
||||
|
||||
if (totalPages <= maxPagesToShow) {
|
||||
// Show all pages if total is small
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// Show first page
|
||||
pages.push(1);
|
||||
|
||||
// Calculate range around current page
|
||||
let startPage = Math.max(2, currentPage - 1);
|
||||
let endPage = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
// Add ellipsis after first page if needed
|
||||
if (startPage > 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Add pages around current page
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
// Add ellipsis before last page if needed
|
||||
if (endPage < totalPages - 1) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// Show last page
|
||||
if (totalPages > 1) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="fix-top pb-120 blog-m">
|
||||
<div className="container">
|
||||
<div className="row align-items-center vertical-column-gap">
|
||||
<div className="col-12 col-lg-7">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">Latest Company Insights</h2>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<form action="#" method="post" autoComplete="off">
|
||||
<div className="search-form">
|
||||
<input
|
||||
type="search"
|
||||
name="search-post"
|
||||
id="searchPost"
|
||||
placeholder="Search"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="search post"
|
||||
title="search post"
|
||||
>
|
||||
<i className="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<PostFilterItems
|
||||
currentPage={currentPage}
|
||||
onPageChange={handlePageChange}
|
||||
onTotalPagesChange={handleTotalPagesChange}
|
||||
postsPerPage={postsPerPage}
|
||||
/>
|
||||
{totalPages > 1 && (
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<div className="section__cta">
|
||||
<ul className="pagination">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
aria-label="Previous page"
|
||||
style={{ opacity: currentPage === 1 ? 0.5 : 1, cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<i className="fa-solid fa-angle-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
{getPageNumbers().map((page, index) => (
|
||||
<li key={index}>
|
||||
{typeof page === 'number' ? (
|
||||
<button
|
||||
onClick={() => handlePageChange(page)}
|
||||
className={currentPage === page ? 'active' : ''}
|
||||
aria-label={`Go to page ${page}`}
|
||||
aria-current={currentPage === page ? 'page' : undefined}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ padding: '0 10px' }}>{page}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
aria-label="Next page"
|
||||
style={{ opacity: currentPage === totalPages ? 0.5 : 1, cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
<i className="fa-solid fa-angle-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogItems;
|
||||
67
frontEnd/components/pages/blog/BlogScrollProgressButton.tsx
Normal file
67
frontEnd/components/pages/blog/BlogScrollProgressButton.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const BlogScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogScrollProgressButton;
|
||||
272
frontEnd/components/pages/blog/BlogSingle.tsx
Normal file
272
frontEnd/components/pages/blog/BlogSingle.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
"use client";
|
||||
import { useParams } from "next/navigation";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useBlogPost } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const BlogSingle = () => {
|
||||
const params = useParams();
|
||||
const slug = params?.slug as string;
|
||||
const { post, loading, error } = useBlogPost(slug);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="loading-state text-center py-5">
|
||||
<div className="spinner-border text-primary mb-3" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="text-tertiary">Loading insight...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !post) {
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="error-state text-center py-5">
|
||||
<div className="error-icon mb-4">
|
||||
<i className="fa-solid fa-exclamation-circle fa-4x text-tertiary"></i>
|
||||
</div>
|
||||
<h2 className="text-secondary mb-3">Insight Not Found</h2>
|
||||
<p className="text-tertiary mb-4">
|
||||
The insight you're looking for doesn't exist or has been removed.
|
||||
</p>
|
||||
<Link href="/insights" className="btn btn-primary">
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="blog-single-section fix-top pb-120">
|
||||
<div className="container">
|
||||
{/* Article Content */}
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<article className="blog-single-article">
|
||||
{/* Article Header */}
|
||||
<header className="article-header">
|
||||
{/* Top Meta Bar */}
|
||||
<div className="article-top-meta d-flex flex-wrap align-items-center justify-content-between mb-4">
|
||||
<div className="left-meta d-flex align-items-center gap-3">
|
||||
{/* Category Badge */}
|
||||
{post.category && (
|
||||
<Link href={`/insights?category=${post.category.slug}`} className="category-badge">
|
||||
<i className="fa-solid fa-folder-open me-2"></i>
|
||||
{post.category.title}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-calendar me-2"></i>
|
||||
<span>
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="right-meta d-flex align-items-center gap-3">
|
||||
{/* Reading Time */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-clock me-2"></i>
|
||||
<span>{post.reading_time} min</span>
|
||||
</div>
|
||||
|
||||
{/* Views */}
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<i className="fa-solid fa-eye me-2"></i>
|
||||
<span>{post.views_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="article-title">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Author and Tags Bar */}
|
||||
<div className="article-bottom-meta d-flex flex-wrap align-items-center justify-content-between mt-4 pt-4">
|
||||
{/* Author */}
|
||||
<div className="author-meta d-flex align-items-center">
|
||||
<div className="author-avatar me-3">
|
||||
{post.author?.avatar ? (
|
||||
<Image
|
||||
src={post.author.avatar}
|
||||
alt={post.author.name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="rounded-circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="avatar-placeholder">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="author-info">
|
||||
<div className="author-label">Written by</div>
|
||||
<div className="author-name">
|
||||
{post.author?.name || post.author_name || 'Admin'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{post.tags && post.tags.length > 0 && (
|
||||
<div className="article-tags d-flex flex-wrap align-items-center gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={`/insights?tag=${tag.slug}`}
|
||||
className="tag-badge"
|
||||
>
|
||||
#{tag.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Featured Image */}
|
||||
{(post.featured_image || post.thumbnail) && (
|
||||
<div className="article-featured-image">
|
||||
<div className="image-wrapper">
|
||||
<Image
|
||||
src={getValidImageUrl(
|
||||
post.featured_image || post.thumbnail,
|
||||
FALLBACK_IMAGES.BLOG
|
||||
)}
|
||||
alt={getValidImageAlt(post.title)}
|
||||
width={1200}
|
||||
height={600}
|
||||
layout="responsive"
|
||||
className="featured-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Article Body */}
|
||||
<div className="article-body">
|
||||
{/* Excerpt */}
|
||||
{post.excerpt && (
|
||||
<div className="article-excerpt">
|
||||
<p className="lead">{post.excerpt}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{post.content && (
|
||||
<div
|
||||
className="article-content enterprise-content"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Section */}
|
||||
<footer className="article-footer">
|
||||
{/* Share Section */}
|
||||
<div className="article-share">
|
||||
<div className="share-container">
|
||||
<h6 className="share-title">
|
||||
Share this insight
|
||||
</h6>
|
||||
<div className="social-share">
|
||||
<Link
|
||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&title=${encodeURIComponent(post.title)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-linkedin"
|
||||
aria-label="Share on LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
<span>LinkedIn</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined' && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
alert('Link copied to clipboard!');
|
||||
}
|
||||
}}
|
||||
className="share-btn share-copy"
|
||||
aria-label="Copy link"
|
||||
>
|
||||
<i className="fa-solid fa-link"></i>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Author Bio */}
|
||||
{post.author && post.author.bio && (
|
||||
<div className="author-bio-section">
|
||||
<div className="author-bio-card">
|
||||
<div className="author-avatar-container">
|
||||
{post.author.avatar ? (
|
||||
<Image
|
||||
src={post.author.avatar}
|
||||
alt={post.author.name}
|
||||
width={90}
|
||||
height={90}
|
||||
className="rounded-circle"
|
||||
/>
|
||||
) : (
|
||||
<div className="author-avatar-large">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="author-bio-content">
|
||||
<div className="bio-label">About the Author</div>
|
||||
<h6 className="author-name">{post.author.name}</h6>
|
||||
<p className="author-bio-text">{post.author.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="article-navigation">
|
||||
<Link href="/insights" className="btn-back-insights">
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogSingle;
|
||||
117
frontEnd/components/pages/blog/LatestPost.tsx
Normal file
117
frontEnd/components/pages/blog/LatestPost.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Autoplay } from "swiper/modules";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import { useLatestPosts } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const LatestPost = () => {
|
||||
const { posts, loading, error } = useLatestPosts(8);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading latest posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || posts.length === 0) {
|
||||
return null; // Don't show the section if there's an error or no posts
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="tp-lp-title text-center text-lg-start">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">
|
||||
Related Insights
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-lp-cta text-center text-lg-end">
|
||||
<Link href="/insights" className="btn-line text-uppercase">
|
||||
View All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="tp-lp-slider-wrapper mt-60">
|
||||
<div className="tp-lp-slider-wrapper">
|
||||
<Swiper
|
||||
slidesPerView={"auto"}
|
||||
spaceBetween={24}
|
||||
slidesPerGroup={1}
|
||||
freeMode={true}
|
||||
speed={1200}
|
||||
loop={posts.length > 3}
|
||||
roundLengths={true}
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true,
|
||||
}}
|
||||
className="tp-lp-slider"
|
||||
>
|
||||
{posts.map((post) => (
|
||||
<SwiperSlide key={post.id}>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/insights/${post.slug}`} className="w-100 overflow-hidden">
|
||||
<Image
|
||||
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220"
|
||||
alt={getValidImageAlt(post.title)}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
{post.author_name || 'Admin'}
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/insights/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LatestPost;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useBlogCategories } from "@/lib/hooks/useBlog";
|
||||
|
||||
const PostFilterButtons = ({ handleClick, active }: any) => {
|
||||
const { categories: apiCategories, loading, error } = useBlogCategories();
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && apiCategories.length > 0) {
|
||||
// Add "All" category at the beginning
|
||||
const allCategory = {
|
||||
id: 0,
|
||||
title: "All",
|
||||
slug: "all",
|
||||
display_order: 0
|
||||
};
|
||||
setCategories([allCategory, ...apiCategories]);
|
||||
}
|
||||
}, [apiCategories, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="post-filter__wrapper mt-80">
|
||||
<p>Loading categories...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// Fallback to showing "All" button only
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="post-filter__wrapper mt-80">
|
||||
<button
|
||||
aria-label="Filter Post"
|
||||
className="active"
|
||||
onClick={() => handleClick("all")}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="post-filter__wrapper mt-80">
|
||||
{categories.map((item) => {
|
||||
return (
|
||||
<button
|
||||
aria-label="Filter Post"
|
||||
key={item.id}
|
||||
className={active === item.slug ? " active" : ""}
|
||||
onClick={() => handleClick(item.slug)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostFilterButtons;
|
||||
139
frontEnd/components/pages/blog/post-filter/PostFilterItems.tsx
Normal file
139
frontEnd/components/pages/blog/post-filter/PostFilterItems.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
import { useState, SetStateAction, useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import PostFilterButtons from "./PostFilterButtons";
|
||||
import { useBlogPosts } from "@/lib/hooks/useBlog";
|
||||
|
||||
interface PostFilterItemsProps {
|
||||
currentPage: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onTotalPagesChange: (totalPages: number) => void;
|
||||
postsPerPage?: number;
|
||||
}
|
||||
|
||||
const PostFilterItems = ({ currentPage, onPageChange, onTotalPagesChange, postsPerPage = 10 }: PostFilterItemsProps) => {
|
||||
const [active, setActive] = useState("all");
|
||||
const { posts: allPosts, loading, error, totalCount } = useBlogPosts({
|
||||
category: active === "all" ? undefined : active,
|
||||
page: currentPage,
|
||||
page_size: postsPerPage
|
||||
});
|
||||
const [displayData, setDisplayData] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && allPosts.length > 0) {
|
||||
setDisplayData(allPosts);
|
||||
}
|
||||
}, [allPosts, loading]);
|
||||
|
||||
// Calculate and update total pages when totalCount changes
|
||||
useEffect(() => {
|
||||
if (totalCount !== undefined && totalCount !== null) {
|
||||
const calculatedTotalPages = Math.ceil(totalCount / postsPerPage);
|
||||
onTotalPagesChange(calculatedTotalPages);
|
||||
}
|
||||
}, [totalCount, postsPerPage, onTotalPagesChange]);
|
||||
|
||||
const handleCategoryClick = (category: SetStateAction<string>) => {
|
||||
if (category === active) return;
|
||||
setActive(category);
|
||||
setDisplayData([]);
|
||||
onPageChange(1); // Reset to page 1 when category changes
|
||||
|
||||
// The API call will be triggered by the change in active state
|
||||
// which will update allPosts and trigger the useEffect above
|
||||
};
|
||||
|
||||
if (loading && displayData.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading posts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<div className="row mt-60">
|
||||
<div className="col-12">
|
||||
<p className="text-center text-danger">Error loading posts. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||
<motion.div className="row mt-60 masonry-grid" layout>
|
||||
<AnimatePresence>
|
||||
{displayData.map((item) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="col-12 col-lg-6 grid-item-main"
|
||||
key={item.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.6 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.6 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link
|
||||
href={`/insights/${item.slug}`}
|
||||
className="w-100 overflow-hidden d-block"
|
||||
>
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={getValidImageUrl(item.thumbnail, FALLBACK_IMAGES.BLOG)}
|
||||
className="w-100 mh-220 parallax-image"
|
||||
alt={getValidImageAlt(item.title)}
|
||||
width={400}
|
||||
height={220}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
{item.author_name || 'Admin'}
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(item.published_at || item.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/insights/${item.slug}`}>{item.title}</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostFilterItems;
|
||||
144
frontEnd/components/pages/career/CareerBanner.tsx
Normal file
144
frontEnd/components/pages/career/CareerBanner.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Link from "next/link";
|
||||
|
||||
const CareerBanner = () => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
if (document.querySelector(".career-banner")) {
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".career-banner",
|
||||
start: "center center",
|
||||
end: "+=100%",
|
||||
scrub: true,
|
||||
pin: false,
|
||||
},
|
||||
});
|
||||
|
||||
tl.to(".cp-banner-thumb", {
|
||||
opacity: 0.1,
|
||||
y: "40%",
|
||||
duration: 2,
|
||||
});
|
||||
|
||||
tl.to(
|
||||
".career-banner",
|
||||
{
|
||||
"--scale": 3,
|
||||
duration: 2,
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="career-banner fix-top bg-black position-relative overflow-hidden" style={{ minHeight: '50vh', paddingTop: '100px', paddingBottom: '60px' }}>
|
||||
<div className="container">
|
||||
<div className="row justify-content-center align-items-center">
|
||||
<div className="col-12 col-lg-10">
|
||||
<div className="cp-banner__content text-center">
|
||||
<h2 className="mt-8 fw-7 text-xxl title-anim text-white mb-5">
|
||||
Build Your Career with Enterprise Excellence
|
||||
</h2>
|
||||
<p className="text-quinary fs-5 mb-5" style={{ maxWidth: '700px', margin: '0 auto 2rem' }}>
|
||||
Join our global team of innovators, problem-solvers, and tech leaders building the future of digital solutions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Job Categories with Icons */}
|
||||
<div className="row justify-content-center mt-5 pt-4">
|
||||
<div className="col-6 col-md-3 text-center mb-4">
|
||||
<div className="job-category-card" style={{
|
||||
padding: '2rem 1rem',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#00d4ff' }}>
|
||||
<i className="fa-solid fa-code"></i>
|
||||
</div>
|
||||
<h5 className="text-white fw-6 mb-2">Engineering</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 col-md-3 text-center mb-4">
|
||||
<div className="job-category-card" style={{
|
||||
padding: '2rem 1rem',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#ff6b9d' }}>
|
||||
<i className="fa-solid fa-palette"></i>
|
||||
</div>
|
||||
<h5 className="text-white fw-6 mb-2">Design</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 col-md-3 text-center mb-4">
|
||||
<div className="job-category-card" style={{
|
||||
padding: '2rem 1rem',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#ffd93d' }}>
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<h5 className="text-white fw-6 mb-2">Business</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 col-md-3 text-center mb-4">
|
||||
<div className="job-category-card" style={{
|
||||
padding: '2rem 1rem',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#a78bfa' }}>
|
||||
<i className="fa-solid fa-users"></i>
|
||||
</div>
|
||||
<h5 className="text-white fw-6 mb-2">Operations</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<Link href="#scroll-to" className="scroll-to">
|
||||
Scroll
|
||||
<span className="arrow"></span>
|
||||
</Link>
|
||||
<span className="sm-c"></span>
|
||||
<span className="sm-c sm-cl"></span>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerBanner;
|
||||
65
frontEnd/components/pages/career/CareerInitAnimations.tsx
Normal file
65
frontEnd/components/pages/career/CareerInitAnimations.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const CareerInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerInitAnimations;
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const CareerScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerScrollProgressButton;
|
||||
1128
frontEnd/components/pages/career/JobApplicationForm.tsx
Normal file
1128
frontEnd/components/pages/career/JobApplicationForm.tsx
Normal file
File diff suppressed because it is too large
Load Diff
681
frontEnd/components/pages/career/JobSingle.tsx
Normal file
681
frontEnd/components/pages/career/JobSingle.tsx
Normal file
@@ -0,0 +1,681 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { JobPosition } from "@/lib/api/careerService";
|
||||
import JobApplicationForm from "./JobApplicationForm";
|
||||
|
||||
interface JobSingleProps {
|
||||
job: JobPosition;
|
||||
}
|
||||
|
||||
const JobSingle = ({ job }: JobSingleProps) => {
|
||||
const [showApplicationForm, setShowApplicationForm] = useState(false);
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (showApplicationForm) {
|
||||
// Get scrollbar width to prevent layout shift
|
||||
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
|
||||
// Save current scroll position
|
||||
const scrollY = window.scrollY;
|
||||
|
||||
// Prevent background scroll
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.top = `-${scrollY}px`;
|
||||
document.body.style.left = '0';
|
||||
document.body.style.right = '0';
|
||||
document.body.style.overflow = 'hidden';
|
||||
if (scrollbarWidth > 0) {
|
||||
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
||||
}
|
||||
} else {
|
||||
// Get the scroll position from body top
|
||||
const scrollY = parseInt(document.body.style.top || '0') * -1;
|
||||
|
||||
// Restore scroll
|
||||
document.body.style.position = '';
|
||||
document.body.style.top = '';
|
||||
document.body.style.left = '';
|
||||
document.body.style.right = '';
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
|
||||
// Restore scroll position
|
||||
window.scrollTo(0, scrollY);
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
const scrollY = parseInt(document.body.style.top || '0') * -1;
|
||||
document.body.style.position = '';
|
||||
document.body.style.top = '';
|
||||
document.body.style.left = '';
|
||||
document.body.style.right = '';
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
if (scrollY > 0) {
|
||||
window.scrollTo(0, scrollY);
|
||||
}
|
||||
};
|
||||
}, [showApplicationForm]);
|
||||
|
||||
const formatSalary = () => {
|
||||
if (job.salary_min && job.salary_max) {
|
||||
return `${job.salary_currency} ${job.salary_min}-${job.salary_max} ${job.salary_period}`;
|
||||
} else if (job.salary_min) {
|
||||
return `From ${job.salary_currency} ${job.salary_min} ${job.salary_period}`;
|
||||
} else if (job.salary_max) {
|
||||
return `Up to ${job.salary_currency} ${job.salary_max} ${job.salary_period}`;
|
||||
}
|
||||
return "Competitive";
|
||||
};
|
||||
|
||||
const scrollToForm = () => {
|
||||
setShowApplicationForm(true);
|
||||
setTimeout(() => {
|
||||
const formElement = document.getElementById('application-form');
|
||||
if (formElement) {
|
||||
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Job Header Banner */}
|
||||
<section className="job-header pt-80 pt-md-100 pt-lg-120 pb-60 pb-md-70 pb-lg-80" style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="job-header-content" style={{ color: '#ffffff' }}>
|
||||
<div className="mb-12 mb-md-16">
|
||||
<span className="badge" style={{
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
color: '#ffffff',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
textTransform: 'none',
|
||||
letterSpacing: '1px',
|
||||
display: 'inline-block'
|
||||
}}>
|
||||
{job.department || 'Career Opportunity'}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="fw-7 mb-16 mb-md-20 mb-lg-24" style={{
|
||||
fontSize: 'clamp(1.75rem, 5vw, 3.5rem)',
|
||||
lineHeight: '1.2',
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
{job.title}
|
||||
</h1>
|
||||
<div className="job-meta d-flex flex-wrap" style={{
|
||||
fontSize: 'clamp(13px, 2vw, 16px)',
|
||||
gap: 'clamp(12px, 2vw, 16px)'
|
||||
}}>
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>location_on</span>
|
||||
<span>{job.location}</span>
|
||||
</div>
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>work</span>
|
||||
<span className="d-none d-sm-inline">{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
|
||||
<span className="d-sm-none">
|
||||
{job.employment_type.split('-')[0].charAt(0).toUpperCase() + job.employment_type.split('-')[0].slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="meta-item d-flex align-items-center">
|
||||
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>group</span>
|
||||
<span className="d-none d-sm-inline">{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'}</span>
|
||||
<span className="d-sm-none">{job.open_positions} {job.open_positions === 1 ? 'Pos' : 'Pos'}</span>
|
||||
</div>
|
||||
{job.experience_required && (
|
||||
<div className="meta-item d-flex align-items-center d-none d-md-flex">
|
||||
<span className="material-symbols-outlined me-2">school</span>
|
||||
<span>{job.experience_required}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Job Content Section */}
|
||||
<section className="job-single pb-80 pb-md-100 pb-lg-120 sticky-wrapper" style={{ marginTop: 'clamp(-30px, -5vw, -40px)' }}>
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap">
|
||||
<div className="col-12 col-lg-8 mb-4 mb-lg-0">
|
||||
<div className="j-d-content" style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 'clamp(8px, 2vw, 12px)',
|
||||
padding: 'clamp(20px, 4vw, 40px)',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.08)'
|
||||
}}>
|
||||
<div className="intro" style={{
|
||||
borderBottom: '2px solid #f0f0f0',
|
||||
paddingBottom: 'clamp(20px, 3vw, 30px)',
|
||||
marginBottom: 'clamp(20px, 3vw, 30px)'
|
||||
}}>
|
||||
<h3 className="fw-6 mb-12 mb-md-16 text-secondary" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
|
||||
About This Position
|
||||
</h3>
|
||||
{job.short_description && (
|
||||
<p style={{
|
||||
color: '#666',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
{job.short_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{job.about_role && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(22px, 4vw, 28px)'
|
||||
}}>info</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
About This Role
|
||||
</h4>
|
||||
</div>
|
||||
<p style={{
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>{job.about_role}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.requirements && job.requirements.length > 0 && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(22px, 4vw, 28px)'
|
||||
}}>task_alt</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
Requirements
|
||||
</h4>
|
||||
</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{job.requirements.map((req, index) => (
|
||||
<li key={index} className="mb-2" style={{
|
||||
paddingLeft: 'clamp(20px, 4vw, 30px)',
|
||||
position: 'relative',
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: 'clamp(6px, 1.5vw, 8px)',
|
||||
width: 'clamp(5px, 1vw, 6px)',
|
||||
height: 'clamp(5px, 1vw, 6px)',
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: '50%'
|
||||
}}></span>
|
||||
{req}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.responsibilities && job.responsibilities.length > 0 && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>assignment</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
Key Responsibilities
|
||||
</h4>
|
||||
</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{job.responsibilities.map((resp, index) => (
|
||||
<li key={index} className="mb-2" style={{
|
||||
paddingLeft: 'clamp(20px, 4vw, 30px)',
|
||||
position: 'relative',
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: 'clamp(6px, 1.5vw, 8px)',
|
||||
width: 'clamp(5px, 1vw, 6px)',
|
||||
height: 'clamp(5px, 1vw, 6px)',
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: '50%'
|
||||
}}></span>
|
||||
{resp}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.qualifications && job.qualifications.length > 0 && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>workspace_premium</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
Qualifications
|
||||
</h4>
|
||||
</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{job.qualifications.map((qual, index) => (
|
||||
<li key={index} className="mb-2" style={{
|
||||
paddingLeft: 'clamp(20px, 4vw, 30px)',
|
||||
position: 'relative',
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: 'clamp(6px, 1.5vw, 8px)',
|
||||
width: 'clamp(5px, 1vw, 6px)',
|
||||
height: 'clamp(5px, 1vw, 6px)',
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: '50%'
|
||||
}}></span>
|
||||
{qual}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.bonus_points && job.bonus_points.length > 0 && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>stars</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
Nice to Have
|
||||
</h4>
|
||||
</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{job.bonus_points.map((bonus, index) => (
|
||||
<li key={index} className="mb-2" style={{
|
||||
paddingLeft: 'clamp(20px, 4vw, 30px)',
|
||||
position: 'relative',
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: 'clamp(6px, 1.5vw, 8px)',
|
||||
width: 'clamp(5px, 1vw, 6px)',
|
||||
height: 'clamp(5px, 1vw, 6px)',
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: '50%'
|
||||
}}></span>
|
||||
{bonus}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{job.benefits && job.benefits.length > 0 && (
|
||||
<div className="group mb-32 mb-md-40">
|
||||
<div className="d-flex align-items-center mb-16 mb-md-20">
|
||||
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>card_giftcard</span>
|
||||
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
|
||||
What We Offer
|
||||
</h4>
|
||||
</div>
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{job.benefits.map((benefit, index) => (
|
||||
<li key={index} className="mb-2" style={{
|
||||
paddingLeft: 'clamp(20px, 4vw, 30px)',
|
||||
position: 'relative',
|
||||
color: '#555',
|
||||
lineHeight: '1.8',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)'
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: 'clamp(6px, 1.5vw, 8px)',
|
||||
width: 'clamp(5px, 1vw, 6px)',
|
||||
height: 'clamp(5px, 1vw, 6px)',
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: '50%'
|
||||
}}></span>
|
||||
{benefit}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-4">
|
||||
<div className="j-d-sidebar" style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 'clamp(8px, 2vw, 12px)',
|
||||
padding: 'clamp(20px, 4vw, 30px)',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
|
||||
position: 'sticky',
|
||||
top: '20px'
|
||||
}}>
|
||||
<div className="intro mb-20 mb-md-30" style={{
|
||||
borderBottom: '2px solid #f0f0f0',
|
||||
paddingBottom: 'clamp(16px, 3vw, 20px)'
|
||||
}}>
|
||||
<span className="text-uppercase" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(11px, 2vw, 12px)',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '2px'
|
||||
}}>
|
||||
JOB DETAILS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<div className="detail-item mb-16 mb-md-24">
|
||||
<div className="d-flex align-items-center mb-6 mb-md-8">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(18px, 3vw, 20px)'
|
||||
}}>payments</span>
|
||||
<p className="fw-6 mb-0" style={{
|
||||
color: '#333',
|
||||
fontSize: 'clamp(14px, 2vw, 15px)'
|
||||
}}>Salary Range</p>
|
||||
</div>
|
||||
<p className="fw-5 mb-0" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(16px, 3vw, 18px)'
|
||||
}}>
|
||||
{formatSalary()}
|
||||
</p>
|
||||
{job.salary_additional && (
|
||||
<p className="mt-6 mt-md-8" style={{
|
||||
color: '#666',
|
||||
fontSize: 'clamp(12px, 2vw, 14px)'
|
||||
}}>
|
||||
{job.salary_additional}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="detail-item mb-16 mb-md-24">
|
||||
<div className="d-flex align-items-center mb-6 mb-md-8">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(18px, 3vw, 20px)'
|
||||
}}>work</span>
|
||||
<p className="fw-6 mb-0" style={{
|
||||
color: '#333',
|
||||
fontSize: 'clamp(14px, 2vw, 15px)'
|
||||
}}>Employment Type</p>
|
||||
</div>
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)'
|
||||
}}>
|
||||
{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="detail-item mb-16 mb-md-24">
|
||||
<div className="d-flex align-items-center mb-6 mb-md-8">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(18px, 3vw, 20px)'
|
||||
}}>location_on</span>
|
||||
<p className="fw-6 mb-0" style={{
|
||||
color: '#333',
|
||||
fontSize: 'clamp(14px, 2vw, 15px)'
|
||||
}}>Location</p>
|
||||
</div>
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)'
|
||||
}}>{job.location}</p>
|
||||
</div>
|
||||
|
||||
<div className="detail-item mb-16 mb-md-24">
|
||||
<div className="d-flex align-items-center mb-6 mb-md-8">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(18px, 3vw, 20px)'
|
||||
}}>event</span>
|
||||
<p className="fw-6 mb-0" style={{
|
||||
color: '#333',
|
||||
fontSize: 'clamp(14px, 2vw, 15px)'
|
||||
}}>Start Date</p>
|
||||
</div>
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)'
|
||||
}}>{job.start_date}</p>
|
||||
</div>
|
||||
|
||||
<div className="detail-item mb-16 mb-md-24">
|
||||
<div className="d-flex align-items-center mb-6 mb-md-8">
|
||||
<span className="material-symbols-outlined me-2" style={{
|
||||
color: '#667eea',
|
||||
fontSize: 'clamp(18px, 3vw, 20px)'
|
||||
}}>groups</span>
|
||||
<p className="fw-6 mb-0" style={{
|
||||
color: '#333',
|
||||
fontSize: 'clamp(14px, 2vw, 15px)'
|
||||
}}>Openings</p>
|
||||
</div>
|
||||
<p style={{
|
||||
color: '#666',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)'
|
||||
}}>
|
||||
{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'} Available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="cta mt-20 mt-md-30">
|
||||
<button
|
||||
onClick={scrollToForm}
|
||||
className="btn w-100 apply-btn"
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
color: '#333',
|
||||
border: '2px solid #667eea',
|
||||
padding: 'clamp(12px, 2vw, 15px) clamp(20px, 4vw, 30px)',
|
||||
fontSize: 'clamp(14px, 2vw, 16px)',
|
||||
fontWeight: '600',
|
||||
borderRadius: 'clamp(6px, 1.5vw, 8px)',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#FFD700';
|
||||
e.currentTarget.style.borderColor = '#FFD700';
|
||||
e.currentTarget.style.color = '#333';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
e.currentTarget.style.borderColor = '#667eea';
|
||||
e.currentTarget.style.color = '#333';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<span className="d-none d-sm-inline">Apply for This Position</span>
|
||||
<span className="d-sm-none">Apply Now</span>
|
||||
</button>
|
||||
<p className="text-center mt-12 mt-md-16" style={{
|
||||
color: '#999',
|
||||
fontSize: 'clamp(11px, 2vw, 13px)'
|
||||
}}>
|
||||
<span className="d-none d-sm-inline">Application takes ~5 minutes</span>
|
||||
<span className="d-sm-none">~5 min</span>
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="/career"
|
||||
className="btn w-100 mt-12 mt-md-16"
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
color: '#667eea',
|
||||
border: '2px solid #e0e0e0',
|
||||
padding: 'clamp(10px, 2vw, 12px) clamp(20px, 4vw, 30px)',
|
||||
fontSize: 'clamp(13px, 2vw, 14px)',
|
||||
fontWeight: '500',
|
||||
borderRadius: 'clamp(6px, 1.5vw, 8px)',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 'clamp(6px, 1.5vw, 8px)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#f5f5f5';
|
||||
e.currentTarget.style.borderColor = '#667eea';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#e0e0e0';
|
||||
}}
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span>
|
||||
<span className="d-none d-sm-inline">Back to Career Page</span>
|
||||
<span className="d-sm-none">Back</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Application Form Modal/Popup */}
|
||||
{showApplicationForm && (
|
||||
<>
|
||||
{/* Backdrop Overlay */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
zIndex: 9998,
|
||||
animation: 'fadeIn 0.3s ease-in-out'
|
||||
}}
|
||||
onClick={() => setShowApplicationForm(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal Container */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="application-form-title"
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 'clamp(10px, 2vw, 20px)',
|
||||
overflow: 'hidden',
|
||||
animation: 'fadeIn 0.3s ease-in-out'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Close when clicking the container background
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowApplicationForm(false);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Close on ESC key
|
||||
if (e.key === 'Escape') {
|
||||
setShowApplicationForm(false);
|
||||
}
|
||||
}}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
setTimeout(() => el.focus(), 100);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
width: '100%',
|
||||
maxWidth: '900px',
|
||||
maxHeight: '90vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
position: 'relative',
|
||||
animation: 'slideUp 0.3s ease-out',
|
||||
outline: 'none',
|
||||
overflow: 'hidden',
|
||||
touchAction: 'none'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onTouchStart={(e) => e.stopPropagation()}
|
||||
onTouchMove={(e) => e.stopPropagation()}
|
||||
>
|
||||
<JobApplicationForm job={job} onClose={() => setShowApplicationForm(false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Animation Styles */}
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobSingle;
|
||||
121
frontEnd/components/pages/career/OpenPosition.tsx
Normal file
121
frontEnd/components/pages/career/OpenPosition.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useJobs } from "@/lib/hooks/useCareer";
|
||||
|
||||
const OpenPosition = () => {
|
||||
const { jobs, loading, error } = useJobs();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center">Loading positions...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center text-danger">Error loading positions. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (jobs.length === 0) {
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
<p className="text-center">No open positions at the moment. Please check back later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="op-position pt-120 pb-120" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro mb-60">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary">
|
||||
Open Positions
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-60">
|
||||
{jobs.map((job, index) => (
|
||||
<div key={job.id} className="op-position-single appear-down">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-sm-2">
|
||||
<span className="fw-7 text-xl text-tertiary">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-12 col-sm-5">
|
||||
<h4 className="fw-7">
|
||||
<Link href={`/career/${job.slug}`}>{job.title}</Link>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="col-12 col-sm-3">
|
||||
<div className="roles">
|
||||
<span className="text-tertiary fw-5 text-xl">
|
||||
({job.open_positions.toString().padStart(2, '0')} Open {job.open_positions === 1 ? 'Role' : 'Roles'})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-2">
|
||||
<div className="cta text-start text-sm-end">
|
||||
<Link href={`/career/${job.slug}`}>
|
||||
<span className="material-symbols-outlined">east</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenPosition;
|
||||
85
frontEnd/components/pages/career/Thrive.tsx
Normal file
85
frontEnd/components/pages/career/Thrive.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import Image from "next/legacy/image";
|
||||
import time from "@/public/images/time.png";
|
||||
import trans from "@/public/images/trans.png";
|
||||
import support from "@/public/images/support.png";
|
||||
import skill from "@/public/images/skill.png";
|
||||
|
||||
const Thrive = () => {
|
||||
return (
|
||||
<section className="thrive pt-120 pb-120 bg-black fade-wrapper">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="intro">
|
||||
<h2 className="mt-8 fw-7 title-anim text-white">
|
||||
What lets us thrive together
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap-lg mt-60">
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={time} alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
Flexible workday
|
||||
</h4>
|
||||
<p className="cur-lg text-quinary">
|
||||
We understand that productivity thrives in balance. Our flexible work arrangements
|
||||
empower you to structure your day for optimal performance while maintaining work-life
|
||||
harmony. Whether remote, hybrid, or on-site, we trust our teams to deliver excellence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={trans} alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
Transparency
|
||||
</h4>
|
||||
<p className="cur-lg text-quinary">
|
||||
Open communication is the foundation of our culture. We believe in clear goal-setting,
|
||||
honest feedback, and accessible leadership. Every team member has visibility into
|
||||
company objectives, strategic decisions, and how their contributions drive enterprise success.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={support} alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
|
||||
<p className="cur-lg text-quinary">
|
||||
Your success is our priority. From comprehensive onboarding to continuous mentorship,
|
||||
we provide enterprise-grade resources, cutting-edge tools, and dedicated support systems.
|
||||
Our collaborative environment ensures you're never alone in tackling complex challenges.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={skill} alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
Growth Skill
|
||||
</h4>
|
||||
<p className="cur-lg text-quinary">
|
||||
Invest in your future with our comprehensive professional development programs. Access
|
||||
industry certifications, advanced training workshops, and leadership development initiatives.
|
||||
We champion continuous learning and provide clear pathways for career advancement.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Thrive;
|
||||
185
frontEnd/components/pages/case-study/CaseItems.tsx
Normal file
185
frontEnd/components/pages/case-study/CaseItems.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudies, useClients } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
|
||||
const CaseItems = () => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
const { caseStudies, loading: casesLoading } = useCaseStudies();
|
||||
const { clients, loading: clientsLoading } = useClients();
|
||||
|
||||
const handleTabClick = (index: number) => {
|
||||
setActiveTabIndex(index);
|
||||
};
|
||||
|
||||
// Filter case studies by category
|
||||
const caseStudiesData = caseStudies.filter((cs) => !cs.client);
|
||||
const clientCaseStudies = caseStudies.filter((cs) => cs.client);
|
||||
|
||||
if (casesLoading || clientsLoading) {
|
||||
return (
|
||||
<section className="fix-top pb-120 c-study">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading case studies...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="fix-top pb-120 c-study">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="c-study-banner pb-120">
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-9">
|
||||
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
|
||||
Case Studies
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
Discover how we help enterprises solve complex challenges with
|
||||
secure, scalable solutions. Our case studies highlight real
|
||||
business outcomes accelerated delivery, reduced costs,
|
||||
improved reliability, and data-driven growth powered by modern
|
||||
cloud, AI, and platform engineering.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="c-study-inner pt-120 mb-60">
|
||||
<div className="c-study-btns">
|
||||
<button
|
||||
className={`study-btn ${
|
||||
activeTabIndex === 0 ? "study-btn-active" : ""
|
||||
}`}
|
||||
onClick={() => handleTabClick(0)}
|
||||
>
|
||||
Case Study
|
||||
</button>
|
||||
<span></span>
|
||||
<button
|
||||
className={`study-btn ${
|
||||
activeTabIndex === 1 ? "study-btn-active" : ""
|
||||
}`}
|
||||
onClick={() => handleTabClick(1)}
|
||||
>
|
||||
Client
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="c-content-wrapper mt-60">
|
||||
<div
|
||||
className={`c-tab-single ${
|
||||
activeTabIndex === 0 ? "active-tab" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
{caseStudiesData.map((caseStudy) => (
|
||||
<div key={caseStudy.id} className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
|
||||
<Image
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
||||
className="w-100 mh-300"
|
||||
alt={caseStudy.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href={`/case-study/${caseStudy.slug}`} className="mb-30 fw-6">
|
||||
{caseStudy.category_name || 'Case Study'}
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href={`/case-study/${caseStudy.slug}`}>
|
||||
{caseStudy.title}
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{caseStudiesData.length === 0 && (
|
||||
<div className="col-12">
|
||||
<p className="text-center">No case studies found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`c-tab-single ${
|
||||
activeTabIndex === 1 ? "active-tab" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
{clientCaseStudies.map((caseStudy, index) => (
|
||||
<div key={caseStudy.id} className="col-12">
|
||||
<div className={`row vertical-column-gap-md align-items-center ${index % 2 === 1 ? 'flex-row-reverse' : ''}`}>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-tab__client">
|
||||
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
|
||||
{caseStudy.client?.name || caseStudy.title}
|
||||
</h2>
|
||||
<p className="cur-lg">
|
||||
{caseStudy.excerpt || caseStudy.client?.description}
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link
|
||||
href={`/case-study/${caseStudy.slug}`}
|
||||
className="btn-anim btn-anim-light"
|
||||
>
|
||||
Read More
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`col-12 col-lg-6 col-xxl-5 ${index % 2 === 0 ? 'offset-xxl-1' : ''}`}>
|
||||
<div className="c-tab__thumb">
|
||||
<Image
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
||||
className="w-100 mh-300"
|
||||
alt={caseStudy.client?.name || caseStudy.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{clientCaseStudies.length === 0 && (
|
||||
<div className="col-12">
|
||||
<p className="text-center">No client case studies found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaseItems;
|
||||
143
frontEnd/components/pages/case-study/CaseSingle.tsx
Normal file
143
frontEnd/components/pages/case-study/CaseSingle.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
import { use } from 'react';
|
||||
import Image from "next/legacy/image";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import poster from "@/public/images/case/poster.png";
|
||||
import project from "@/public/images/case/project.png";
|
||||
import nine from "@/public/images/case/nine.png";
|
||||
|
||||
interface CaseSingleProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const CaseSingle = ({ slug }: CaseSingleProps) => {
|
||||
const { caseStudy, loading, error } = useCaseStudy(slug);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading case study...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !caseStudy) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center text-danger">Case study not found.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="c-details-intro">
|
||||
<h2 className="mt-8 text-secondary title-anim fw-7">
|
||||
{caseStudy.title}
|
||||
</h2>
|
||||
{caseStudy.subtitle && (
|
||||
<h4 className="mt-4 text-secondary">{caseStudy.subtitle}</h4>
|
||||
)}
|
||||
<div className="poster mt-60 fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={caseStudy.poster_image ? getImageUrl(caseStudy.poster_image) : poster}
|
||||
className="w-100 parallax-image mh-300"
|
||||
alt={caseStudy.title}
|
||||
width={1200}
|
||||
height={600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap align-items-center pt-120 pb-120">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="c-details-content">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
Project
|
||||
</h2>
|
||||
{caseStudy.project_overview ? (
|
||||
<p className="cur-lg">{caseStudy.project_overview}</p>
|
||||
) : (
|
||||
<div
|
||||
className="cur-lg"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.description || '' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1 fade-wrapper">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={caseStudy.project_image ? getImageUrl(caseStudy.project_image) : project}
|
||||
className="w-100 parallax-image mh-260"
|
||||
alt={`${caseStudy.title} - Project`}
|
||||
width={600}
|
||||
height={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{caseStudy.site_map_content && (
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="road-map__content">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
|
||||
Site Map
|
||||
</h2>
|
||||
<p className="cur-lg">{caseStudy.site_map_content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.gallery_images && caseStudy.gallery_images.length > 0 && (
|
||||
<div className="row vertical-column-gap mt-60 fade-wrapper">
|
||||
{caseStudy.gallery_images.map((image) => (
|
||||
<div key={image.id} className="col-12 col-sm-6 col-xl-3">
|
||||
<div className="c-details-thumb fade-top">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={getImageUrl(image.image) || nine}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt={image.caption || caseStudy.title}
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaseSingle;
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const CaseStudyInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaseStudyInitAnimations;
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const CaseStudyScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CaseStudyScrollProgressButton;
|
||||
52
frontEnd/components/pages/case-study/Process.tsx
Normal file
52
frontEnd/components/pages/case-study/Process.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
|
||||
interface ProcessProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const Process = ({ slug }: ProcessProps) => {
|
||||
const { caseStudy, loading } = useCaseStudy(slug);
|
||||
|
||||
if (loading || !caseStudy || !caseStudy.process_steps || caseStudy.process_steps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 tp-process bg-black sticky-wrapper">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="process__content sticky-item">
|
||||
<h2 className="mt-8 title-anim text-white fw-7 mb-24">
|
||||
{caseStudy.title} Process
|
||||
</h2>
|
||||
<p className="cur-lg text-quinary">
|
||||
{caseStudy.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1">
|
||||
<div className="process__thumb sticky-item">
|
||||
{caseStudy.process_steps.map((step) => (
|
||||
<div key={step.id} className="process__single">
|
||||
<span className="op-text text-white mb-40 cur-lg">
|
||||
{String(step.step_number).padStart(2, '0')}
|
||||
</span>
|
||||
<h5 className="mt-8 text-white mb-24 title-anim">
|
||||
{step.title}
|
||||
</h5>
|
||||
<p className="cur-lg text-quinary">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Process;
|
||||
67
frontEnd/components/pages/case-study/RelatedCase.tsx
Normal file
67
frontEnd/components/pages/case-study/RelatedCase.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
|
||||
interface RelatedCaseProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const RelatedCase = ({ slug }: RelatedCaseProps) => {
|
||||
const { caseStudy, loading } = useCaseStudy(slug);
|
||||
|
||||
if (loading || !caseStudy || !caseStudy.related_case_studies || caseStudy.related_case_studies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 c-study fade-wrapper">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-9">
|
||||
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
|
||||
Similar Case Studies
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
{caseStudy.related_case_studies.slice(0, 2).map((relatedCase) => (
|
||||
<div key={relatedCase.id} className="col-12 col-lg-6">
|
||||
<div className="c-study-single fade-top">
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="w-100">
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
|
||||
className="w-100 mh-300 parallax-image"
|
||||
alt={relatedCase.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="mb-30 fw-6">
|
||||
{relatedCase.category_name || 'Case Study'}
|
||||
</Link>
|
||||
<h4 className="fw-6 mt-8 text-secondary">
|
||||
<Link href={`/case-study/${relatedCase.slug}`}>
|
||||
{relatedCase.title}
|
||||
</Link>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedCase;
|
||||
65
frontEnd/components/pages/contact/ContactInitAnimations.tsx
Normal file
65
frontEnd/components/pages/contact/ContactInitAnimations.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ContactInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactInitAnimations;
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const ContactScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactScrollProgressButton;
|
||||
602
frontEnd/components/pages/contact/ContactSection.tsx
Normal file
602
frontEnd/components/pages/contact/ContactSection.tsx
Normal file
@@ -0,0 +1,602 @@
|
||||
"use client";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import thumb from "@/public/images/contact-thumb.png";
|
||||
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
|
||||
|
||||
const ContactSection = () => {
|
||||
const pathname = usePathname();
|
||||
const isServiceSingle = pathname === "/service-single" || pathname === "/about-us";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
company: '',
|
||||
jobTitle: '',
|
||||
industry: '',
|
||||
companySize: '',
|
||||
budget: '',
|
||||
timeline: '',
|
||||
projectType: '',
|
||||
message: '',
|
||||
newsletter: false,
|
||||
privacy: false
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<{
|
||||
type: 'success' | 'error' | null;
|
||||
message: string;
|
||||
}>({ type: null, message: '' });
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
if (type === 'checkbox') {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormData(prev => ({ ...prev, [name]: checked }));
|
||||
} else {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus({ type: null, message: '' });
|
||||
|
||||
try {
|
||||
// Transform form data to match API requirements
|
||||
const apiData: ContactFormData = {
|
||||
first_name: formData.firstName,
|
||||
last_name: formData.lastName,
|
||||
email: formData.email,
|
||||
phone: formData.phone || undefined,
|
||||
company: formData.company,
|
||||
job_title: formData.jobTitle,
|
||||
industry: formData.industry || undefined,
|
||||
company_size: formData.companySize || undefined,
|
||||
project_type: formData.projectType || undefined,
|
||||
timeline: formData.timeline || undefined,
|
||||
budget: formData.budget || undefined,
|
||||
message: formData.message,
|
||||
newsletter_subscription: formData.newsletter,
|
||||
privacy_consent: formData.privacy
|
||||
};
|
||||
|
||||
// Submit to Django API
|
||||
const response = await contactApiService.submitContactForm(apiData);
|
||||
|
||||
setSubmitStatus({
|
||||
type: 'success',
|
||||
message: response.message || 'Thank you! We\'ll contact you within 24 hours.'
|
||||
});
|
||||
|
||||
// Reset form on success
|
||||
setFormData({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
company: '',
|
||||
jobTitle: '',
|
||||
industry: '',
|
||||
companySize: '',
|
||||
budget: '',
|
||||
timeline: '',
|
||||
projectType: '',
|
||||
message: '',
|
||||
newsletter: false,
|
||||
privacy: false
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
setSubmitStatus({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : 'Failed to submit form. Please try again.'
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
"tp-contact pb-120 fade-wrapper" +
|
||||
(isServiceSingle ? " pt-120" : " fix-top")
|
||||
}
|
||||
>
|
||||
<div className="container">
|
||||
{/* Contact Information Cards */}
|
||||
<div className="row mb-40">
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="contact-info-card">
|
||||
<div className="contact-info-icon">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
</div>
|
||||
<h4>Phone Support</h4>
|
||||
<p>Main Contact & Emergency</p>
|
||||
<a href="tel:+359897338147">+359 897 338 147</a>
|
||||
<a href="tel:+359896138030">Emergency: +359 896 13 80 30</a>
|
||||
<span className="contact-hours">Available 24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="contact-info-card">
|
||||
<div className="contact-info-icon">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
</div>
|
||||
<h4>Email Support</h4>
|
||||
<p>Software Solutions</p>
|
||||
<a href="mailto:info@gnxsoft.com">info@gnxsoft.com</a>
|
||||
<a href="mailto:contact@gnxsoft.com">contact@gnxsoft.com</a>
|
||||
<span className="contact-hours">24/7 Response Time</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="contact-info-card">
|
||||
<div className="contact-info-icon">
|
||||
<i className="fa-solid fa-map-marker-alt"></i>
|
||||
</div>
|
||||
<h4>Office Locations</h4>
|
||||
<p>Our Location</p>
|
||||
<address>
|
||||
GNX Soft Ltd.<br />
|
||||
Tsar Simeon I, 56<br />
|
||||
Burgas, Burgas 8000, Bulgaria
|
||||
</address>
|
||||
<span className="contact-hours">Schedule a Visit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row vertical-column-gap-md justify-content-between mt-40">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="tp-contact__content">
|
||||
|
||||
<div className="contact-form mt-40">
|
||||
<form onSubmit={handleSubmit} className="enterprise-form">
|
||||
<div className="form-section">
|
||||
<h4 className="form-section-title">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
Personal Information
|
||||
</h4>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="firstName">First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
id="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="John"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="lastName">Last Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
id="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Smith"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="email">Business Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="john.smith@company.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="phone">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
id="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h4 className="form-section-title">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
Company Information
|
||||
</h4>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="company">Company Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
id="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Acme Corporation"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="jobTitle">Job Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="jobTitle"
|
||||
id="jobTitle"
|
||||
value={formData.jobTitle}
|
||||
onChange={handleInputChange}
|
||||
placeholder="CTO, IT Director, etc."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="industry">Industry</label>
|
||||
<select
|
||||
name="industry"
|
||||
id="industry"
|
||||
value={formData.industry}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Industry</option>
|
||||
<option value="technology">Technology</option>
|
||||
<option value="finance">Finance</option>
|
||||
<option value="healthcare">Healthcare</option>
|
||||
<option value="manufacturing">Manufacturing</option>
|
||||
<option value="retail">Retail</option>
|
||||
<option value="education">Education</option>
|
||||
<option value="government">Government</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="companySize">Company Size</label>
|
||||
<select
|
||||
name="companySize"
|
||||
id="companySize"
|
||||
value={formData.companySize}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Company Size</option>
|
||||
<option value="1-10">1-10 employees</option>
|
||||
<option value="11-50">11-50 employees</option>
|
||||
<option value="51-200">51-200 employees</option>
|
||||
<option value="201-1000">201-1000 employees</option>
|
||||
<option value="1000+">1000+ employees</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h4 className="form-section-title">
|
||||
<i className="fa-solid fa-project-diagram"></i>
|
||||
Project Details
|
||||
</h4>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="projectType">Project Type</label>
|
||||
<select
|
||||
name="projectType"
|
||||
id="projectType"
|
||||
value={formData.projectType}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Project Type</option>
|
||||
<option value="software-development">Software Development</option>
|
||||
<option value="cloud-migration">Cloud Migration</option>
|
||||
<option value="digital-transformation">Digital Transformation</option>
|
||||
<option value="data-analytics">Data Analytics</option>
|
||||
<option value="security-compliance">Security & Compliance</option>
|
||||
<option value="integration">System Integration</option>
|
||||
<option value="consulting">Consulting Services</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="timeline">Project Timeline</label>
|
||||
<select
|
||||
name="timeline"
|
||||
id="timeline"
|
||||
value={formData.timeline}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Timeline</option>
|
||||
<option value="immediate">Immediate (0-3 months)</option>
|
||||
<option value="short">Short-term (3-6 months)</option>
|
||||
<option value="medium">Medium-term (6-12 months)</option>
|
||||
<option value="long">Long-term (12+ months)</option>
|
||||
<option value="planning">Still planning</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="input-single">
|
||||
<label htmlFor="budget">Project Budget Range</label>
|
||||
<select
|
||||
name="budget"
|
||||
id="budget"
|
||||
value={formData.budget}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Budget Range</option>
|
||||
<option value="under-50k">Under €50,000</option>
|
||||
<option value="50k-100k">€50,000 - €100,000</option>
|
||||
<option value="100k-250k">€100,000 - €250,000</option>
|
||||
<option value="250k-500k">€250,000 - €500,000</option>
|
||||
<option value="500k-1m">€500,000 - €1,000,000</option>
|
||||
<option value="over-1m">Over €1,000,000</option>
|
||||
<option value="discuss">Prefer to discuss</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="input-single">
|
||||
<label htmlFor="message">Project Description *</label>
|
||||
<textarea
|
||||
name="message"
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Please describe your project requirements, current challenges, and expected outcomes..."
|
||||
rows={5}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h4 className="form-section-title">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
Privacy & Communication
|
||||
</h4>
|
||||
<div className="checkbox-group">
|
||||
<div className="checkbox-single">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="newsletter"
|
||||
id="newsletter"
|
||||
checked={formData.newsletter}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<label htmlFor="newsletter">
|
||||
Subscribe to our newsletter for industry insights and product updates
|
||||
</label>
|
||||
</div>
|
||||
<div className="checkbox-single">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="privacy"
|
||||
id="privacy"
|
||||
checked={formData.privacy}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="privacy">
|
||||
I agree to the <a href="/policy?type=privacy">Privacy Policy</a> and consent to being contacted by our team *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{submitStatus.type && (
|
||||
<div className={`form-status mt-30 ${submitStatus.type === 'success' ? 'success' : 'error'}`}>
|
||||
<div className="status-content">
|
||||
<i className={`fa-solid ${submitStatus.type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}`}></i>
|
||||
<span>{submitStatus.message}</span>
|
||||
{submitStatus.type === 'error' && (
|
||||
<div className="error-details mt-10">
|
||||
<small>
|
||||
If the problem persists, please contact us directly at{' '}
|
||||
<a href="mailto:info@gnxsoft.com" className="text-primary">info@gnxsoft.com</a>
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-actions mt-40">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary enterprise-btn"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span>Sending...</span>
|
||||
<i className="fa-solid fa-spinner fa-spin"></i>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Request Software Consultation</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<p className="form-disclaimer">
|
||||
<i className="fa-solid fa-lock"></i>
|
||||
Your information is secure and will only be used to provide you with relevant software solutions.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-contact__sidebar">
|
||||
<div className="contact-sidebar-card">
|
||||
<div className="sidebar-header">
|
||||
<h3>Why Choose Our Software Solutions?</h3>
|
||||
<div className="enterprise-badge">
|
||||
<i className="fa-solid fa-award"></i>
|
||||
<span>Enterprise Grade</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-features">
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<h4>Incident Management</h4>
|
||||
<p>Advanced incident management software with real-time monitoring and automated response capabilities.</p>
|
||||
<div className="feature-tags">
|
||||
<span className="tag">Real-time</span>
|
||||
<span className="tag">Automated</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-users"></i>
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<h4>Custom Development</h4>
|
||||
<p>Tailored software solutions built to your exact specifications with dedicated development teams.</p>
|
||||
<div className="feature-tags">
|
||||
<span className="tag">Dedicated Teams</span>
|
||||
<span className="tag">Custom</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<h4>System Integrations & APIs</h4>
|
||||
<p>Seamless integration with your existing systems and third-party applications through robust APIs for unified workflows.</p>
|
||||
<div className="feature-tags">
|
||||
<span className="tag">API-First</span>
|
||||
<span className="tag">Seamless</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-cogs"></i>
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<h4>Data Replication</h4>
|
||||
<p>Reliable data replication and synchronization solutions to ensure data consistency across systems.</p>
|
||||
<div className="feature-tags">
|
||||
<span className="tag">Reliable</span>
|
||||
<span className="tag">Sync</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-sidebar-card">
|
||||
<h3>Next Steps</h3>
|
||||
<div className="next-steps">
|
||||
<div className="step-item">
|
||||
<div className="step-number">1</div>
|
||||
<div className="step-content">
|
||||
<h4>Initial Consultation</h4>
|
||||
<p>We'll review your requirements and provide initial recommendations within 24 hours.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-item">
|
||||
<div className="step-number">2</div>
|
||||
<div className="step-content">
|
||||
<h4>Custom Demo</h4>
|
||||
<p>Schedule a personalized demonstration tailored to your specific use case.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-item">
|
||||
<div className="step-number">3</div>
|
||||
<div className="step-content">
|
||||
<h4>Proposal & Pricing</h4>
|
||||
<p>Receive a detailed proposal with custom pricing and implementation timeline.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-sidebar-card">
|
||||
<h3>Find Us</h3>
|
||||
<div className="company-map">
|
||||
<div className="map-container">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2933.123456789!2d27.4758968970689!3d42.496781103070504!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x40a6b8c9d1234567%3A0x1234567890abcdef!2sBurgas%2C%20Bulgaria!5e0!3m2!1sen!2sbg!4v1234567890123!5m2!1sen!2sbg"
|
||||
width="100%"
|
||||
height="200"
|
||||
style={{ border: 0, borderRadius: '8px' }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Company Location"
|
||||
/>
|
||||
</div>
|
||||
<div className="map-info">
|
||||
<div className="location-details">
|
||||
<div className="location-icon">
|
||||
<i className="fa-solid fa-map-marker-alt"></i>
|
||||
</div>
|
||||
<div className="location-text">
|
||||
<h4>GNX Soft Ltd.</h4>
|
||||
<p>Tsar Simeon I, 56<br />Burgas, Burgas 8000, Bulgaria</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="location-actions">
|
||||
<a href="https://maps.google.com/?q=Tsar+Simeon+I+56+Burgas+Bulgaria" target="_blank" rel="noopener noreferrer" className="map-link">
|
||||
<i className="fa-solid fa-directions"></i>
|
||||
Get Directions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSection;
|
||||
433
frontEnd/components/pages/home/HomeBanner.tsx
Normal file
433
frontEnd/components/pages/home/HomeBanner.tsx
Normal file
@@ -0,0 +1,433 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const HomeBanner = () => {
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState(0);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
// Fix viewport height for mobile browsers (especially iOS Safari)
|
||||
useEffect(() => {
|
||||
const setVH = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
setVH();
|
||||
window.addEventListener('resize', setVH);
|
||||
// Use 'resize' event instead of deprecated 'orientationchange'
|
||||
// The resize event fires on orientation change as well
|
||||
const handleOrientationChange = () => {
|
||||
// Small delay to ensure viewport has updated
|
||||
setTimeout(setVH, 100);
|
||||
};
|
||||
window.addEventListener('resize', handleOrientationChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', setVH);
|
||||
window.removeEventListener('resize', handleOrientationChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Static banner slides data based on actual services
|
||||
const carouselTexts = [
|
||||
{
|
||||
id: 1,
|
||||
badge: "Custom Development",
|
||||
icon: "fa-solid fa-code",
|
||||
heading: "Tailored Enterprise Software ",
|
||||
highlight: "Development",
|
||||
subheading: "Aligned with Your Business Goals",
|
||||
description: "We design and build custom digital solutions that deliver reliable, scalable, and future-ready applications, driving measurable value and competitive advantage for your enterprise.",
|
||||
button_text: "Explore Solutions",
|
||||
button_url: "/services/custom-software-development",
|
||||
is_active: true,
|
||||
display_order: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
badge: "Business Intelligence",
|
||||
icon: "fa-solid fa-brain",
|
||||
heading: "AI-Powered ",
|
||||
highlight: "Analytics",
|
||||
subheading: "Transform Data into Insights",
|
||||
description: "Turn enterprise data into actionable intelligence with advanced AI and machine learning, enabling smarter decisions, performance optimization, and data-driven innovation.",
|
||||
button_text: "Discover AI Solutions",
|
||||
button_url: "/services/ai-powered-business-intelligence",
|
||||
is_active: true,
|
||||
display_order: 2,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
badge: "System Integration",
|
||||
icon: "fa-solid fa-plug",
|
||||
heading: "Enterprise Systems ",
|
||||
highlight: "Integration",
|
||||
subheading: "Seamless Connectivity",
|
||||
description: "Connect everything — from payment systems and ERP platforms to cloud services, enabling your enterprise to operate seamlessly across physical and digital environments.",
|
||||
button_text: "View Integrations",
|
||||
button_url: "/services/external-systems-integrations",
|
||||
is_active: true,
|
||||
display_order: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
badge: "Incident Management",
|
||||
icon: "fa-solid fa-bell",
|
||||
heading: "Intelligent Incident ",
|
||||
highlight: "Management",
|
||||
subheading: "Minimize Downtime & Protect Trust",
|
||||
description: "Cloud-based incident management that empowers teams to detect, respond, and resolve issues faster, reducing downtime and maintaining customer confidence.",
|
||||
button_text: "Learn More",
|
||||
button_url: "/services/incident-management-saas",
|
||||
is_active: true,
|
||||
display_order: 4,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Carousel rotation effect
|
||||
useEffect(() => {
|
||||
if (carouselTexts.length <= 1) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setIsTransitioning(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentTextIndex((prevIndex) =>
|
||||
prevIndex === carouselTexts.length - 1 ? 0 : prevIndex + 1
|
||||
);
|
||||
setIsTransitioning(false);
|
||||
}, 1000); // Slightly longer for smoother transition
|
||||
}, 6000); // Increased interval for slower changes
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [carouselTexts.length]);
|
||||
|
||||
const currentText = carouselTexts[currentTextIndex];
|
||||
|
||||
if (!currentText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="modern-banner">
|
||||
<div className="banner-background">
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Industrial Enterprise Background Elements */}
|
||||
<div className="enterprise-bg-elements">
|
||||
{/* Flying Code Elements */}
|
||||
<div className="flying-code">
|
||||
<div className="code-snippet code-1">
|
||||
<span className="code-line">const enterprise = {'{'}</span>
|
||||
<span className="code-line"> security: 'enterprise-grade',</span>
|
||||
<span className="code-line"> scalability: 'unlimited'</span>
|
||||
<span className="code-line">{'}'};</span>
|
||||
</div>
|
||||
<div className="code-snippet code-2">
|
||||
<span className="code-line">if (security === 'max') {'{'}</span>
|
||||
<span className="code-line"> deploy.enterprise();</span>
|
||||
<span className="code-line">{'}'}</span>
|
||||
</div>
|
||||
<div className="code-snippet code-3">
|
||||
<span className="code-line">class EnterpriseSoftware {'{'}</span>
|
||||
<span className="code-line"> constructor() {'{'}</span>
|
||||
<span className="code-line"> this.secure = true;</span>
|
||||
<span className="code-line"> {'}'}</span>
|
||||
<span className="code-line">{'}'}</span>
|
||||
</div>
|
||||
<div className="code-snippet code-4">
|
||||
<span className="code-line">API.authenticate({'{'}</span>
|
||||
<span className="code-line"> level: 'enterprise',</span>
|
||||
<span className="code-line"> encryption: 'AES-256'</span>
|
||||
<span className="code-line">{'}'});</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industrial Grid */}
|
||||
<div className="industrial-grid">
|
||||
<div className="grid-line horizontal h-1"></div>
|
||||
<div className="grid-line horizontal h-2"></div>
|
||||
<div className="grid-line horizontal h-3"></div>
|
||||
<div className="grid-line horizontal h-4"></div>
|
||||
<div className="grid-line vertical v-1"></div>
|
||||
<div className="grid-line vertical v-2"></div>
|
||||
<div className="grid-line vertical v-3"></div>
|
||||
<div className="grid-line vertical v-4"></div>
|
||||
</div>
|
||||
|
||||
{/* Security Elements */}
|
||||
<div className="security-elements">
|
||||
<div className="shield shield-1">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div className="shield shield-2">
|
||||
<i className="fa-solid fa-lock"></i>
|
||||
</div>
|
||||
<div className="shield shield-3">
|
||||
<i className="fa-solid fa-key"></i>
|
||||
</div>
|
||||
<div className="shield shield-4">
|
||||
<i className="fa-solid fa-fingerprint"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Circuit Patterns */}
|
||||
<div className="circuit-patterns">
|
||||
<div className="circuit circuit-1">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
<div className="circuit circuit-2">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
<div className="circuit circuit-3">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Streams */}
|
||||
<div className="data-streams">
|
||||
<div className="stream stream-1">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
<div className="stream stream-2">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
<div className="stream stream-3">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Request/Response Data */}
|
||||
<div className="request-response-data">
|
||||
<div className="api-request req-1">
|
||||
<div className="request-label">POST /api/enterprise</div>
|
||||
<div className="request-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-response resp-1">
|
||||
<div className="response-label">200 OK</div>
|
||||
<div className="response-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-request req-2">
|
||||
<div className="request-label">GET /api/analytics</div>
|
||||
<div className="request-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-response resp-2">
|
||||
<div className="response-label">201 Created</div>
|
||||
<div className="response-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Space Data Generation */}
|
||||
<div className="space-data-generation">
|
||||
<div className="data-cluster cluster-1">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-2">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-3">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-4">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Connections */}
|
||||
<div className="database-connections">
|
||||
<div className="db-connection conn-1">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-database"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
<div className="db-connection conn-2">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-server"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
<div className="db-connection conn-3">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-cloud"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Real-time Metrics */}
|
||||
<div className="real-time-metrics">
|
||||
<div className="metric metric-1">
|
||||
<div className="metric-label">API Calls/sec</div>
|
||||
<div className="metric-value">2,847</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric metric-2">
|
||||
<div className="metric-label">Data Processed</div>
|
||||
<div className="metric-value">15.2TB</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric metric-3">
|
||||
<div className="metric-label">Active Users</div>
|
||||
<div className="metric-value">45,892</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="banner-content">
|
||||
<div className="content-center">
|
||||
<div className="badge-container">
|
||||
<span className="badge">
|
||||
<i className={currentText.icon}></i>
|
||||
{currentText.badge}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className={`main-heading carousel-text ${isTransitioning ? 'fade-out' : 'fade-in'}`}>
|
||||
{currentText.heading}
|
||||
<span className="gradient-text"> {currentText.highlight}</span>
|
||||
<br />
|
||||
{currentText.subheading}
|
||||
</h1>
|
||||
|
||||
<p className={`description carousel-text ${isTransitioning ? 'fade-out' : 'fade-in'}`}>
|
||||
{currentText.description}
|
||||
</p>
|
||||
|
||||
{/* Carousel Indicators */}
|
||||
<div className="carousel-indicators">
|
||||
{carouselTexts.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`indicator ${index === currentTextIndex ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (index !== currentTextIndex) {
|
||||
setIsTransitioning(true);
|
||||
setTimeout(() => {
|
||||
setCurrentTextIndex(index);
|
||||
setIsTransitioning(false);
|
||||
}, 1000);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cta-section">
|
||||
<Link href={currentText.button_url || "#"} className="cta-primary">
|
||||
<span>{currentText.button_text || "Learn More"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
<Link href="contact-us" className="cta-secondary">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>Contact Sales</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="trust-indicators">
|
||||
<div className="trust-item">
|
||||
<div className="trust-number">30+</div>
|
||||
<div className="trust-label">Enterprise Clients</div>
|
||||
</div>
|
||||
<div className="trust-item">
|
||||
<div className="trust-number">99.9%</div>
|
||||
<div className="trust-label">Uptime SLA</div>
|
||||
</div>
|
||||
<div className="trust-item">
|
||||
<div className="trust-number">24/7</div>
|
||||
<div className="trust-label">Enterprise Support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div className="scroll-indicator" onClick={() => window.scrollTo({ top: window.innerHeight, behavior: 'smooth' })}>
|
||||
<div className="scroll-text">Scroll to explore</div>
|
||||
<div className="scroll-arrow">
|
||||
<i className="fa-solid fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeBanner;
|
||||
65
frontEnd/components/pages/home/HomeInitAnimations.tsx
Normal file
65
frontEnd/components/pages/home/HomeInitAnimations.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const HomeInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeInitAnimations;
|
||||
123
frontEnd/components/pages/home/HomeLatestPost.tsx
Normal file
123
frontEnd/components/pages/home/HomeLatestPost.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Autoplay } from "swiper/modules";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import { useLatestPosts } from "@/lib/hooks/useBlog";
|
||||
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
|
||||
const HomeLatestPost = () => {
|
||||
const { posts, loading, error } = useLatestPosts(12); // Get 12 latest posts
|
||||
|
||||
return (
|
||||
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap align-items-center">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="tp-lp-title text-center text-lg-start">
|
||||
<h2 className="mt-8 fw-7 text-secondary title-anim">
|
||||
Latest Insights
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-lp-cta text-center text-lg-end">
|
||||
<Link href="/insights" className="btn-line text-uppercase">
|
||||
View All Insights
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="tp-lp-slider-wrapper mt-60">
|
||||
{loading ? (
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="text-tertiary mt-3">Loading insights...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-danger">Error loading insights. Please try again later.</p>
|
||||
</div>
|
||||
) : posts.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-tertiary">No insights available yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tp-lp-slider-wrapper">
|
||||
<Swiper
|
||||
slidesPerView={"auto"}
|
||||
spaceBetween={24}
|
||||
slidesPerGroup={1}
|
||||
freeMode={true}
|
||||
speed={1200}
|
||||
loop={posts.length > 3}
|
||||
roundLengths={true}
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
delay: 5000,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true,
|
||||
}}
|
||||
className="tp-lp-slider"
|
||||
>
|
||||
{posts.map((post) => (
|
||||
<SwiperSlide key={post.id}>
|
||||
<div className="tp-lp-slider__single topy-tilt">
|
||||
<div className="thumb mb-24">
|
||||
<Link
|
||||
href={`/insights/${post.slug}`}
|
||||
className="w-100 overflow-hidden d-block"
|
||||
>
|
||||
<div className="parallax-image-wrap">
|
||||
<div className="parallax-image-inner">
|
||||
<Image
|
||||
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
|
||||
width={400}
|
||||
height={220}
|
||||
className="w-100 mh-220 parallax-image"
|
||||
alt={getValidImageAlt(post.title)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="tp-lp-post__meta mb-24 mt-8">
|
||||
<p className="author text-xs text-tertiary">
|
||||
{post.author_name || 'Admin'}
|
||||
</p>
|
||||
<span></span>
|
||||
<p className="date text-xs text-tertiary">
|
||||
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h5 className="mt-8 fw-5 text-secondary">
|
||||
<Link href={`/insights/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeLatestPost;
|
||||
67
frontEnd/components/pages/home/HomeScrollProgressButton.tsx
Normal file
67
frontEnd/components/pages/home/HomeScrollProgressButton.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const HomeScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeScrollProgressButton;
|
||||
121
frontEnd/components/pages/home/Overview.tsx
Normal file
121
frontEnd/components/pages/home/Overview.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { useServices } from "@/lib/hooks/useServices";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
import one from "@/public/images/overview/one.png";
|
||||
import two from "@/public/images/overview/two.png";
|
||||
import three from "@/public/images/overview/three.png";
|
||||
import four from "@/public/images/overview/four.png";
|
||||
import five from "@/public/images/overview/five.png";
|
||||
|
||||
// Default images array for fallback
|
||||
const defaultImages = [one, two, three, four, five];
|
||||
|
||||
const Overview = () => {
|
||||
// Memoize the parameters to prevent infinite re-renders
|
||||
const serviceParams = useMemo(() => ({
|
||||
ordering: 'display_order',
|
||||
page: 1
|
||||
}), []);
|
||||
|
||||
// Fetch services from API, limit to 5 for overview section
|
||||
const { services, loading, error } = useServices(serviceParams);
|
||||
|
||||
// Get first 5 services for the overview
|
||||
const displayServices = services.slice(0, 5);
|
||||
return (
|
||||
<section
|
||||
className="tp-overview pt-120 pb-120 sticky-wrapper"
|
||||
id="scroll-to"
|
||||
>
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-overview__content sticky-item">
|
||||
<h2 className="fw-7 text-secondary title-anim mb-30 mt-8">
|
||||
Enterprise Solutions That Scale
|
||||
</h2>
|
||||
<p className="mt-8 cur-lg">
|
||||
Our enterprise software development teams deliver mission-critical solutions
|
||||
for Fortune 500 companies. We design, build, and deploy scalable systems
|
||||
that transform your business operations, enhance security, and drive digital
|
||||
innovation across your organization.
|
||||
</p>
|
||||
<div className="mt-40">
|
||||
<Link href="services" className="btn-line text-uppercase">
|
||||
EXPLORE SOLUTIONS
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1">
|
||||
<div className="tp-overview__items sticky-item">
|
||||
{loading ? (
|
||||
// Loading state
|
||||
<div className="text-center py-5">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3 text-muted">Loading services...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
// Error state
|
||||
<div className="text-center py-5">
|
||||
<p className="text-danger">Failed to load services. Please try again later.</p>
|
||||
</div>
|
||||
) : displayServices.length === 0 ? (
|
||||
// No services state
|
||||
<div className="text-center py-5">
|
||||
<p className="text-muted">No services available at the moment.</p>
|
||||
</div>
|
||||
) : (
|
||||
// Services list
|
||||
displayServices.map((service, index) => {
|
||||
// Always use hardcoded images, not API images
|
||||
const serviceImage = defaultImages[index] || defaultImages[0];
|
||||
|
||||
return (
|
||||
<div key={service.id} className="tp-overview-single appear-down">
|
||||
<div className="thumb">
|
||||
<Image
|
||||
src={serviceImage}
|
||||
width={80}
|
||||
height={80}
|
||||
alt={service.title}
|
||||
/>
|
||||
</div>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
<h4 className="mt-8 mb-12 fw-6 text-secondary">
|
||||
<Link href={`/services/${service.slug}`}>
|
||||
{service.title}
|
||||
</Link>
|
||||
</h4>
|
||||
<p className="text-tertiary">
|
||||
{service.short_description || service.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="cta">
|
||||
<Link href={`/services/${service.slug}`}>
|
||||
<span className="material-symbols-outlined">
|
||||
call_made
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
78
frontEnd/components/pages/home/ServiceIntro.tsx
Normal file
78
frontEnd/components/pages/home/ServiceIntro.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import thumb from "@/public/images/leading.jpg";
|
||||
|
||||
const ServiceIntro = () => {
|
||||
return (
|
||||
<section className="tp-service pt-120 pb-120">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap-md">
|
||||
<div className="col-12 col-lg-4">
|
||||
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
|
||||
<Link href="services">
|
||||
<Image
|
||||
src={thumb}
|
||||
alt="Enterprise Software Solutions"
|
||||
width={400}
|
||||
height={500}
|
||||
objectFit="cover"
|
||||
style={{ display: 'block', border: 'none', margin: 0, padding: 0 }}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-8">
|
||||
<div className="tp-service__content">
|
||||
<div className="tp-section-wrapper">
|
||||
|
||||
<h2 className="title-anim text-secondary fw-7 mb-30">
|
||||
Accelerating Digital Transformation Through<br />
|
||||
Mission-Critical Enterprise Software
|
||||
</h2>
|
||||
</div>
|
||||
<div className="pl-50">
|
||||
<p className="cur-lg mb-25">
|
||||
GNX partners with Fortune 40 companies and global enterprises to architect,
|
||||
develop, and deploy business-critical software solutions that drive measurable
|
||||
results. Our engineering teams deliver secure, scalable, and compliant systems
|
||||
that power digital innovation across industries.
|
||||
</p>
|
||||
<p className="cur-lg mb-30">
|
||||
From cloud-native architectures and enterprise integration platforms to
|
||||
AI-powered analytics and legacy modernization, we provide end-to-end
|
||||
technology solutions that reduce operational costs, enhance efficiency,
|
||||
and deliver competitive advantage.
|
||||
</p>
|
||||
<div className="tp-service__features mb-40">
|
||||
<ul className="list-unstyled">
|
||||
<li className="mb-15">
|
||||
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||
<span className="fw-6">Enterprise-Grade Security & Compliance</span>
|
||||
</li>
|
||||
<li className="mb-15">
|
||||
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||
<span className="fw-6">Scalable Cloud-Native Architectures</span>
|
||||
</li>
|
||||
<li className="mb-15">
|
||||
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||
<span className="fw-6">24/7 Support & Dedicated Engineering Teams</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-60">
|
||||
<Link href="services" className="btn-anim btn-anim-light">
|
||||
Explore Our Solutions
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceIntro;
|
||||
175
frontEnd/components/pages/home/Story.tsx
Normal file
175
frontEnd/components/pages/home/Story.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
const Story = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
||||
|
||||
// Fetch case studies from API with ordering and limit
|
||||
const params = useMemo(() => ({
|
||||
ordering: 'display_order',
|
||||
page_size: 5
|
||||
}), []);
|
||||
|
||||
const { caseStudies, loading, error } = useCaseStudies(params);
|
||||
|
||||
// Fallback to static data if API fails or is loading
|
||||
const staticStoryData = [
|
||||
{
|
||||
id: 1,
|
||||
category_name: "Financial Services",
|
||||
title: "Banking System Modernization",
|
||||
excerpt: "Complete digital transformation of legacy banking systems with enhanced security and real-time processing capabilities.",
|
||||
thumbnail: "/images/case/one.png",
|
||||
slug: "banking-system-modernization",
|
||||
display_order: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category_name: "Healthcare",
|
||||
title: "Patient Management System",
|
||||
excerpt: "Enterprise-grade patient management system with HIPAA compliance and seamless integration across multiple healthcare facilities.",
|
||||
thumbnail: "/images/case/two.png",
|
||||
slug: "patient-management-system",
|
||||
display_order: 2,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category_name: "Manufacturing",
|
||||
title: "Supply Chain Optimization",
|
||||
excerpt: "Advanced supply chain management system with real-time tracking, predictive analytics, and automated inventory management.",
|
||||
thumbnail: "/images/case/three.png",
|
||||
slug: "supply-chain-optimization",
|
||||
display_order: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category_name: "E-commerce",
|
||||
title: "Multi-Platform Integration",
|
||||
excerpt: "Seamless integration of multiple e-commerce platforms with unified inventory management and real-time synchronization.",
|
||||
thumbnail: "/images/case/four.png",
|
||||
slug: "multi-platform-integration",
|
||||
display_order: 4,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category_name: "Education",
|
||||
title: "Learning Management System",
|
||||
excerpt: "Comprehensive LMS with advanced analytics, automated grading, and seamless integration with existing educational tools.",
|
||||
thumbnail: "/images/case/five.png",
|
||||
slug: "learning-management-system",
|
||||
display_order: 5,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// Use API data if available, otherwise use static data
|
||||
const storyData = caseStudies.length > 0 ? caseStudies : staticStoryData;
|
||||
|
||||
// Log when API data is loaded
|
||||
useEffect(() => {
|
||||
if (caseStudies.length > 0) {
|
||||
}
|
||||
}, [caseStudies]);
|
||||
|
||||
const handleMouseEnter = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
setActiveImageIndex(index);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
|
||||
<div className="container">
|
||||
<div className="row vertical-column-gap-md">
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-story__content sticky-item">
|
||||
<h2 className="mt-8 title-anim text-white fw-7">
|
||||
Enterprise Case Studies
|
||||
</h2>
|
||||
<div className="tp-story__items">
|
||||
{storyData.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`tp-story__single fade-top ${
|
||||
index === activeIndex ? "active" : ""
|
||||
}`}
|
||||
onMouseEnter={() => handleMouseEnter(index)}
|
||||
>
|
||||
<p className="fw-6 mt-8">
|
||||
<Link href={`/case-study/${item.slug}`}>
|
||||
{item.category_name || "Case Study"}
|
||||
</Link>
|
||||
</p>
|
||||
<h5 className="fw-4 mt-12 mb-12 text-white">
|
||||
{item.title}
|
||||
</h5>
|
||||
<p className="text-xs">{item.excerpt}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1 d-none d-lg-block">
|
||||
<div className="tp-story__thumbs sticky-item">
|
||||
{storyData.map((item, index) => {
|
||||
// Get the image URL - handle different scenarios
|
||||
let imageUrl;
|
||||
if (item.thumbnail) {
|
||||
if (item.thumbnail.startsWith('http')) {
|
||||
// Full URL (external)
|
||||
imageUrl = item.thumbnail;
|
||||
} else if (item.thumbnail.startsWith('/media')) {
|
||||
// Relative path starting with /media
|
||||
imageUrl = `${API_CONFIG.BASE_URL}${item.thumbnail}`;
|
||||
} else {
|
||||
// Just filename or relative path
|
||||
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
|
||||
}
|
||||
} else {
|
||||
// Fallback to static image
|
||||
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`tp-story-thumb ${
|
||||
index === activeImageIndex ? "thumb-active" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
width={600}
|
||||
height={300}
|
||||
className="w-100 mh-300"
|
||||
alt={item.title || "Case Study"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Story;
|
||||
104
frontEnd/components/pages/services/ServiceDeliverables.tsx
Normal file
104
frontEnd/components/pages/services/ServiceDeliverables.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServiceDeliverablesProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServiceDeliverables = ({ service }: ServiceDeliverablesProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Modern entrance animations
|
||||
gsap.set(".deliverable-item", {
|
||||
y: 60,
|
||||
opacity: 0,
|
||||
scale: 0.9,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".deliverable-item", {
|
||||
start: "-150px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
stagger: {
|
||||
amount: 0.6,
|
||||
from: "start"
|
||||
},
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
}),
|
||||
});
|
||||
|
||||
// Animate section header
|
||||
gsap.fromTo(".section-header",
|
||||
{
|
||||
y: 40,
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: ".section-header",
|
||||
start: "-100px bottom"
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!service.deliverables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deliverablesList = service.deliverables
|
||||
.split(/[,;•\n]/)
|
||||
.map(item => item.trim())
|
||||
.filter(item => item.length > 0);
|
||||
|
||||
return (
|
||||
<section className="enterprise-deliverables py-4">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="section-header text-center mb-4">
|
||||
<span className="enterprise-section-tag">What You Get</span>
|
||||
<h2 className="enterprise-section-title mb-3">
|
||||
What You Get with {service.title}
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
{service.deliverables_description || `Our comprehensive ${service.title.toLowerCase()} service includes everything you need for success`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="row g-5">
|
||||
{deliverablesList.map((deliverable, index) => (
|
||||
<div key={index} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="deliverable-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-check-circle"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">
|
||||
{deliverable}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceDeliverables;
|
||||
125
frontEnd/components/pages/services/ServiceDetails.tsx
Normal file
125
frontEnd/components/pages/services/ServiceDetails.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Image from "next/legacy/image";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServiceDetailsProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServiceDetails = ({ service }: ServiceDetailsProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Animate content on scroll
|
||||
gsap.set(".detail-content", {
|
||||
x: -50,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
gsap.set(".detail-image", {
|
||||
x: 50,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".detail-content, .detail-image", {
|
||||
start: "-100px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="service-details" className="enterprise-details py-5">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="detail-content">
|
||||
<div className="section-header mb-5">
|
||||
<span className="enterprise-section-tag">About Service</span>
|
||||
<h2 className="enterprise-section-title mb-4">
|
||||
About Our {service.title}
|
||||
</h2>
|
||||
{service.short_description && (
|
||||
<p className="enterprise-section-description">
|
||||
{service.short_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="enterprise-stats mb-4">
|
||||
<div className="row g-3">
|
||||
<div className="col-12">
|
||||
<div className="enterprise-stat-card">
|
||||
<div className="stat-icon">
|
||||
<i className="fa-solid fa-star"></i>
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">
|
||||
{service.featured ? 'Featured' : 'Standard'}
|
||||
</div>
|
||||
<div className="stat-label">Service Type</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="enterprise-meta mb-4">
|
||||
<div className="meta-item">
|
||||
<div className="meta-icon">
|
||||
<i className="fa-solid fa-tag"></i>
|
||||
</div>
|
||||
<div className="meta-content">
|
||||
<span className="meta-label">Category</span>
|
||||
<span className="meta-value">{service.category?.name || 'General'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{service.duration && (
|
||||
<div className="meta-item">
|
||||
<div className="meta-icon">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
</div>
|
||||
<div className="meta-content">
|
||||
<span className="meta-label">Duration</span>
|
||||
<span className="meta-value">{service.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="detail-image">
|
||||
<div className="enterprise-image-wrapper">
|
||||
<Image
|
||||
src={serviceUtils.getServiceImageUrl(service) || '/images/service/default.png'}
|
||||
alt={service.title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="enterprise-service-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceDetails;
|
||||
102
frontEnd/components/pages/services/ServiceFeatures.tsx
Normal file
102
frontEnd/components/pages/services/ServiceFeatures.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import { Service, ServiceFeature } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServiceFeaturesProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServiceFeatures = ({ service }: ServiceFeaturesProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Modern entrance animations
|
||||
gsap.set(".feature-item", {
|
||||
y: 60,
|
||||
opacity: 0,
|
||||
scale: 0.9,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".feature-item", {
|
||||
start: "-150px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
stagger: {
|
||||
amount: 0.6,
|
||||
from: "start"
|
||||
},
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
}),
|
||||
});
|
||||
|
||||
// Animate section header
|
||||
gsap.fromTo(".section-header",
|
||||
{
|
||||
y: 40,
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: ".section-header",
|
||||
start: "-100px bottom"
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!service.features || service.features.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="enterprise-features py-4">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="section-header text-center mb-4">
|
||||
<span className="enterprise-section-tag">Key Features</span>
|
||||
<h2 className="enterprise-section-title mb-3">
|
||||
{service.title} Features
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
{service.features_description || `Discover the key features that make our ${service.title.toLowerCase()} service stand out from the competition`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="row g-5">
|
||||
{service.features.map((feature: ServiceFeature, index: number) => (
|
||||
<div key={feature.id} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="feature-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className={`fa-solid fa-${feature.icon || 'check'}`}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">
|
||||
{feature.title}
|
||||
</h6>
|
||||
<p className="card-description">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceFeatures;
|
||||
155
frontEnd/components/pages/services/ServiceMain.tsx
Normal file
155
frontEnd/components/pages/services/ServiceMain.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import { useServices } from "@/lib/hooks/useServices";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
|
||||
const ServiceMain = () => {
|
||||
// Fetch services from API
|
||||
const { services, loading, error } = useServices();
|
||||
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3">Loading services...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4>Error Loading Services</h4>
|
||||
<p>{error}</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show empty state
|
||||
if (!services || services.length === 0) {
|
||||
return (
|
||||
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center">
|
||||
<h3>No Services Available</h3>
|
||||
<p>We're working on adding new services. Please check back later.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="enterprise-services py-5" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="section-header text-center mb-5">
|
||||
<span className="enterprise-section-tag">Our Services</span>
|
||||
<h2 className="enterprise-section-title mb-4">
|
||||
Professional Software Development Services
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
We deliver comprehensive technology solutions tailored to your business needs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row g-4">
|
||||
{services.map((service, index) => (
|
||||
<div key={service.id} className="col-12 col-lg-6 col-xl-4">
|
||||
<div className="enterprise-service-card">
|
||||
<div className="service-image-wrapper">
|
||||
<Link href={`/services/${service.slug}`}>
|
||||
<Image
|
||||
src={getValidImageUrl(serviceUtils.getServiceImageUrl(service), FALLBACK_IMAGES.SERVICE)}
|
||||
alt={service.title}
|
||||
width={400}
|
||||
height={300}
|
||||
className="service-image"
|
||||
/>
|
||||
</Link>
|
||||
<div className="service-overlay">
|
||||
<Link href={`/services/${service.slug}`} className="btn btn-primary">
|
||||
<span>Learn More</span>
|
||||
<i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="service-content">
|
||||
<div className="service-meta">
|
||||
<span className="service-number">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{service.category && (
|
||||
<span className="service-category">
|
||||
{service.category.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="service-title">
|
||||
<Link href={`/services/${service.slug}`}>
|
||||
{service.title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="service-description">
|
||||
{service.short_description || service.description}
|
||||
</p>
|
||||
|
||||
<div className="service-footer">
|
||||
<Link href={`/services/${service.slug}`} className="service-link">
|
||||
<span>View Details</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceMain;
|
||||
151
frontEnd/components/pages/services/ServicePricing.tsx
Normal file
151
frontEnd/components/pages/services/ServicePricing.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Link from "next/link";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServicePricingProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServicePricing = ({ service }: ServicePricingProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Animate pricing section
|
||||
gsap.set(".pricing-content", {
|
||||
y: 50,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: ".pricing-content",
|
||||
start: "-100px bottom",
|
||||
onEnter: () =>
|
||||
gsap.to(".pricing-content", {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="enterprise-pricing py-5">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="row">
|
||||
<div className="col-12 col-lg-8 offset-lg-2">
|
||||
<div className="section-header text-center mb-5">
|
||||
<span className="enterprise-section-tag">Pricing & Packages</span>
|
||||
<h2 className="enterprise-section-title mb-4">
|
||||
Pricing & Packages
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
Get started with our {service.title.toLowerCase()} service.
|
||||
Contact us for a customized quote based on your specific requirements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pricing-content text-center">
|
||||
|
||||
<div className="enterprise-pricing-card">
|
||||
<div className="pricing-header">
|
||||
<div className="pricing-badge">
|
||||
<span>Most Popular</span>
|
||||
</div>
|
||||
<h3 className="pricing-title">
|
||||
{service.title}
|
||||
</h3>
|
||||
<div className="price-display">
|
||||
<span className="price-period">
|
||||
Contact Us for Pricing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pricing-features">
|
||||
<ul className="enterprise-feature-list">
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</div>
|
||||
<span>Custom {service.title.toLowerCase()} solution</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</div>
|
||||
<span>Professional consultation</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</div>
|
||||
<span>Ongoing support & maintenance</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</div>
|
||||
<span>Quality assurance & testing</span>
|
||||
</li>
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-check"></i>
|
||||
</div>
|
||||
<span>Documentation & training</span>
|
||||
</li>
|
||||
{service.duration && (
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
</div>
|
||||
<span>Project duration: {service.duration}</span>
|
||||
</li>
|
||||
)}
|
||||
{service.technologies && (
|
||||
<li className="feature-item">
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-code"></i>
|
||||
</div>
|
||||
<span>Latest technologies & frameworks</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="pricing-cta">
|
||||
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
|
||||
<span>Get Free Quote</span>
|
||||
<i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</Link>
|
||||
<Link href="/services" className="btn btn-outline-secondary btn-lg mb-3">
|
||||
<span>View All Services</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pricing-note">
|
||||
<div className="note-content">
|
||||
<i className="fa-solid fa-info-circle"></i>
|
||||
<p>
|
||||
Final pricing depends on project scope, complexity, and specific requirements.
|
||||
Contact us for a detailed proposal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicePricing;
|
||||
130
frontEnd/components/pages/services/ServiceProcess.tsx
Normal file
130
frontEnd/components/pages/services/ServiceProcess.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServiceProcessProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServiceProcess = ({ service }: ServiceProcessProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Modern entrance animations
|
||||
gsap.set(".process-step", {
|
||||
y: 60,
|
||||
opacity: 0,
|
||||
scale: 0.9,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".process-step", {
|
||||
start: "-150px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
stagger: {
|
||||
amount: 0.6,
|
||||
from: "start"
|
||||
},
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
}),
|
||||
});
|
||||
|
||||
// Animate section header
|
||||
gsap.fromTo(".section-header",
|
||||
{
|
||||
y: 40,
|
||||
opacity: 0
|
||||
},
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration: 1,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: ".section-header",
|
||||
start: "-100px bottom"
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (!service.process_steps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Split process steps by common separators
|
||||
const processSteps = service.process_steps
|
||||
.split(/[,;•\n]/)
|
||||
.map(step => step.trim())
|
||||
.filter(step => step.length > 0);
|
||||
|
||||
return (
|
||||
<section className="enterprise-process py-4">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="section-header text-center mb-4">
|
||||
<span className="enterprise-section-tag">Our Process</span>
|
||||
<h2 className="enterprise-section-title mb-3">
|
||||
{service.title} Process
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
{service.process_description || `Our proven methodology ensures successful delivery of your ${service.title.toLowerCase()} project.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="row g-5">
|
||||
{processSteps.map((step, index) => (
|
||||
<div key={index} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="process-step enterprise-process-step-compact">
|
||||
<div className="step-number">
|
||||
<span className="step-number-text">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<h6 className="step-title">
|
||||
{step}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to generate step descriptions based on step name and service
|
||||
const getStepDescription = (step: string, serviceTitle: string): string => {
|
||||
const descriptions: { [key: string]: string } = {
|
||||
'Requirements Analysis': 'We analyze your business requirements and technical specifications to ensure we understand your goals.',
|
||||
'System Design': 'Our team creates a comprehensive system architecture and design that meets your specific needs.',
|
||||
'Development': 'We implement the solution using industry best practices and modern technologies.',
|
||||
'Testing': 'Rigorous testing ensures your solution is reliable, secure, and performs optimally.',
|
||||
'Deployment': 'We handle the deployment process and ensure smooth transition to production.',
|
||||
'Training': 'We provide comprehensive training to your team for successful adoption.',
|
||||
'API Planning': 'We design the API architecture and define endpoints based on your integration needs.',
|
||||
'API Development': 'We build robust, scalable APIs using modern frameworks and best practices.',
|
||||
'Documentation': 'Comprehensive API documentation ensures easy integration and maintenance.',
|
||||
'Integration': 'We integrate the API with your existing systems and third-party services.',
|
||||
'Assessment': 'We evaluate your current infrastructure and identify migration opportunities.',
|
||||
'Migration Planning': 'We create a detailed migration strategy with minimal downtime.',
|
||||
'Implementation': 'We execute the migration plan with careful monitoring and rollback procedures.',
|
||||
'Optimization': 'We optimize your cloud infrastructure for performance and cost efficiency.',
|
||||
'Support': 'Ongoing support and maintenance ensure your solution continues to perform optimally.'
|
||||
};
|
||||
|
||||
return descriptions[step] || `This step involves ${step.toLowerCase()} to ensure the success of your ${serviceTitle.toLowerCase()} project.`;
|
||||
};
|
||||
|
||||
export default ServiceProcess;
|
||||
113
frontEnd/components/pages/services/ServicesBanner.tsx
Normal file
113
frontEnd/components/pages/services/ServicesBanner.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
const ServicesBanner = () => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
if (document.querySelector(".service-banner")) {
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".service-banner",
|
||||
start: "center center",
|
||||
end: "+=100%",
|
||||
scrub: true,
|
||||
pin: false,
|
||||
},
|
||||
});
|
||||
|
||||
tl.to(".thumb-one", {
|
||||
opacity: 0.3,
|
||||
y: "-100%",
|
||||
zIndex: -1,
|
||||
duration: 2,
|
||||
});
|
||||
|
||||
tl.to(
|
||||
".thumb-two",
|
||||
{
|
||||
opacity: 0.3,
|
||||
scale: 2,
|
||||
y: "100%",
|
||||
zIndex: -1,
|
||||
duration: 2,
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="enterprise-banner position-relative overflow-hidden">
|
||||
<div className="banner-background">
|
||||
<div className="gradient-overlay"></div>
|
||||
<div className="geometric-pattern"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="enterprise-banner__content">
|
||||
<div className="banner-badge mb-4">
|
||||
<span className="enterprise-badge">
|
||||
Enterprise Services
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="enterprise-title mb-4">
|
||||
The end-to-end bespoke software development company you need
|
||||
</h1>
|
||||
|
||||
<p className="enterprise-description mb-5">
|
||||
Empower your business with sophisticated tech solutions by GNX.
|
||||
From ideation to implementation, with a team of experts right at your fingertips
|
||||
</p>
|
||||
|
||||
<div className="enterprise-cta">
|
||||
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
|
||||
<span>Get Free Quote</span>
|
||||
<i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</Link>
|
||||
<Link href="#scroll-to" className="btn btn-outline-light btn-lg mb-3">
|
||||
<span>Explore Services</span>
|
||||
<i className="fa-solid fa-arrow-down ms-2"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<Link href="#scroll-to" className="scroll-to">
|
||||
Scroll
|
||||
<span className="arrow"></span>
|
||||
</Link>
|
||||
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesBanner;
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ButtonHoverAnimation = dynamic(
|
||||
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const VanillaTiltHover = dynamic(
|
||||
() => import("../../shared/layout/animations/VanillaTiltHover"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const SplitTextAnimations = dynamic(
|
||||
() => import("../../shared/layout/animations/SplitTextAnimations"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ServicesInitAnimations = () => {
|
||||
return (
|
||||
<>
|
||||
<SmoothScroll />
|
||||
<ParallaxImage />
|
||||
<FadeImageBottom />
|
||||
<ButtonHoverAnimation />
|
||||
<VanillaTiltHover />
|
||||
<SplitTextAnimations />
|
||||
<ScrollToElement />
|
||||
<AppearDown />
|
||||
<FadeAnimations />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesInitAnimations;
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const ServicesScrollProgressButton = () => {
|
||||
useEffect(() => {
|
||||
window.scroll(0, 0);
|
||||
}, []);
|
||||
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const scrollRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
const totalHeight = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = (window.scrollY / totalHeight) * 100;
|
||||
setScrollProgress(progress);
|
||||
setIsActive(window.scrollY > 50);
|
||||
};
|
||||
|
||||
const handleProgressClick = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={scrollRef}
|
||||
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
|
||||
onClick={handleProgressClick}
|
||||
title="Go To Top"
|
||||
>
|
||||
<span></span>
|
||||
<svg
|
||||
className="progress-circle svg-content"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="-1 -1 102 102"
|
||||
>
|
||||
<path
|
||||
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
|
||||
stroke="#3887FE"
|
||||
strokeWidth="4"
|
||||
fill="none"
|
||||
style={{
|
||||
strokeDasharray: "308.66px",
|
||||
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesScrollProgressButton;
|
||||
220
frontEnd/components/pages/services/Transform.tsx
Normal file
220
frontEnd/components/pages/services/Transform.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import gsap from "gsap";
|
||||
import ScrollTrigger from "gsap/dist/ScrollTrigger";
|
||||
import thumb from "@/public/images/transform-thumb.png";
|
||||
import teamThumb from "@/public/images/team-thumb.png";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
|
||||
interface TransformProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const Transform = ({ service }: TransformProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
gsap.set(".foot-fade", {
|
||||
x: -100,
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
ScrollTrigger.batch(".foot-fade", {
|
||||
start: "-100px bottom",
|
||||
onEnter: (elements) =>
|
||||
gsap.to(elements, {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
stagger: 0.3,
|
||||
}),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="enterprise-transform py-5" id="scroll-to">
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="row align-items-center mb-5">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="transform__content">
|
||||
<div className="section-header mb-4">
|
||||
<span className="enterprise-section-tag">Transform Your Business</span>
|
||||
<h2 className="enterprise-section-title mb-4">
|
||||
Transform your business with {service.title}
|
||||
</h2>
|
||||
<p className="enterprise-section-description">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="transform__thumb">
|
||||
<div className="enterprise-image-wrapper">
|
||||
<Image
|
||||
src={serviceUtils.getServiceImageUrl(service) || thumb}
|
||||
className="enterprise-service-image"
|
||||
alt={service.title}
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section-header text-center mb-4">
|
||||
<span className="enterprise-section-tag">Why Choose Us</span>
|
||||
<h2 className="enterprise-section-title mb-3">
|
||||
{service.why_choose_description || `Why choose our ${service.title.toLowerCase()}?`}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="row g-5">
|
||||
{service.features && service.features.length > 0 ? (
|
||||
service.features.slice(0, 6).map((feature, index) => (
|
||||
<div key={feature.id} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="benefit-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className={`fa-solid fa-${feature.icon || 'check'}`}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">{feature.title}</h6>
|
||||
<p className="card-description">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
// Fallback content if no features are available
|
||||
<>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="benefit-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-rocket"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Fast Delivery</h6>
|
||||
<p className="card-description">
|
||||
{service.duration ? `Delivered in ${service.duration}` : 'Quick turnaround times for your project needs'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="benefit-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Quality Assured</h6>
|
||||
<p className="card-description">
|
||||
Rigorous testing and quality control ensure reliable, robust solutions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="benefit-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Ongoing Support</h6>
|
||||
<p className="card-description">
|
||||
Comprehensive support and maintenance to ensure optimal performance
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="section-header text-center mb-4 mt-6">
|
||||
<span className="enterprise-section-tag">Our Expertise</span>
|
||||
<h2 className="enterprise-section-title mb-3">
|
||||
Our Expertise
|
||||
</h2>
|
||||
{service.expertise_description && (
|
||||
<p className="enterprise-section-description">
|
||||
{service.expertise_description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row g-5">
|
||||
{service.expertise_items && service.expertise_items.length > 0 ? (
|
||||
service.expertise_items.slice(0, 3).map((expertise, index) => (
|
||||
<div key={expertise.id} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="expertise-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className={`fa-solid fa-${expertise.icon || 'star'}`}></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">{expertise.title}</h6>
|
||||
<p className="card-description">
|
||||
{expertise.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
// Fallback content if no expertise items are available
|
||||
<>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="expertise-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-users"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Expert Team</h6>
|
||||
<p className="card-description">
|
||||
Our experienced team specializes in {service.title.toLowerCase()} with years of industry expertise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="expertise-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-code"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Modern Technologies</h6>
|
||||
<p className="card-description">
|
||||
We use cutting-edge technologies and frameworks to deliver scalable, secure solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="expertise-item enterprise-section-card">
|
||||
<div className="card-icon">
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
<h6 className="card-title">Proven Results</h6>
|
||||
<p className="card-description">
|
||||
We have successfully delivered solutions to numerous clients, helping them achieve their goals.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transform;
|
||||
470
frontEnd/components/pages/support/CreateTicketForm.tsx
Normal file
470
frontEnd/components/pages/support/CreateTicketForm.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
interface CreateTicketFormProps {
|
||||
onOpenStatusCheck?: () => void;
|
||||
}
|
||||
|
||||
const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
const { categories, loading: categoriesLoading } = useTicketCategories();
|
||||
|
||||
const [formData, setFormData] = useState<CreateTicketData>({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value
|
||||
}));
|
||||
// Clear field error when user starts typing
|
||||
if (fieldErrors[name]) {
|
||||
setFieldErrors(prev => ({ ...prev, [name]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!formData.user_name.trim()) {
|
||||
errors.user_name = 'Full name is required';
|
||||
}
|
||||
|
||||
if (!formData.user_email.trim()) {
|
||||
errors.user_email = 'Email address is required';
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.user_email)) {
|
||||
errors.user_email = 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
if (!formData.title.trim()) {
|
||||
errors.title = 'Subject is required';
|
||||
}
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
errors.description = 'Description is required';
|
||||
} else if (formData.description.trim().length < 10) {
|
||||
errors.description = 'Please provide a more detailed description (minimum 10 characters)';
|
||||
}
|
||||
|
||||
setFieldErrors(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitError(null);
|
||||
setSubmitSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await createTicket(formData);
|
||||
setTicketNumber(response.ticket_number);
|
||||
setSubmitSuccess(true);
|
||||
|
||||
// Reset form
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
ticket_type: 'general',
|
||||
user_name: '',
|
||||
user_email: '',
|
||||
user_phone: '',
|
||||
company: '',
|
||||
category: undefined
|
||||
});
|
||||
setFieldErrors({});
|
||||
} catch (error: any) {
|
||||
console.error('Ticket creation error:', error);
|
||||
|
||||
// Provide user-friendly error messages based on error type
|
||||
let errorMessage = 'Failed to submit ticket. Please try again.';
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('email')) {
|
||||
errorMessage = 'There was an issue with your email address. Please check and try again.';
|
||||
} else if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = 'Network error. Please check your connection and try again.';
|
||||
} else if (error.message.includes('validation')) {
|
||||
errorMessage = 'Please check all required fields and try again.';
|
||||
} else if (error.message.includes('server') || error.message.includes('500')) {
|
||||
errorMessage = 'Server error. Our team has been notified. Please try again later.';
|
||||
} else {
|
||||
// Use the actual error message if it's user-friendly
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitError(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
<h3>Ticket Created Successfully!</h3>
|
||||
<div className="ticket-number-container">
|
||||
<div className="ticket-number-label">Your Ticket Number</div>
|
||||
<div className="ticket-number-value">{ticketNumber}</div>
|
||||
<button
|
||||
className="btn-copy-ticket"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ticketNumber);
|
||||
const btn = document.querySelector('.btn-copy-ticket');
|
||||
if (btn) {
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.textContent = 'Copy';
|
||||
}, 2000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p className="ticket-info">
|
||||
We've received your support request and will respond as soon as possible.
|
||||
Please save your ticket number for future reference.
|
||||
</p>
|
||||
<div className="success-actions">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setSubmitSuccess(false)}
|
||||
>
|
||||
<i className="fa-solid fa-plus me-2"></i>
|
||||
Submit Another Ticket
|
||||
</button>
|
||||
{onOpenStatusCheck && (
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={onOpenStatusCheck}
|
||||
>
|
||||
<i className="fa-solid fa-search me-2"></i>
|
||||
Check Ticket Status
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const issueTypeIcons: Record<string, string> = {
|
||||
general: 'fa-info-circle',
|
||||
technical: 'fa-tools',
|
||||
billing: 'fa-credit-card',
|
||||
feature_request: 'fa-lightbulb',
|
||||
bug_report: 'fa-bug',
|
||||
account: 'fa-user-circle'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="create-ticket-form enterprise-form">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12">
|
||||
<div className="form-header-enterprise">
|
||||
<div className="form-header-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h2>Submit a Support Ticket</h2>
|
||||
<p>Fill out the form below and our dedicated support team will get back to you within 24 hours.</p>
|
||||
<div className="info-banner">
|
||||
<i className="fa-solid fa-shield-check"></i>
|
||||
<div>
|
||||
<strong>Secure & Confidential</strong>
|
||||
<span>All tickets are encrypted and handled with enterprise-grade security standards.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<div>
|
||||
<strong>Submission Error</strong>
|
||||
<p>{submitError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
{/* Personal Information */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
<h3>Personal Information</h3>
|
||||
</div>
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_name" className="form-label-enterprise">
|
||||
<span>Full Name</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="user_name"
|
||||
name="user_name"
|
||||
value={formData.user_name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.user_name ? 'error' : ''}`}
|
||||
placeholder="Enter your full name"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.user_name && (
|
||||
<span className="field-error">{fieldErrors.user_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_email" className="form-label-enterprise">
|
||||
<span>Email Address</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
<input
|
||||
type="email"
|
||||
id="user_email"
|
||||
name="user_email"
|
||||
value={formData.user_email}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.user_email ? 'error' : ''}`}
|
||||
placeholder="your.email@company.com"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.user_email && (
|
||||
<span className="field-error">{fieldErrors.user_email}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="user_phone" className="form-label-enterprise">
|
||||
<span>Phone Number</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<input
|
||||
type="tel"
|
||||
id="user_phone"
|
||||
name="user_phone"
|
||||
value={formData.user_phone}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="company" className="form-label-enterprise">
|
||||
<span>Company Name</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="company"
|
||||
name="company"
|
||||
value={formData.company}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise"
|
||||
placeholder="Your Company Inc."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ticket Details */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<i className="fa-solid fa-clipboard-list"></i>
|
||||
<h3>Ticket Details</h3>
|
||||
</div>
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="ticket_type" className="form-label-enterprise">
|
||||
<span>Issue Type</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="select-with-icon">
|
||||
<i className={`fa-solid ${issueTypeIcons[formData.ticket_type] || 'fa-tag'}`}></i>
|
||||
<select
|
||||
id="ticket_type"
|
||||
name="ticket_type"
|
||||
value={formData.ticket_type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="form-control-enterprise select-enterprise"
|
||||
>
|
||||
<option value="general">General Inquiry</option>
|
||||
<option value="technical">Technical Issue</option>
|
||||
<option value="billing">Billing Question</option>
|
||||
<option value="feature_request">Feature Request</option>
|
||||
<option value="bug_report">Bug Report</option>
|
||||
<option value="account">Account Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="category" className="form-label-enterprise">
|
||||
<span>Category</span>
|
||||
<span className="optional-badge">Optional</span>
|
||||
</label>
|
||||
<div className="select-with-icon">
|
||||
<i className="fa-solid fa-folder"></i>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control-enterprise select-enterprise"
|
||||
disabled={categoriesLoading}
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{Array.isArray(categories) && categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="title" className="form-label-enterprise">
|
||||
<span>Subject</span>
|
||||
<span className="required-badge">Required</span>
|
||||
</label>
|
||||
<div className="input-with-icon">
|
||||
<i className="fa-solid fa-heading"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise ${fieldErrors.title ? 'error' : ''}`}
|
||||
placeholder="Brief, descriptive subject line"
|
||||
/>
|
||||
</div>
|
||||
{fieldErrors.title && (
|
||||
<span className="field-error">{fieldErrors.title}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="form-group-enterprise">
|
||||
<label htmlFor="description" className="form-label-enterprise">
|
||||
<span>Description</span>
|
||||
<span className="required-badge">Required</span>
|
||||
<span className="char-count">{formData.description.length} characters</span>
|
||||
</label>
|
||||
<div className="textarea-wrapper">
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className={`form-control-enterprise textarea-enterprise ${fieldErrors.description ? 'error' : ''}`}
|
||||
rows={6}
|
||||
placeholder="Please provide detailed information about your issue, including any error messages, steps to reproduce, or relevant context..."
|
||||
/>
|
||||
<div className="textarea-footer">
|
||||
<i className="fa-solid fa-lightbulb"></i>
|
||||
<span>Tip: More details help us resolve your issue faster</span>
|
||||
</div>
|
||||
</div>
|
||||
{fieldErrors.description && (
|
||||
<span className="field-error">{fieldErrors.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Section */}
|
||||
<div className="form-submit-section">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-submit-enterprise"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="spinner-enterprise"></span>
|
||||
<span>Submitting Ticket...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-paper-plane"></i>
|
||||
<span>Submit Ticket</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<p className="submit-note">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
Average response time: <strong>2-4 hours</strong>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTicketForm;
|
||||
|
||||
217
frontEnd/components/pages/support/KnowledgeBase.tsx
Normal file
217
frontEnd/components/pages/support/KnowledgeBase.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
|
||||
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
|
||||
|
||||
const KnowledgeBase = () => {
|
||||
const { categories, loading: categoriesLoading } = useKnowledgeBaseCategories();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
|
||||
|
||||
// Fetch all articles (for browsing and category filtering)
|
||||
const { articles: allArticles, loading: allArticlesLoading } = useKnowledgeBaseArticles();
|
||||
|
||||
// Fetch featured articles (for default view)
|
||||
const { articles: featuredArticles, loading: featuredLoading } = useFeaturedArticles();
|
||||
|
||||
// Determine which articles to display
|
||||
let displayArticles = featuredArticles;
|
||||
let isLoading = featuredLoading;
|
||||
let headerText = 'Featured Articles';
|
||||
|
||||
if (searchTerm) {
|
||||
// If searching, filter all articles by search term
|
||||
displayArticles = allArticles.filter(article =>
|
||||
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
isLoading = allArticlesLoading;
|
||||
headerText = 'Search Results';
|
||||
} else if (selectedCategory) {
|
||||
// If a category is selected, filter articles by that category
|
||||
displayArticles = allArticles.filter(article => article.category_slug === selectedCategory);
|
||||
isLoading = allArticlesLoading;
|
||||
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
|
||||
headerText = `${categoryName} Articles`;
|
||||
}
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// The search is already being performed by the hook
|
||||
};
|
||||
|
||||
const filteredCategories = selectedCategory
|
||||
? categories.filter(cat => cat.slug === selectedCategory)
|
||||
: categories;
|
||||
|
||||
return (
|
||||
<div className="knowledge-base">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10">
|
||||
<div className="form-header text-center">
|
||||
<h2>Knowledge Base</h2>
|
||||
<p>Find answers to frequently asked questions and explore our documentation.</p>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<form onSubmit={handleSearch} className="kb-search-form">
|
||||
<div className="search-input-group">
|
||||
<i className="fa-solid fa-search search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search articles, topics, or keywords..."
|
||||
className="form-control"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-search"
|
||||
onClick={() => setSearchTerm('')}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Categories */}
|
||||
{!searchTerm && (
|
||||
<div className="kb-categories">
|
||||
<h3>Browse by Category</h3>
|
||||
{categoriesLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row g-4">
|
||||
{Array.isArray(categories) && categories.map(category => (
|
||||
<div key={category.id} className="col-md-6 col-lg-4">
|
||||
<div
|
||||
className="category-card"
|
||||
onClick={() => setSelectedCategory(category.slug)}
|
||||
style={{ borderLeftColor: category.color }}
|
||||
>
|
||||
<div
|
||||
className="category-icon"
|
||||
style={{ color: category.color }}
|
||||
>
|
||||
<i className={`fa-solid ${category.icon}`}></i>
|
||||
</div>
|
||||
<div className="category-content">
|
||||
<h4>{category.name}</h4>
|
||||
<p>{category.description}</p>
|
||||
<div className="category-meta">
|
||||
<span className="article-count">
|
||||
{category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured/Search Results Articles */}
|
||||
<div className="kb-articles">
|
||||
<div className="articles-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3>{headerText}</h3>
|
||||
{selectedCategory && !searchTerm && (
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
Back to All Articles
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="search-info">
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : displayArticles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<i className="fa-solid fa-search empty-icon"></i>
|
||||
<h4>No articles found</h4>
|
||||
<p>
|
||||
{searchTerm
|
||||
? `We couldn't find any articles matching "${searchTerm}". Try different keywords.`
|
||||
: 'No articles available at the moment.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="articles-list">
|
||||
{Array.isArray(displayArticles) && displayArticles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
className="article-item"
|
||||
onClick={() => setSelectedArticleSlug(article.slug)}
|
||||
>
|
||||
<div className="article-header">
|
||||
<h4>{article.title}</h4>
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="article-summary">{article.summary}</p>
|
||||
<div className="article-meta">
|
||||
<span className="article-category">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
<span className="article-stats">
|
||||
<i className="fa-solid fa-thumbs-up me-1"></i>
|
||||
{article.helpful_count} helpful
|
||||
</span>
|
||||
</div>
|
||||
<button className="article-read-more">
|
||||
Read More <i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Article Modal */}
|
||||
{selectedArticleSlug && (
|
||||
<KnowledgeBaseArticleModal
|
||||
slug={selectedArticleSlug}
|
||||
onClose={() => setSelectedArticleSlug(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBase;
|
||||
|
||||
138
frontEnd/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
138
frontEnd/components/pages/support/KnowledgeBaseArticleModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
|
||||
import { markArticleHelpful } from '@/lib/api/supportService';
|
||||
|
||||
interface KnowledgeBaseArticleModalProps {
|
||||
slug: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalProps) => {
|
||||
const { article, loading, error } = useKnowledgeBaseArticle(slug);
|
||||
const [feedbackGiven, setFeedbackGiven] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent body scroll when modal is open
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFeedback = async (helpful: boolean) => {
|
||||
if (!article || feedbackGiven) return;
|
||||
|
||||
try {
|
||||
await markArticleHelpful(slug, helpful);
|
||||
setFeedbackGiven(true);
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="kb-modal-overlay" onClick={onClose}>
|
||||
<div className="kb-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="modal-close" onClick={onClose} aria-label="Close modal">
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
<div className="modal-content">
|
||||
{loading && (
|
||||
<div className="loading-state">
|
||||
<div className="spinner-border" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p>Loading article...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-state">
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<h3>Error Loading Article</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{article && (
|
||||
<>
|
||||
<div className="article-header">
|
||||
{article.is_featured && (
|
||||
<span className="featured-badge">
|
||||
<i className="fa-solid fa-star me-1"></i>
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<h2>{article.title}</h2>
|
||||
<div className="article-meta">
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-folder me-1"></i>
|
||||
{article.category_name}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-calendar me-1"></i>
|
||||
{formatDate(article.published_at || article.created_at)}
|
||||
</span>
|
||||
<span className="meta-item">
|
||||
<i className="fa-solid fa-eye me-1"></i>
|
||||
{article.view_count} views
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="article-body">
|
||||
<div
|
||||
className="article-content"
|
||||
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="article-footer">
|
||||
<div className="article-feedback">
|
||||
<h4>Was this article helpful?</h4>
|
||||
{feedbackGiven ? (
|
||||
<p className="feedback-thanks">
|
||||
<i className="fa-solid fa-check-circle me-2"></i>
|
||||
Thank you for your feedback!
|
||||
</p>
|
||||
) : (
|
||||
<div className="feedback-buttons">
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={() => handleFeedback(true)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-up me-2"></i>
|
||||
Yes ({article.helpful_count})
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
onClick={() => handleFeedback(false)}
|
||||
>
|
||||
<i className="fa-solid fa-thumbs-down me-2"></i>
|
||||
No ({article.not_helpful_count})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseArticleModal;
|
||||
|
||||
174
frontEnd/components/pages/support/SupportCenterContent.tsx
Normal file
174
frontEnd/components/pages/support/SupportCenterContent.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
import { useEffect } from 'react';
|
||||
import CreateTicketForm from './CreateTicketForm';
|
||||
import KnowledgeBase from './KnowledgeBase';
|
||||
import TicketStatusCheck from './TicketStatusCheck';
|
||||
|
||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||
|
||||
interface SupportCenterContentProps {
|
||||
activeModal: ModalType;
|
||||
onClose: () => void;
|
||||
onOpenModal?: (type: ModalType) => void;
|
||||
}
|
||||
|
||||
const SupportCenterContent = ({ activeModal, onClose, onOpenModal }: SupportCenterContentProps) => {
|
||||
// Close modal on escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && activeModal) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [activeModal, onClose]);
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (activeModal) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [activeModal]);
|
||||
|
||||
if (!activeModal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Overlay */}
|
||||
<div
|
||||
className="support-modal-overlay"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
zIndex: 9998,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
backdropFilter: 'blur(5px)',
|
||||
}}
|
||||
>
|
||||
{/* Modal Content */}
|
||||
<div
|
||||
className="support-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '12px',
|
||||
maxWidth: '1000px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
||||
animation: 'modalSlideIn 0.3s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="support-modal-close"
|
||||
aria-label="Close modal"
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
float: 'right',
|
||||
background: '#f3f4f6',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
color: '#374151',
|
||||
transition: 'all 0.2s',
|
||||
zIndex: 10,
|
||||
marginBottom: '-40px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#e5e7eb';
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#f3f4f6';
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="support-modal-body" style={{ padding: '40px' }}>
|
||||
{activeModal === 'create' && (
|
||||
<CreateTicketForm onOpenStatusCheck={() => {
|
||||
if (onOpenModal) {
|
||||
onOpenModal('status');
|
||||
}
|
||||
}} />
|
||||
)}
|
||||
{activeModal === 'knowledge' && <KnowledgeBase />}
|
||||
{activeModal === 'status' && <TicketStatusCheck />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Animation Keyframes */}
|
||||
<style jsx>{`
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.support-modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.support-modal-body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterContent;
|
||||
|
||||
151
frontEnd/components/pages/support/SupportCenterHero.tsx
Normal file
151
frontEnd/components/pages/support/SupportCenterHero.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
type ModalType = 'create' | 'knowledge' | 'status' | null;
|
||||
|
||||
interface SupportCenterHeroProps {
|
||||
onFeatureClick: (type: ModalType) => void;
|
||||
}
|
||||
|
||||
const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
|
||||
return (
|
||||
<section className="support-hero">
|
||||
{/* Animated Background */}
|
||||
<div className="hero-background">
|
||||
{/* Floating Support Icons */}
|
||||
<div className="floating-tech tech-1">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-2">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-3">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-4">
|
||||
<i className="fa-solid fa-comments"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-5">
|
||||
<i className="fa-solid fa-life-ring"></i>
|
||||
</div>
|
||||
<div className="floating-tech tech-6">
|
||||
<i className="fa-solid fa-user-shield"></i>
|
||||
</div>
|
||||
|
||||
{/* Grid Pattern */}
|
||||
<div className="grid-overlay"></div>
|
||||
|
||||
{/* Animated Gradient Orbs */}
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Video Overlay */}
|
||||
<div className="video-overlay"></div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-lg-10 col-xl-8">
|
||||
<div className="support-hero__content text-center">
|
||||
<h1 className="support-hero__title">
|
||||
Support Center
|
||||
</h1>
|
||||
<p className="support-hero__subtitle">
|
||||
Get expert assistance whenever you need it. Our dedicated support team is here to help you succeed.
|
||||
</p>
|
||||
|
||||
<div className="support-hero__features">
|
||||
<div className="row g-3 g-md-4 g-lg-5 justify-content-center">
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('create')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('create')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
</div>
|
||||
<h3>Submit Tickets</h3>
|
||||
<p>Create and track support requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('knowledge')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('knowledge')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-book"></i>
|
||||
</div>
|
||||
<h3>Knowledge Base</h3>
|
||||
<p>Find answers to common questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<div
|
||||
className="feature-item clickable"
|
||||
onClick={() => onFeatureClick('status')}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('status')}
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-search"></i>
|
||||
</div>
|
||||
<h3>Track Status</h3>
|
||||
<p>Monitor your ticket progress</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=privacy"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
</div>
|
||||
<h3>Privacy Policy</h3>
|
||||
<p>Learn about data protection</p>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=terms"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-file-contract"></i>
|
||||
</div>
|
||||
<h3>Terms of Use</h3>
|
||||
<p>Review our service terms</p>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
|
||||
<a
|
||||
href="/policy?type=support"
|
||||
className="feature-item clickable link-item"
|
||||
>
|
||||
<div className="feature-icon">
|
||||
<i className="fa-solid fa-headset"></i>
|
||||
</div>
|
||||
<h3>Support Policy</h3>
|
||||
<p>Understand our support coverage</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportCenterHero;
|
||||
|
||||
323
frontEnd/components/pages/support/TicketStatusCheck.tsx
Normal file
323
frontEnd/components/pages/support/TicketStatusCheck.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
||||
|
||||
const TicketStatusCheck = () => {
|
||||
const [ticketNumber, setTicketNumber] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSearching(true);
|
||||
setSearchError(null);
|
||||
setTicket(null);
|
||||
|
||||
try {
|
||||
const response = await checkTicketStatus(ticketNumber);
|
||||
setTicket(response);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Ticket not found') {
|
||||
setSearchError('Ticket not found. Please check your ticket number and try again.');
|
||||
} else {
|
||||
setSearchError('An error occurred while searching. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const getRelativeTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
|
||||
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
|
||||
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
|
||||
return formatDate(dateString);
|
||||
};
|
||||
|
||||
const getStatusIcon = (statusName: string) => {
|
||||
const status = statusName.toLowerCase();
|
||||
if (status.includes('open') || status.includes('new')) return 'fa-inbox';
|
||||
if (status.includes('progress') || status.includes('working')) return 'fa-spinner';
|
||||
if (status.includes('pending') || status.includes('waiting')) return 'fa-clock';
|
||||
if (status.includes('resolved') || status.includes('closed')) return 'fa-check-circle';
|
||||
return 'fa-ticket';
|
||||
};
|
||||
|
||||
const getPriorityIcon = (priorityName: string) => {
|
||||
const priority = priorityName.toLowerCase();
|
||||
if (priority.includes('urgent') || priority.includes('critical')) return 'fa-exclamation-triangle';
|
||||
if (priority.includes('high')) return 'fa-arrow-up';
|
||||
if (priority.includes('medium')) return 'fa-minus';
|
||||
if (priority.includes('low')) return 'fa-arrow-down';
|
||||
return 'fa-flag';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ticket-status-check enterprise-status-check">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12">
|
||||
<div className="status-header-enterprise">
|
||||
<div className="status-header-icon">
|
||||
<i className="fa-solid fa-magnifying-glass"></i>
|
||||
</div>
|
||||
<h2>Check Ticket Status</h2>
|
||||
<p>Track your support request in real-time with instant status updates</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="status-search-form-enterprise">
|
||||
<div className="search-container">
|
||||
<div className="search-input-wrapper">
|
||||
<i className="fa-solid fa-ticket search-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={ticketNumber}
|
||||
onChange={(e) => setTicketNumber(e.target.value.toUpperCase())}
|
||||
placeholder="TKT-YYYYMMDD-XXXXX"
|
||||
required
|
||||
className="search-input-enterprise"
|
||||
pattern="TKT-\d{8}-[A-Z0-9]{5}"
|
||||
title="Please enter a valid ticket number (e.g., TKT-20231015-ABCDE)"
|
||||
/>
|
||||
{ticketNumber && (
|
||||
<button
|
||||
type="button"
|
||||
className="clear-btn"
|
||||
onClick={() => {
|
||||
setTicketNumber('');
|
||||
setTicket(null);
|
||||
setSearchError(null);
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-times"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-search-enterprise"
|
||||
disabled={isSearching}
|
||||
>
|
||||
{isSearching ? (
|
||||
<>
|
||||
<span className="spinner-enterprise"></span>
|
||||
<span>Searching...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-solid fa-search"></i>
|
||||
<span>Search</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="search-hint">
|
||||
<i className="fa-solid fa-info-circle"></i>
|
||||
Enter your ticket number exactly as it appears in your confirmation email
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<i className="fa-solid fa-exclamation-circle"></i>
|
||||
<div>
|
||||
<strong>Ticket Not Found</strong>
|
||||
<p>{searchError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ticket && (
|
||||
<div className="ticket-details-enterprise">
|
||||
{/* Header Section */}
|
||||
<div className="ticket-header-enterprise">
|
||||
<div className="ticket-number-section">
|
||||
<span className="ticket-label">Ticket Number</span>
|
||||
<div className="ticket-number-display">
|
||||
<i className="fa-solid fa-ticket"></i>
|
||||
<span>{ticket.ticket_number}</span>
|
||||
<button
|
||||
className="btn-copy-inline"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(ticket.ticket_number);
|
||||
const btn = document.querySelector('.btn-copy-inline');
|
||||
if (btn) {
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="status-badge-enterprise"
|
||||
style={{
|
||||
backgroundColor: ticket.status_color || '#6366f1',
|
||||
boxShadow: `0 0 20px ${ticket.status_color}33`
|
||||
}}
|
||||
>
|
||||
<i className={`fa-solid ${getStatusIcon(ticket.status_name)}`}></i>
|
||||
<span>{ticket.status_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Cards Section */}
|
||||
<div className="ticket-info-cards">
|
||||
<div className="info-card">
|
||||
<i className="fa-solid fa-calendar-plus"></i>
|
||||
<div>
|
||||
<span className="card-label">Created</span>
|
||||
<span className="card-value">{getRelativeTime(ticket.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-card">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
<div>
|
||||
<span className="card-label">Last Updated</span>
|
||||
<span className="card-value">{getRelativeTime(ticket.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{ticket.priority_name && (
|
||||
<div className="info-card">
|
||||
<i className={`fa-solid ${getPriorityIcon(ticket.priority_name)}`}></i>
|
||||
<div>
|
||||
<span className="card-label">Priority</span>
|
||||
<span
|
||||
className="card-value priority-value"
|
||||
style={{ color: ticket.priority_color }}
|
||||
>
|
||||
{ticket.priority_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ticket.category_name && (
|
||||
<div className="info-card">
|
||||
<i className="fa-solid fa-folder-open"></i>
|
||||
<div>
|
||||
<span className="card-label">Category</span>
|
||||
<span className="card-value">{ticket.category_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="ticket-content-section">
|
||||
<div className="content-header">
|
||||
<h3>{ticket.title}</h3>
|
||||
<div className="content-meta">
|
||||
<span><i className="fa-solid fa-user"></i> {ticket.user_name}</span>
|
||||
{ticket.company && (
|
||||
<span><i className="fa-solid fa-building"></i> {ticket.company}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="description-section">
|
||||
<h4><i className="fa-solid fa-align-left"></i> Description</h4>
|
||||
<p>{ticket.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Messages Section */}
|
||||
{ticket.messages && ticket.messages.filter(msg => !msg.is_internal).length > 0 && (
|
||||
<div className="messages-section-enterprise">
|
||||
<h4>
|
||||
<i className="fa-solid fa-comments"></i>
|
||||
Conversation
|
||||
<span className="count-badge">{ticket.messages.filter(msg => !msg.is_internal).length}</span>
|
||||
</h4>
|
||||
<div className="messages-list-enterprise">
|
||||
{ticket.messages
|
||||
.filter(msg => !msg.is_internal)
|
||||
.map((message) => (
|
||||
<div key={message.id} className="message-card-enterprise">
|
||||
<div className="message-avatar">
|
||||
<i className="fa-solid fa-user"></i>
|
||||
</div>
|
||||
<div className="message-content-wrapper">
|
||||
<div className="message-header-enterprise">
|
||||
<span className="message-author">{message.author_name || message.author_email}</span>
|
||||
<span className="message-time">{getRelativeTime(message.created_at)}</span>
|
||||
</div>
|
||||
<div className="message-text">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activity Timeline */}
|
||||
{ticket.activities && ticket.activities.length > 0 && (
|
||||
<div className="timeline-section-enterprise">
|
||||
<h4>
|
||||
<i className="fa-solid fa-list-check"></i>
|
||||
Activity Timeline
|
||||
</h4>
|
||||
<div className="timeline-items">
|
||||
{ticket.activities.slice(0, 5).map((activity, index) => (
|
||||
<div key={activity.id} className="timeline-item-enterprise">
|
||||
<div className="timeline-marker">
|
||||
<i className="fa-solid fa-circle"></i>
|
||||
</div>
|
||||
<div className="timeline-content-wrapper">
|
||||
<div className="timeline-text">{activity.description}</div>
|
||||
<div className="timeline-time">{getRelativeTime(activity.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="ticket-footer-enterprise">
|
||||
<div className="footer-info">
|
||||
<i className="fa-solid fa-shield-check"></i>
|
||||
<span>This ticket is securely tracked and monitored by our support team</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn-refresh"
|
||||
onClick={() => handleSubmit({ preventDefault: () => {} } as FormEvent<HTMLFormElement>)}
|
||||
>
|
||||
<i className="fa-solid fa-rotate"></i>
|
||||
Refresh Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TicketStatusCheck;
|
||||
|
||||
370
frontEnd/components/shared/banners/ServiceDetailsBanner.tsx
Normal file
370
frontEnd/components/shared/banners/ServiceDetailsBanner.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
|
||||
interface ServiceDetailsBannerProps {
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
if (document.querySelector(".service-banner")) {
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: ".service-banner",
|
||||
start: "center center",
|
||||
end: "+=100%",
|
||||
scrub: true,
|
||||
pin: false,
|
||||
},
|
||||
});
|
||||
|
||||
tl.to(".thumb-one", {
|
||||
opacity: 0.3,
|
||||
y: "-100%",
|
||||
zIndex: -1,
|
||||
duration: 2,
|
||||
});
|
||||
|
||||
tl.to(
|
||||
".thumb-two",
|
||||
{
|
||||
opacity: 0.3,
|
||||
scale: 2,
|
||||
y: "100%",
|
||||
zIndex: -1,
|
||||
duration: 2,
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="enterprise-banner position-relative overflow-hidden">
|
||||
<div className="banner-background">
|
||||
<div className="gradient-orb orb-1"></div>
|
||||
<div className="gradient-orb orb-2"></div>
|
||||
<div className="gradient-orb orb-3"></div>
|
||||
|
||||
{/* Service-Specific Background Elements */}
|
||||
<div className="enterprise-bg-elements">
|
||||
{/* Flying Service Code Elements */}
|
||||
<div className="flying-code">
|
||||
<div className="code-snippet code-1">
|
||||
<span className="code-line">const service = {'{'}</span>
|
||||
<span className="code-line"> name: '{service.title}',</span>
|
||||
<span className="code-line"> category: '{service.category?.name}'</span>
|
||||
<span className="code-line">{'}'};</span>
|
||||
</div>
|
||||
<div className="code-snippet code-2">
|
||||
<span className="code-line">if (service.featured) {'{'}</span>
|
||||
<span className="code-line"> deploy.premium();</span>
|
||||
<span className="code-line">{'}'}</span>
|
||||
</div>
|
||||
<div className="code-snippet code-3">
|
||||
<span className="code-line">class ServiceDelivery {'{'}</span>
|
||||
<span className="code-line"> constructor() {'{'}</span>
|
||||
<span className="code-line"> this.quality = 'enterprise';</span>
|
||||
<span className="code-line"> {'}'}</span>
|
||||
<span className="code-line">{'}'}</span>
|
||||
</div>
|
||||
<div className="code-snippet code-4">
|
||||
<span className="code-line">API.deliver({'{'}</span>
|
||||
<span className="code-line"> service: '{service.title}',</span>
|
||||
<span className="code-line"> duration: '{service.duration}'</span>
|
||||
<span className="code-line">{'}'});</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Industrial Grid */}
|
||||
<div className="industrial-grid">
|
||||
<div className="grid-line horizontal h-1"></div>
|
||||
<div className="grid-line horizontal h-2"></div>
|
||||
<div className="grid-line horizontal h-3"></div>
|
||||
<div className="grid-line horizontal h-4"></div>
|
||||
<div className="grid-line vertical v-1"></div>
|
||||
<div className="grid-line vertical v-2"></div>
|
||||
<div className="grid-line vertical v-3"></div>
|
||||
<div className="grid-line vertical v-4"></div>
|
||||
</div>
|
||||
|
||||
{/* Service Elements */}
|
||||
<div className="security-elements">
|
||||
<div className="shield shield-1">
|
||||
<i className="fa-solid fa-cogs"></i>
|
||||
</div>
|
||||
<div className="shield shield-2">
|
||||
<i className="fa-solid fa-rocket"></i>
|
||||
</div>
|
||||
<div className="shield shield-3">
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="shield shield-4">
|
||||
<i className="fa-solid fa-users"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Circuit Patterns */}
|
||||
<div className="circuit-patterns">
|
||||
<div className="circuit circuit-1">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
<div className="circuit circuit-2">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
<div className="circuit circuit-3">
|
||||
<div className="circuit-node"></div>
|
||||
<div className="circuit-line"></div>
|
||||
<div className="circuit-node"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Streams */}
|
||||
<div className="data-streams">
|
||||
<div className="stream stream-1">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
<div className="stream stream-2">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
<div className="stream stream-3">
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
<div className="data-bit"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Request/Response Data */}
|
||||
<div className="request-response-data">
|
||||
<div className="api-request req-1">
|
||||
<div className="request-label">POST /api/services</div>
|
||||
<div className="request-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-response resp-1">
|
||||
<div className="response-label">200 OK</div>
|
||||
<div className="response-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-request req-2">
|
||||
<div className="request-label">GET /api/delivery</div>
|
||||
<div className="request-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="api-response resp-2">
|
||||
<div className="response-label">201 Created</div>
|
||||
<div className="response-data">
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
<div className="data-packet"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Data Generation */}
|
||||
<div className="space-data-generation">
|
||||
<div className="data-cluster cluster-1">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-2">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-3">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
<div className="data-cluster cluster-4">
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
<div className="data-point"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Connections */}
|
||||
<div className="database-connections">
|
||||
<div className="db-connection conn-1">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-cogs"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
<div className="db-connection conn-2">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-rocket"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
<div className="db-connection conn-3">
|
||||
<div className="connection-line"></div>
|
||||
<div className="db-node">
|
||||
<i className="fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="connection-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Metrics */}
|
||||
<div className="real-time-metrics">
|
||||
<div className="metric metric-1">
|
||||
<div className="metric-label">Service Uptime</div>
|
||||
<div className="metric-value">99.9%</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric metric-2">
|
||||
<div className="metric-label">Response Time</div>
|
||||
<div className="metric-value">45ms</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="metric metric-3">
|
||||
<div className="metric-label">Client Satisfaction</div>
|
||||
<div className="metric-value">98%</div>
|
||||
<div className="metric-bar">
|
||||
<div className="bar-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-12 col-xl-10">
|
||||
<div className="enterprise-banner__content">
|
||||
<div className="banner-badge mb-4">
|
||||
<span className="enterprise-badge">
|
||||
{service.category?.name || 'Professional Service'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="enterprise-title mb-4">
|
||||
{service.title}
|
||||
</h1>
|
||||
|
||||
<p className="enterprise-description mb-5">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
<div className="enterprise-highlights mb-5">
|
||||
<div className="row justify-content-center g-4">
|
||||
{service.duration && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card">
|
||||
<div className="highlight-icon">
|
||||
<i className="fa-solid fa-clock"></i>
|
||||
</div>
|
||||
<div className="highlight-content">
|
||||
<span className="highlight-label">Duration</span>
|
||||
<span className="highlight-value">{service.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.featured && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card featured">
|
||||
<div className="highlight-icon">
|
||||
<i className="fa-solid fa-star"></i>
|
||||
</div>
|
||||
<div className="highlight-content">
|
||||
<span className="highlight-label">Premium</span>
|
||||
<span className="highlight-value">Featured Service</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="enterprise-cta">
|
||||
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
|
||||
<span>Get Free Quote</span>
|
||||
<i className="fa-solid fa-arrow-right ms-2"></i>
|
||||
</Link>
|
||||
<Link href="#service-details" className="btn btn-outline-light btn-lg mb-3">
|
||||
<span>Learn More</span>
|
||||
<i className="fa-solid fa-arrow-down ms-2"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<Link href="#scroll-to" className="scroll-to">
|
||||
Scroll
|
||||
<span className="arrow"></span>
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceDetailsBanner;
|
||||
387
frontEnd/components/shared/layout/CookieConsent.tsx
Normal file
387
frontEnd/components/shared/layout/CookieConsent.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useCookieConsent, CookiePreferences } from './CookieConsentContext';
|
||||
|
||||
// Cookie type definitions
|
||||
interface CookieType {
|
||||
id: keyof CookiePreferences;
|
||||
name: string;
|
||||
description: string;
|
||||
required: boolean;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const cookieTypes: CookieType[] = [
|
||||
{
|
||||
id: 'necessary',
|
||||
name: 'Necessary Cookies',
|
||||
description: 'Essential cookies required for the website to function properly. These cannot be disabled.',
|
||||
required: true,
|
||||
icon: 'fas fa-shield-alt',
|
||||
},
|
||||
{
|
||||
id: 'functional',
|
||||
name: 'Functional Cookies',
|
||||
description: 'These cookies enable enhanced functionality and personalization, such as remembering your preferences.',
|
||||
required: false,
|
||||
icon: 'fas fa-cogs',
|
||||
},
|
||||
];
|
||||
|
||||
// Main Cookie Consent Banner Component
|
||||
export const CookieConsentBanner: React.FC = () => {
|
||||
const { state, config, acceptAll, acceptNecessary, showSettings } = useCookieConsent();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.showBanner) {
|
||||
// Small delay to ensure smooth animation
|
||||
const timer = setTimeout(() => setIsVisible(true), 100);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, [state.showBanner]);
|
||||
|
||||
if (!state.showBanner || !isVisible) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{/* Fullscreen overlay to center the banner */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 10000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(17, 24, 39, 0.45)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
}}
|
||||
>
|
||||
{/* Centered enterprise-style card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.96, y: 8 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.98, y: 8 }}
|
||||
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||
className="cookie-consent-banner"
|
||||
style={{
|
||||
width: 'min(680px, 92vw)',
|
||||
background: '#0b1220',
|
||||
color: '#e5e7eb',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 25px 70px rgba(0,0,0,0.45)',
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div className="cookie-consent-banner__container" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div className="cookie-consent-banner__content" style={{ display: 'flex', gap: 16 }}>
|
||||
<div
|
||||
className="cookie-consent-banner__icon"
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: 'linear-gradient(135deg, rgba(199, 213, 236, 0.39), rgba(147,197,253,0.08))',
|
||||
color: '#93c5fd',
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-cookie-bite"></i>
|
||||
</div>
|
||||
<div className="cookie-consent-banner__text" style={{ display: 'grid', gap: 6 }}>
|
||||
<h3 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>Cookie Preferences</h3>
|
||||
<p style={{ margin: 0, lineHeight: 1.6, color: '#ffffff' }}>
|
||||
We use only essential functional cookies to ensure our website works properly. We do not collect
|
||||
personal data or use tracking cookies. Your privacy is important to us.
|
||||
</p>
|
||||
{config.showPrivacyNotice && (
|
||||
<div className="cookie-consent-banner__links" style={{ marginTop: 6 }}>
|
||||
<a
|
||||
href={config.privacyPolicyUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#93c5fd', textDecoration: 'underline' }}
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="cookie-consent-banner__actions"
|
||||
style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 8 }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-consent-banner__btn cookie-consent-banner__btn--secondary"
|
||||
onClick={showSettings}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'transparent',
|
||||
color: '#e5e7eb',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-cog" style={{ marginRight: 8 }}></i>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-consent-banner__btn cookie-consent-banner__btn--primary"
|
||||
onClick={acceptNecessary}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(59,130,246,0.35)',
|
||||
background: 'linear-gradient(135deg, rgba(59,130,246,0.25), rgba(37,99,235,0.35))',
|
||||
color: '#ffffff',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-check" style={{ marginRight: 8 }}></i>
|
||||
Accept Functional Only
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
// Cookie Settings Modal Component
|
||||
export const CookieSettingsModal: React.FC = () => {
|
||||
const { state, config, hideSettings, acceptSelected, updatePreferences, withdrawConsent, exportConsentData } = useCookieConsent();
|
||||
const [tempPreferences, setTempPreferences] = useState<CookiePreferences>(state.preferences);
|
||||
|
||||
useEffect(() => {
|
||||
setTempPreferences(state.preferences);
|
||||
}, [state.preferences]);
|
||||
|
||||
const handlePreferenceChange = (type: keyof CookiePreferences, value: boolean) => {
|
||||
if (type === 'necessary') return; // Cannot change necessary cookies
|
||||
|
||||
setTempPreferences(prev => ({
|
||||
...prev,
|
||||
[type]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSavePreferences = () => {
|
||||
acceptSelected(tempPreferences);
|
||||
};
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
const allAccepted: CookiePreferences = {
|
||||
necessary: true,
|
||||
functional: true,
|
||||
};
|
||||
acceptSelected(allAccepted);
|
||||
};
|
||||
|
||||
if (!state.showSettings) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="cookie-settings-overlay"
|
||||
onClick={hideSettings}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="cookie-settings-modal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="cookie-settings-modal__header">
|
||||
<div className="cookie-settings-modal__header-content">
|
||||
<h2>Cookie Preferences</h2>
|
||||
<p className="cookie-settings-modal__version">v{config.version}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-settings-modal__close"
|
||||
onClick={hideSettings}
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="cookie-settings-modal__content">
|
||||
<div className="cookie-settings-modal__description">
|
||||
<p>
|
||||
We respect your privacy and only use essential functional cookies.
|
||||
You can choose which types of cookies to allow below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="cookie-settings-modal__types">
|
||||
{cookieTypes.map((cookieType) => (
|
||||
<div
|
||||
key={cookieType.id}
|
||||
className={`cookie-settings-modal__type ${
|
||||
cookieType.required ? 'cookie-settings-modal__type--required' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="cookie-settings-modal__type-header">
|
||||
<div className="cookie-settings-modal__type-info">
|
||||
<i className={cookieType.icon}></i>
|
||||
<div>
|
||||
<h4>{cookieType.name}</h4>
|
||||
<p>{cookieType.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cookie-settings-modal__type-toggle">
|
||||
<label className="cookie-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tempPreferences[cookieType.id]}
|
||||
onChange={(e) => handlePreferenceChange(cookieType.id, e.target.checked)}
|
||||
disabled={cookieType.required}
|
||||
/>
|
||||
<span className="cookie-toggle__slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="cookie-settings-modal__privacy">
|
||||
<h4>Privacy Information</h4>
|
||||
<ul>
|
||||
<li>We do not collect personal data without your explicit consent</li>
|
||||
<li>Functional cookies are used only to maintain website functionality</li>
|
||||
<li>We do not use tracking, analytics, or marketing cookies</li>
|
||||
<li>You can change your preferences at any time</li>
|
||||
<li>Data retention period: {config.retentionPeriod} days</li>
|
||||
<li>Contact: <a href={`mailto:${config.dataControllerEmail}`}>{config.dataControllerEmail}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{config.enableDetailedSettings && (
|
||||
<div className="cookie-settings-modal__enterprise">
|
||||
<h4>Enterprise Features</h4>
|
||||
<div className="cookie-settings-modal__enterprise-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-settings-modal__btn cookie-settings-modal__btn--outline"
|
||||
onClick={() => {
|
||||
const data = exportConsentData();
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `cookie-consent-data-${new Date().toISOString().split('T')[0]}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-download"></i>
|
||||
Export Data
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-settings-modal__btn cookie-settings-modal__btn--danger"
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to withdraw your consent? This will reset all preferences.')) {
|
||||
withdrawConsent();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="fas fa-user-times"></i>
|
||||
Withdraw Consent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="cookie-settings-modal__footer">
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-settings-modal__btn cookie-settings-modal__btn--secondary"
|
||||
onClick={hideSettings}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-settings-modal__btn cookie-settings-modal__btn--primary"
|
||||
onClick={handleSavePreferences}
|
||||
>
|
||||
Save Preferences
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Cookie Consent Component that combines both banner and modal
|
||||
export const CookieConsent: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<CookieConsentBanner />
|
||||
<CookieSettingsModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Cookie Preferences Display Component (for footer or privacy page)
|
||||
export const CookiePreferencesDisplay: React.FC = () => {
|
||||
const { state, showSettings, resetConsent } = useCookieConsent();
|
||||
|
||||
return (
|
||||
<div className="cookie-preferences-display">
|
||||
<h3>Cookie Preferences</h3>
|
||||
<div className="cookie-preferences-display__status">
|
||||
<p>
|
||||
<strong>Status:</strong> {state.hasConsented ? 'Consent Given' : 'No Consent'}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Functional Cookies:</strong> {state.preferences.functional ? 'Enabled' : 'Disabled'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="cookie-preferences-display__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-preferences-display__btn"
|
||||
onClick={showSettings}
|
||||
>
|
||||
Update Preferences
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="cookie-preferences-display__btn cookie-preferences-display__btn--reset"
|
||||
onClick={resetConsent}
|
||||
>
|
||||
Reset Consent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
413
frontEnd/components/shared/layout/CookieConsentContext.tsx
Normal file
413
frontEnd/components/shared/layout/CookieConsentContext.tsx
Normal file
@@ -0,0 +1,413 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
// Types for cookie consent
|
||||
export interface CookiePreferences {
|
||||
necessary: boolean;
|
||||
functional: boolean;
|
||||
}
|
||||
|
||||
export interface ConsentAuditLog {
|
||||
timestamp: string;
|
||||
action: 'consent_given' | 'consent_updated' | 'consent_withdrawn' | 'settings_opened';
|
||||
preferences: CookiePreferences;
|
||||
userAgent: string;
|
||||
ipAddress?: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface CookieConsentConfig {
|
||||
version: string;
|
||||
companyName: string;
|
||||
privacyPolicyUrl: string;
|
||||
cookiePolicyUrl: string;
|
||||
dataControllerEmail: string;
|
||||
retentionPeriod: number; // days
|
||||
enableAuditLog: boolean;
|
||||
enableDetailedSettings: boolean;
|
||||
showPrivacyNotice: boolean;
|
||||
}
|
||||
|
||||
export interface CookieConsentState {
|
||||
hasConsented: boolean;
|
||||
preferences: CookiePreferences;
|
||||
showBanner: boolean;
|
||||
showSettings: boolean;
|
||||
consentDate?: string;
|
||||
lastUpdated?: string;
|
||||
auditLog: ConsentAuditLog[];
|
||||
}
|
||||
|
||||
export interface CookieConsentContextType {
|
||||
state: CookieConsentState;
|
||||
config: CookieConsentConfig;
|
||||
acceptAll: () => void;
|
||||
acceptNecessary: () => void;
|
||||
acceptSelected: (preferences: Partial<CookiePreferences>) => void;
|
||||
showSettings: () => void;
|
||||
hideSettings: () => void;
|
||||
updatePreferences: (preferences: Partial<CookiePreferences>) => void;
|
||||
resetConsent: () => void;
|
||||
withdrawConsent: () => void;
|
||||
exportConsentData: () => string;
|
||||
getConsentSummary: () => any;
|
||||
}
|
||||
|
||||
// Default cookie preferences
|
||||
const defaultPreferences: CookiePreferences = {
|
||||
necessary: true, // Always true, cannot be disabled
|
||||
functional: false,
|
||||
};
|
||||
|
||||
// Enterprise configuration
|
||||
const defaultConfig: CookieConsentConfig = {
|
||||
version: '2.0',
|
||||
companyName: 'Your Company Name',
|
||||
privacyPolicyUrl: '/policy?type=privacy',
|
||||
cookiePolicyUrl: '/policy?type=privacy',
|
||||
dataControllerEmail: 'privacy@yourcompany.com',
|
||||
retentionPeriod: 365, // 1 year
|
||||
enableAuditLog: true,
|
||||
enableDetailedSettings: true,
|
||||
showPrivacyNotice: true,
|
||||
};
|
||||
|
||||
// Default state
|
||||
const defaultState: CookieConsentState = {
|
||||
hasConsented: false,
|
||||
preferences: defaultPreferences,
|
||||
showBanner: false,
|
||||
showSettings: false,
|
||||
auditLog: [],
|
||||
};
|
||||
|
||||
// Create context
|
||||
const CookieConsentContext = createContext<CookieConsentContextType | undefined>(undefined);
|
||||
|
||||
// Storage keys
|
||||
const CONSENT_STORAGE_KEY = 'cookie-consent-preferences';
|
||||
const CONSENT_VERSION = '2.0';
|
||||
|
||||
// Utility functions
|
||||
const generateSessionId = (): string => {
|
||||
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
};
|
||||
|
||||
const createAuditLogEntry = (
|
||||
action: ConsentAuditLog['action'],
|
||||
preferences: CookiePreferences
|
||||
): ConsentAuditLog => {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
action,
|
||||
preferences,
|
||||
userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : '',
|
||||
sessionId: generateSessionId(),
|
||||
};
|
||||
};
|
||||
|
||||
// Provider component
|
||||
export const CookieConsentProvider: React.FC<{
|
||||
children: ReactNode;
|
||||
config?: Partial<CookieConsentConfig>;
|
||||
}> = ({ children, config: customConfig }) => {
|
||||
const [state, setState] = useState<CookieConsentState>(defaultState);
|
||||
const config = { ...defaultConfig, ...customConfig };
|
||||
|
||||
// Load saved preferences on mount
|
||||
useEffect(() => {
|
||||
const loadSavedPreferences = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
// Check if saved version matches current version
|
||||
if (parsed.version === CONSENT_VERSION) {
|
||||
setState({
|
||||
hasConsented: true,
|
||||
preferences: parsed.preferences,
|
||||
showBanner: false,
|
||||
showSettings: false,
|
||||
consentDate: parsed.consentDate,
|
||||
lastUpdated: parsed.lastUpdated,
|
||||
auditLog: parsed.auditLog || [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
// Show banner if no valid consent found
|
||||
setState(prev => ({ ...prev, showBanner: true }));
|
||||
};
|
||||
|
||||
loadSavedPreferences();
|
||||
}, []);
|
||||
|
||||
// Save preferences to localStorage with audit logging
|
||||
const savePreferences = (preferences: CookiePreferences, action: ConsentAuditLog['action'] = 'consent_given') => {
|
||||
try {
|
||||
const auditEntry = config.enableAuditLog ? createAuditLogEntry(action, preferences) : null;
|
||||
|
||||
const data = {
|
||||
version: CONSENT_VERSION,
|
||||
preferences,
|
||||
timestamp: new Date().toISOString(),
|
||||
consentDate: state.consentDate || new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
auditLog: auditEntry ? [...state.auditLog, auditEntry] : state.auditLog,
|
||||
};
|
||||
|
||||
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(data));
|
||||
|
||||
// Update state with audit log
|
||||
if (auditEntry) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
auditLog: [...prev.auditLog, auditEntry],
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
};
|
||||
|
||||
// Accept all cookies
|
||||
const acceptAll = () => {
|
||||
const allAccepted: CookiePreferences = {
|
||||
necessary: true,
|
||||
functional: true,
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
hasConsented: true,
|
||||
preferences: allAccepted,
|
||||
showBanner: false,
|
||||
showSettings: false,
|
||||
consentDate: prev.consentDate || new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
savePreferences(allAccepted, 'consent_given');
|
||||
};
|
||||
|
||||
// Accept only necessary cookies
|
||||
const acceptNecessary = () => {
|
||||
const necessaryOnly: CookiePreferences = {
|
||||
necessary: true,
|
||||
functional: false,
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
hasConsented: true,
|
||||
preferences: necessaryOnly,
|
||||
showBanner: false,
|
||||
showSettings: false,
|
||||
consentDate: prev.consentDate || new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
savePreferences(necessaryOnly, 'consent_given');
|
||||
};
|
||||
|
||||
// Accept selected cookies
|
||||
const acceptSelected = (preferences: Partial<CookiePreferences>) => {
|
||||
const newPreferences: CookiePreferences = {
|
||||
necessary: true, // Always true
|
||||
functional: preferences.functional ?? false,
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
hasConsented: true,
|
||||
preferences: newPreferences,
|
||||
showBanner: false,
|
||||
showSettings: false,
|
||||
consentDate: prev.consentDate || new Date().toISOString(),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
savePreferences(newPreferences, 'consent_updated');
|
||||
};
|
||||
|
||||
// Show settings modal
|
||||
const showSettings = () => {
|
||||
setState(prev => ({ ...prev, showSettings: true }));
|
||||
|
||||
// Log settings opened
|
||||
if (config.enableAuditLog) {
|
||||
const auditEntry = createAuditLogEntry('settings_opened', state.preferences);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
auditLog: [...prev.auditLog, auditEntry],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Hide settings modal
|
||||
const hideSettings = () => {
|
||||
setState(prev => ({ ...prev, showSettings: false }));
|
||||
};
|
||||
|
||||
// Update preferences (for settings modal)
|
||||
const updatePreferences = (preferences: Partial<CookiePreferences>) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
preferences: {
|
||||
...prev.preferences,
|
||||
...preferences,
|
||||
necessary: true, // Always keep necessary as true
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// Reset consent (for testing or user request)
|
||||
const resetConsent = () => {
|
||||
try {
|
||||
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
setState({
|
||||
hasConsented: false,
|
||||
preferences: defaultPreferences,
|
||||
showBanner: true,
|
||||
showSettings: false,
|
||||
auditLog: [],
|
||||
});
|
||||
};
|
||||
|
||||
// Withdraw consent (GDPR compliance)
|
||||
const withdrawConsent = () => {
|
||||
try {
|
||||
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
const auditEntry = config.enableAuditLog ? createAuditLogEntry('consent_withdrawn', defaultPreferences) : null;
|
||||
|
||||
setState({
|
||||
hasConsented: false,
|
||||
preferences: defaultPreferences,
|
||||
showBanner: true,
|
||||
showSettings: false,
|
||||
auditLog: auditEntry ? [...state.auditLog, auditEntry] : state.auditLog,
|
||||
});
|
||||
};
|
||||
|
||||
// Export consent data (GDPR compliance)
|
||||
const exportConsentData = (): string => {
|
||||
const exportData = {
|
||||
consentData: {
|
||||
hasConsented: state.hasConsented,
|
||||
preferences: state.preferences,
|
||||
consentDate: state.consentDate,
|
||||
lastUpdated: state.lastUpdated,
|
||||
auditLog: state.auditLog,
|
||||
},
|
||||
config: {
|
||||
version: config.version,
|
||||
companyName: config.companyName,
|
||||
retentionPeriod: config.retentionPeriod,
|
||||
},
|
||||
exportDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
};
|
||||
|
||||
// Get consent summary
|
||||
const getConsentSummary = () => {
|
||||
return {
|
||||
hasConsented: state.hasConsented,
|
||||
preferences: state.preferences,
|
||||
consentDate: state.consentDate,
|
||||
lastUpdated: state.lastUpdated,
|
||||
auditLogCount: state.auditLog.length,
|
||||
config: {
|
||||
version: config.version,
|
||||
companyName: config.companyName,
|
||||
retentionPeriod: config.retentionPeriod,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const contextValue: CookieConsentContextType = {
|
||||
state,
|
||||
config,
|
||||
acceptAll,
|
||||
acceptNecessary,
|
||||
acceptSelected,
|
||||
showSettings,
|
||||
hideSettings,
|
||||
updatePreferences,
|
||||
resetConsent,
|
||||
withdrawConsent,
|
||||
exportConsentData,
|
||||
getConsentSummary,
|
||||
};
|
||||
|
||||
return (
|
||||
<CookieConsentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</CookieConsentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use cookie consent context
|
||||
export const useCookieConsent = (): CookieConsentContextType => {
|
||||
const context = useContext(CookieConsentContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCookieConsent must be used within a CookieConsentProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Hook to check if specific cookie type is allowed
|
||||
export const useCookiePermission = (type: keyof CookiePreferences): boolean => {
|
||||
const { state } = useCookieConsent();
|
||||
return state.preferences[type];
|
||||
};
|
||||
|
||||
// Hook for functional features (only runs if functional cookies are allowed)
|
||||
export const useFunctional = () => {
|
||||
const { state } = useCookieConsent();
|
||||
const isEnabled = state.preferences.functional && state.hasConsented;
|
||||
|
||||
const saveUserPreference = (key: string, value: any) => {
|
||||
if (isEnabled && typeof window !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserPreference = (key: string, defaultValue?: any) => {
|
||||
if (isEnabled && typeof window !== 'undefined') {
|
||||
try {
|
||||
const saved = localStorage.getItem(`user-preference-${key}`);
|
||||
return saved ? JSON.parse(saved) : defaultValue;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const rememberUserAction = (action: string, data?: any) => {
|
||||
if (isEnabled) {
|
||||
// Implement your user action tracking logic here
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
saveUserPreference,
|
||||
loadUserPreference,
|
||||
rememberUserAction,
|
||||
isEnabled,
|
||||
};
|
||||
};
|
||||
97
frontEnd/components/shared/layout/CookieConsentUtils.tsx
Normal file
97
frontEnd/components/shared/layout/CookieConsentUtils.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCookieConsent, useCookiePermission } from './CookieConsentContext';
|
||||
|
||||
// Utility hook for conditional rendering based on cookie permissions
|
||||
export const useConditionalFeature = (featureType: 'functional') => {
|
||||
const isAllowed = useCookiePermission(featureType);
|
||||
const { state } = useCookieConsent();
|
||||
|
||||
return {
|
||||
isAllowed,
|
||||
hasConsented: state.hasConsented,
|
||||
canShow: isAllowed && state.hasConsented,
|
||||
};
|
||||
};
|
||||
|
||||
// Note: Analytics and marketing hooks removed as we don't collect this data
|
||||
|
||||
// Hook for functional features (only runs if functional cookies are allowed)
|
||||
export const useFunctional = () => {
|
||||
const { canShow } = useConditionalFeature('functional');
|
||||
|
||||
const saveUserPreference = (key: string, value: any) => {
|
||||
if (canShow && typeof window !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserPreference = (key: string, defaultValue?: any) => {
|
||||
if (canShow && typeof window !== 'undefined') {
|
||||
try {
|
||||
const saved = localStorage.getItem(`user-preference-${key}`);
|
||||
return saved ? JSON.parse(saved) : defaultValue;
|
||||
} catch (error) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
const rememberUserAction = (action: string, data?: any) => {
|
||||
if (canShow) {
|
||||
// Implement your user action tracking logic here
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
saveUserPreference,
|
||||
loadUserPreference,
|
||||
rememberUserAction,
|
||||
isEnabled: canShow,
|
||||
};
|
||||
};
|
||||
|
||||
// Component wrapper for conditional rendering
|
||||
interface ConditionalFeatureProps {
|
||||
feature: 'functional';
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ConditionalFeature: React.FC<ConditionalFeatureProps> = ({
|
||||
feature,
|
||||
children,
|
||||
fallback = null,
|
||||
}) => {
|
||||
const { canShow } = useConditionalFeature(feature);
|
||||
|
||||
return canShow ? <>{children}</> : <>{fallback}</>;
|
||||
};
|
||||
|
||||
// Example usage components (analytics and marketing removed)
|
||||
|
||||
export const UserPreferenceManager: React.FC<{
|
||||
preferenceKey: string;
|
||||
defaultValue?: any;
|
||||
children: (value: any, setValue: (value: any) => void) => React.ReactNode;
|
||||
}> = ({ preferenceKey, defaultValue, children }) => {
|
||||
const { saveUserPreference, loadUserPreference } = useFunctional();
|
||||
const [value, setValue] = React.useState(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = loadUserPreference(preferenceKey, defaultValue);
|
||||
setValue(saved);
|
||||
}, [preferenceKey, defaultValue, loadUserPreference]);
|
||||
|
||||
const handleSetValue = (newValue: any) => {
|
||||
setValue(newValue);
|
||||
saveUserPreference(preferenceKey, newValue);
|
||||
};
|
||||
|
||||
return <>{children(value, handleSetValue)}</>;
|
||||
};
|
||||
62
frontEnd/components/shared/layout/LayoutWrapper.tsx
Normal file
62
frontEnd/components/shared/layout/LayoutWrapper.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import Preloader from "./Preloader";
|
||||
import ScrollToTop from "./ScrollToTop";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface LayoutWrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const LayoutWrapper = ({ children }: LayoutWrapperProps) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Force scroll to top on every pathname change - runs FIRST
|
||||
window.history.scrollRestoration = 'manual';
|
||||
|
||||
// Immediate scroll
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
|
||||
// Disable any smooth scroll temporarily
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
const originalHtmlScroll = html.style.scrollBehavior;
|
||||
const originalBodyScroll = body.style.scrollBehavior;
|
||||
|
||||
html.style.scrollBehavior = 'auto';
|
||||
body.style.scrollBehavior = 'auto';
|
||||
|
||||
// Multiple forced scrolls
|
||||
const scrollInterval = setInterval(() => {
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
}, 10);
|
||||
|
||||
// Clean up after 300ms
|
||||
const cleanup = setTimeout(() => {
|
||||
clearInterval(scrollInterval);
|
||||
html.style.scrollBehavior = originalHtmlScroll;
|
||||
body.style.scrollBehavior = originalBodyScroll;
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearInterval(scrollInterval);
|
||||
clearTimeout(cleanup);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollToTop />
|
||||
<Preloader>
|
||||
{children}
|
||||
</Preloader>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutWrapper;
|
||||
405
frontEnd/components/shared/layout/Preloader.css
Normal file
405
frontEnd/components/shared/layout/Preloader.css
Normal file
@@ -0,0 +1,405 @@
|
||||
/* Enterprise Preloader Overlay */
|
||||
.gnx-preloader-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999 !important;
|
||||
animation: fadeIn 0.4s ease-in;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Geometric Background Pattern */
|
||||
.gnx-preloader-bg-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
animation: patternMove 20s linear infinite;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gnx-preloader-bg-pattern::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.gnx-preloader-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Professional Logo Styling */
|
||||
.gnx-preloader-logo {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gnx-logo-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8) 0%, rgba(15, 23, 42, 0.8) 100%);
|
||||
border-radius: 20px;
|
||||
overflow: visible;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.gnx-logo-border {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899);
|
||||
border-radius: 22px;
|
||||
opacity: 0.5;
|
||||
z-index: -1;
|
||||
animation: borderGlow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.gnx-logo-image {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
filter: brightness(1.2) contrast(1.1);
|
||||
transition: all 0.3s ease;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Enterprise Branding */
|
||||
.gnx-enterprise-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gnx-brand-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #f1f5f9;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.gnx-brand-subtitle {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Professional Progress Container */
|
||||
.gnx-progress-container {
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.gnx-progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.gnx-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.gnx-progress-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: progressShine 2s infinite;
|
||||
}
|
||||
|
||||
.gnx-progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gnx-progress-text {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.gnx-progress-percentage {
|
||||
font-size: 1rem;
|
||||
color: #f1f5f9;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Courier New', monospace;
|
||||
min-width: 45px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Professional Loading Indicator */
|
||||
.gnx-loading-indicator {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.gnx-spinner {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.gnx-spinner-ring {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spinRing 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
||||
}
|
||||
|
||||
.gnx-spinner-ring:nth-child(1) {
|
||||
border-top-color: #3b82f6;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.gnx-spinner-ring:nth-child(2) {
|
||||
border-top-color: #8b5cf6;
|
||||
animation-delay: 0.2s;
|
||||
width: 75%;
|
||||
height: 75%;
|
||||
top: 12.5%;
|
||||
left: 12.5%;
|
||||
}
|
||||
|
||||
.gnx-spinner-ring:nth-child(3) {
|
||||
border-top-color: #ec4899;
|
||||
animation-delay: 0.4s;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
}
|
||||
|
||||
/* Corporate Footer */
|
||||
.gnx-preloader-footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
|
||||
.gnx-footer-text {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Content visibility */
|
||||
.gnx-content-hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
.gnx-content-visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
animation: contentFadeIn 0.5s ease-in;
|
||||
}
|
||||
|
||||
/* Enterprise Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes contentFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes patternMove {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderGlow {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
filter: blur(10px);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
filter: blur(15px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progressShine {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinRing {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.gnx-preloader-container {
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.gnx-logo-wrapper {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.gnx-logo-image {
|
||||
width: 75px !important;
|
||||
height: 56px !important;
|
||||
}
|
||||
|
||||
.gnx-brand-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.gnx-brand-subtitle {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.gnx-progress-container {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.gnx-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.gnx-preloader-container {
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.gnx-logo-wrapper {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.gnx-logo-image {
|
||||
width: 60px !important;
|
||||
height: 45px !important;
|
||||
}
|
||||
|
||||
.gnx-brand-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.gnx-brand-subtitle {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.gnx-progress-container {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.gnx-spinner {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.gnx-footer-text {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
151
frontEnd/components/shared/layout/Preloader.tsx
Normal file
151
frontEnd/components/shared/layout/Preloader.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import "./Preloader.css";
|
||||
|
||||
interface PreloaderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Preloader = ({ children }: PreloaderProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const pathname = usePathname();
|
||||
|
||||
// Initial mount - show preloader on first load
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
setIsLoading(true);
|
||||
setProgress(0);
|
||||
|
||||
// Simulate loading progress
|
||||
const progressInterval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
return 90;
|
||||
}
|
||||
return prev + Math.random() * 15 + 5;
|
||||
});
|
||||
}, 50);
|
||||
|
||||
// Complete loading after minimum duration
|
||||
const completeTimer = setTimeout(() => {
|
||||
setProgress(100);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 200);
|
||||
}, 600);
|
||||
|
||||
return () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(completeTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle route changes
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
|
||||
// Show preloader on route change
|
||||
setIsLoading(true);
|
||||
setProgress(0);
|
||||
|
||||
// Simulate loading progress
|
||||
const progressInterval = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
clearInterval(progressInterval);
|
||||
return 90;
|
||||
}
|
||||
return prev + Math.random() * 20 + 10;
|
||||
});
|
||||
}, 40);
|
||||
|
||||
// Complete loading
|
||||
const completeTimer = setTimeout(() => {
|
||||
setProgress(100);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 150);
|
||||
}, 400);
|
||||
|
||||
return () => {
|
||||
clearInterval(progressInterval);
|
||||
clearTimeout(completeTimer);
|
||||
};
|
||||
}, [pathname, isMounted]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className="gnx-preloader-overlay">
|
||||
{/* Geometric background pattern */}
|
||||
<div className="gnx-preloader-bg-pattern"></div>
|
||||
|
||||
<div className="gnx-preloader-container">
|
||||
{/* Logo with professional wrapper */}
|
||||
<div className="gnx-preloader-logo">
|
||||
<div className="gnx-logo-wrapper">
|
||||
<div className="gnx-logo-border"></div>
|
||||
<Image
|
||||
src="/images/logo.png"
|
||||
alt="GNX Logo"
|
||||
width={100}
|
||||
height={75}
|
||||
className="gnx-logo-image"
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enterprise branding */}
|
||||
<div className="gnx-enterprise-brand">
|
||||
<h1 className="gnx-brand-title">GNX Enterprise</h1>
|
||||
<p className="gnx-brand-subtitle">Digital Transformation Solutions</p>
|
||||
</div>
|
||||
|
||||
{/* Professional progress indicator */}
|
||||
<div className="gnx-progress-container">
|
||||
<div className="gnx-progress-bar">
|
||||
<div
|
||||
className="gnx-progress-fill"
|
||||
style={{ width: `${progress}%` }}
|
||||
>
|
||||
<div className="gnx-progress-shine"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gnx-progress-info">
|
||||
<span className="gnx-progress-text">Loading</span>
|
||||
<span className="gnx-progress-percentage">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Professional loading indicator */}
|
||||
<div className="gnx-loading-indicator">
|
||||
<div className="gnx-spinner">
|
||||
<div className="gnx-spinner-ring"></div>
|
||||
<div className="gnx-spinner-ring"></div>
|
||||
<div className="gnx-spinner-ring"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corporate footer */}
|
||||
<div className="gnx-preloader-footer">
|
||||
<p className="gnx-footer-text">Powered by Advanced Technology</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={isLoading ? "gnx-content-hidden" : "gnx-content-visible"}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preloader;
|
||||
40
frontEnd/components/shared/layout/ScrollToTop.tsx
Normal file
40
frontEnd/components/shared/layout/ScrollToTop.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const ScrollToTop = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Aggressive scroll to top - run immediately and synchronously
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
};
|
||||
|
||||
// 1. Immediate execution
|
||||
scrollToTop();
|
||||
|
||||
// 2. After microtask
|
||||
Promise.resolve().then(scrollToTop);
|
||||
|
||||
// 3. After next frame
|
||||
requestAnimationFrame(scrollToTop);
|
||||
|
||||
// 4. Multiple delayed attempts to override any smooth scroll libraries
|
||||
const timeouts = [0, 10, 50, 100, 150, 200].map(delay =>
|
||||
setTimeout(scrollToTop, delay)
|
||||
);
|
||||
|
||||
return () => {
|
||||
timeouts.forEach(clearTimeout);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
|
||||
37
frontEnd/components/shared/layout/animations/AppearDown.tsx
Normal file
37
frontEnd/components/shared/layout/animations/AppearDown.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
|
||||
const AppearDown = () => {
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= 992) {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
const appearDownSections = document.querySelectorAll(".appear-down");
|
||||
appearDownSections.forEach((section) => {
|
||||
gsap.fromTo(
|
||||
section,
|
||||
{
|
||||
scale: 0.8,
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
scrub: 1,
|
||||
start: "top bottom",
|
||||
end: "bottom center",
|
||||
markers: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default AppearDown;
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const ButtonHoverAnimation = () => {
|
||||
useEffect(() => {
|
||||
const btnAnim = document.querySelectorAll(".btn-anim");
|
||||
if (btnAnim.length > 0) {
|
||||
btnAnim.forEach((element) => {
|
||||
element.addEventListener("mouseenter", handleMouseEnter);
|
||||
element.addEventListener("mouseleave", handleMouseLeave);
|
||||
});
|
||||
|
||||
return () => {
|
||||
btnAnim.forEach((element) => {
|
||||
element.removeEventListener("mouseenter", handleMouseEnter);
|
||||
element.removeEventListener("mouseleave", handleMouseLeave);
|
||||
});
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = (e: any) => {
|
||||
const element = e.currentTarget as any;
|
||||
const span = element.querySelector("span");
|
||||
if (span) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
span.style.left = `${e.clientX - rect.left}px`;
|
||||
span.style.top = `${e.clientY - rect.top}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: any) => {
|
||||
const element = e.currentTarget as HTMLElement;
|
||||
const span = element.querySelector("span");
|
||||
if (span) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
span.style.left = `${e.clientX - rect.left}px`;
|
||||
span.style.top = `${e.clientY - rect.top}px`;
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ButtonHoverAnimation;
|
||||
122
frontEnd/components/shared/layout/animations/FadeAnimations.tsx
Normal file
122
frontEnd/components/shared/layout/animations/FadeAnimations.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
|
||||
const FadeAnimations = () => {
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= 992) {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
const fadeWrapperRefs = document.querySelectorAll(".fade-wrapper");
|
||||
fadeWrapperRefs.forEach((fadeWrapperRef) => {
|
||||
const fadeItems = fadeWrapperRef.querySelectorAll(".fade-top");
|
||||
const fadeItemsBottom = fadeWrapperRef.querySelectorAll(".fade-bottom");
|
||||
const fadeItemsLeft = fadeWrapperRef.querySelectorAll(".fade-left");
|
||||
const fadeItemsRight = fadeWrapperRef.querySelectorAll(".fade-right");
|
||||
|
||||
// from top
|
||||
fadeItems.forEach((element, index) => {
|
||||
const delay = index * 0.15;
|
||||
gsap.set(element, {
|
||||
opacity: 0,
|
||||
y: 100,
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: "top 100%",
|
||||
end: "bottom 20%",
|
||||
scrub: 0.5,
|
||||
onEnter: () => {
|
||||
gsap.to(element, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 1,
|
||||
delay: delay,
|
||||
});
|
||||
},
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
||||
// from bottom
|
||||
fadeItemsBottom.forEach((element, index) => {
|
||||
const delay = index * 0.15;
|
||||
gsap.set(element, {
|
||||
opacity: 0,
|
||||
y: -100,
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: "top 100%",
|
||||
end: "bottom 20%",
|
||||
scrub: 0.5,
|
||||
onEnter: () => {
|
||||
gsap.to(element, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 1,
|
||||
delay: delay,
|
||||
});
|
||||
},
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
||||
// from left
|
||||
fadeItemsLeft.forEach((element, index) => {
|
||||
const delay = index * 0.15;
|
||||
gsap.set(element, {
|
||||
opacity: 0,
|
||||
x: 100,
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: "top 100%",
|
||||
end: "bottom 20%",
|
||||
scrub: 0.5,
|
||||
onEnter: () => {
|
||||
gsap.to(element, {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
duration: 1,
|
||||
delay: delay,
|
||||
});
|
||||
},
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
|
||||
// from right
|
||||
fadeItemsRight.forEach((element, index) => {
|
||||
const delay = index * 0.15;
|
||||
gsap.set(element, {
|
||||
opacity: 0,
|
||||
x: -100,
|
||||
});
|
||||
|
||||
ScrollTrigger.create({
|
||||
trigger: element,
|
||||
start: "top 100%",
|
||||
end: "bottom 20%",
|
||||
scrub: 0.5,
|
||||
onEnter: () => {
|
||||
gsap.to(element, {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
duration: 1,
|
||||
delay: delay,
|
||||
});
|
||||
},
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default FadeAnimations;
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
|
||||
const FadeImageBottom = () => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
const deviceWidth = window.innerWidth;
|
||||
|
||||
if (
|
||||
document.querySelectorAll(".fade-img").length > 0 &&
|
||||
deviceWidth >= 992
|
||||
) {
|
||||
gsap.utils.toArray(".fade-img").forEach((el: any) => {
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "center center",
|
||||
end: "+=40%",
|
||||
scrub: 1,
|
||||
pin: false,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
|
||||
tl.to(el, {
|
||||
y: "120px",
|
||||
zIndex: "-1",
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default FadeImageBottom;
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
|
||||
const ParallaxImage = () => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const imageParallax = document.querySelectorAll(".parallax-image");
|
||||
|
||||
if (imageParallax.length > 0) {
|
||||
imageParallax.forEach((element) => {
|
||||
const animImageParallax = element as HTMLElement;
|
||||
const aipWrap = animImageParallax.closest(
|
||||
".parallax-image-wrap"
|
||||
) as HTMLElement;
|
||||
const aipInner = aipWrap?.querySelector(".parallax-image-inner");
|
||||
|
||||
if (aipWrap && aipInner) {
|
||||
let tl_ImageParallax = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: aipWrap,
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
|
||||
tl_ImageParallax.to(animImageParallax, {
|
||||
yPercent: 30,
|
||||
ease: "none",
|
||||
});
|
||||
gsap.fromTo(
|
||||
aipInner,
|
||||
{
|
||||
scale: 1.2,
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
scrollTrigger: {
|
||||
trigger: aipWrap,
|
||||
start: "top 99%",
|
||||
markers: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
ScrollTrigger.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ParallaxImage;
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import { ScrollToPlugin } from "gsap/dist/ScrollToPlugin";
|
||||
|
||||
const ScrollToElement = () => {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
|
||||
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget.getAttribute("href");
|
||||
if (target) {
|
||||
gsap.to(window, {
|
||||
scrollTo: {
|
||||
y: target,
|
||||
offsetY: 200,
|
||||
},
|
||||
duration: 1.5,
|
||||
ease: "power3.inOut",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const links = document.querySelectorAll('a[href^="#"]');
|
||||
links.forEach((anchor: any) => {
|
||||
anchor.addEventListener("click", handleLinkClick);
|
||||
});
|
||||
|
||||
return () => {
|
||||
links.forEach((anchor: any) => {
|
||||
anchor.removeEventListener("click", handleLinkClick);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ScrollToElement;
|
||||
107
frontEnd/components/shared/layout/animations/SmoothScroll.tsx
Normal file
107
frontEnd/components/shared/layout/animations/SmoothScroll.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import Lenis from "lenis";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const SmoothScroll = () => {
|
||||
const lenisRef = useRef<Lenis | null>(null);
|
||||
const pathname = usePathname();
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
// Handle pathname changes - PRIORITY 1
|
||||
useEffect(() => {
|
||||
setIsNavigating(true);
|
||||
|
||||
// Stop Lenis completely
|
||||
if (lenisRef.current) {
|
||||
lenisRef.current.stop();
|
||||
lenisRef.current.scrollTo(0, { immediate: true, force: true, lock: true });
|
||||
}
|
||||
|
||||
// Force scroll to top with all methods
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
|
||||
// Keep forcing scroll for a brief period
|
||||
const forceScroll = () => {
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
};
|
||||
|
||||
// Force scroll every 16ms (one frame) for 200ms
|
||||
const intervalId = setInterval(forceScroll, 16);
|
||||
|
||||
// After navigation is settled, restart Lenis
|
||||
const restartTimeout = setTimeout(() => {
|
||||
clearInterval(intervalId);
|
||||
|
||||
if (lenisRef.current) {
|
||||
lenisRef.current.scrollTo(0, { immediate: true, force: true });
|
||||
lenisRef.current.start();
|
||||
}
|
||||
|
||||
setIsNavigating(false);
|
||||
|
||||
// Final scroll enforcement
|
||||
window.scrollTo(0, 0);
|
||||
document.documentElement.scrollTop = 0;
|
||||
document.body.scrollTop = 0;
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(restartTimeout);
|
||||
};
|
||||
}, [pathname]);
|
||||
|
||||
// Initialize Lenis - PRIORITY 2
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
orientation: 'vertical',
|
||||
gestureOrientation: 'vertical',
|
||||
smoothWheel: true,
|
||||
wheelMultiplier: 1,
|
||||
smoothTouch: false,
|
||||
touchMultiplier: 2,
|
||||
infinite: false,
|
||||
});
|
||||
|
||||
lenisRef.current = lenis;
|
||||
|
||||
// Force initial scroll to top
|
||||
lenis.scrollTo(0, { immediate: true, force: true });
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
// Connect to GSAP ticker
|
||||
const tickerCallback = (time: number) => {
|
||||
if (!isNavigating) {
|
||||
lenis.raf(time * 350);
|
||||
}
|
||||
};
|
||||
|
||||
gsap.ticker.add(tickerCallback);
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
// Sync with ScrollTrigger
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
|
||||
return () => {
|
||||
lenis.destroy();
|
||||
gsap.ticker.remove(tickerCallback);
|
||||
lenisRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SmoothScroll;
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||
import SplitType from "split-type";
|
||||
|
||||
const SplitTextAnimations = () => {
|
||||
useEffect(() => {
|
||||
if (window.innerWidth >= 992) {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
new SplitType(".title-anim", {
|
||||
types: ["chars", "words"],
|
||||
});
|
||||
|
||||
const titleAnims = document.querySelectorAll(".title-anim");
|
||||
titleAnims.forEach((titleAnim) => {
|
||||
const charElements = titleAnim.querySelectorAll(".char");
|
||||
|
||||
charElements.forEach((char, index) => {
|
||||
const tl2 = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: char,
|
||||
start: "top 90%",
|
||||
end: "bottom 60%",
|
||||
scrub: false,
|
||||
markers: false,
|
||||
toggleActions: "play none none none",
|
||||
},
|
||||
});
|
||||
|
||||
const charDelay = index * 0.03;
|
||||
|
||||
tl2.from(char, {
|
||||
duration: 0.8,
|
||||
x: 70,
|
||||
delay: charDelay,
|
||||
autoAlpha: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const titleElements = document.querySelectorAll(".title-anim");
|
||||
|
||||
titleElements.forEach((el) => {
|
||||
const triggerEl = el as gsap.DOMTarget;
|
||||
gsap.to(triggerEl, {
|
||||
scrollTrigger: {
|
||||
trigger: triggerEl,
|
||||
start: "top 100%",
|
||||
markers: false,
|
||||
onEnter: () => {
|
||||
if (el instanceof Element) {
|
||||
el.classList.add("title-anim-active");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SplitTextAnimations;
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
import VanillaTilt from "vanilla-tilt";
|
||||
|
||||
const VanillaTiltHover = () => {
|
||||
const tiltSelectors = [".btn-anim", ".topy-tilt"];
|
||||
const tiltElements = document.querySelectorAll(tiltSelectors.join(", "));
|
||||
|
||||
tiltElements.forEach((element) => {
|
||||
const tiltElement = element as HTMLElement;
|
||||
let tiltConfig: any = {
|
||||
speed: 3000,
|
||||
};
|
||||
|
||||
if (tiltElement.classList.contains("btn-anim")) {
|
||||
tiltConfig = {
|
||||
...tiltConfig,
|
||||
max: 15,
|
||||
perspective: 400,
|
||||
};
|
||||
} else if (tiltElement.classList.contains("topy-tilt")) {
|
||||
tiltConfig = {
|
||||
...tiltConfig,
|
||||
max: 5,
|
||||
};
|
||||
}
|
||||
|
||||
VanillaTilt.init(tiltElement, tiltConfig);
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default VanillaTiltHover;
|
||||
267
frontEnd/components/shared/layout/footer/Footer.tsx
Normal file
267
frontEnd/components/shared/layout/footer/Footer.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import Image from "next/legacy/image";
|
||||
import location from "@/public/images/footer/location.png";
|
||||
import phone from "@/public/images/footer/phone.png";
|
||||
import gmail from "@/public/images/footer/gmail.png";
|
||||
import { useNavigationServices } from "@/lib/hooks/useServices";
|
||||
import { useJobs } from "@/lib/hooks/useCareer";
|
||||
|
||||
const Footer = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const { services: dynamicServices, loading: servicesLoading } = useNavigationServices();
|
||||
const { jobs, loading: jobsLoading } = useJobs();
|
||||
|
||||
// Static header data
|
||||
const headerData = {
|
||||
title: "GNX Soft Ltd.",
|
||||
logoUrl: "/images/logo.png",
|
||||
logoLightUrl: "/images/logo-light.png",
|
||||
navigationType: "both",
|
||||
headerClass: "tp-header",
|
||||
scrolledClass: "navbar-active",
|
||||
buttonText: "Let's Talk",
|
||||
buttonUrl: "/contact-us",
|
||||
buttonClass: "btn btn-primary d-none d-sm-flex",
|
||||
isActive: true,
|
||||
displayOrder: 1,
|
||||
metaData: JSON.stringify({
|
||||
mobileBreakpoint: 992,
|
||||
scrollThreshold: 50,
|
||||
hideOnMobile: false,
|
||||
mobileFirst: true,
|
||||
hamburgerMenu: true
|
||||
})
|
||||
};
|
||||
|
||||
// Get logo URL from static data
|
||||
const logoSrc = headerData.logoUrl;
|
||||
|
||||
return (
|
||||
<footer className="footer position-relative overflow-x-clip">
|
||||
<div className="container">
|
||||
{/* Enterprise Footer Logo Section */}
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="footer-logo-section text-center pt-40 pb-30">
|
||||
<div className="enterprise-logo-container">
|
||||
<div className="enterprise-security-badges">
|
||||
{/* Left Badge */}
|
||||
<div className="security-badges-left">
|
||||
<div className="security-badge">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
<span>Enterprise Solutions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center Logo */}
|
||||
<div className="logo-center">
|
||||
<Link href="/" aria-label="go to home" className="footer-logo">
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt="Logo"
|
||||
width={120}
|
||||
height={90}
|
||||
className="footer-logo-image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right Badge */}
|
||||
<div className="security-badges-right">
|
||||
<div className="security-badge">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
<span>Incident Management</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12">
|
||||
<div className="pt-40">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row vertical-column-gap-lg pt-40">
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Company</h6>
|
||||
<ul className="footer-links">
|
||||
<li><Link href="about-us">About Us</Link></li>
|
||||
<li><Link href="career">Careers</Link></li>
|
||||
<li><Link href="case-study">Success Stories</Link></li>
|
||||
<li><Link href="contact-us">Contact Us</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Services</h6>
|
||||
<ul className="footer-links">
|
||||
{servicesLoading ? (
|
||||
<>
|
||||
<li><Link href="/services">Our Services</Link></li>
|
||||
</>
|
||||
) : (
|
||||
dynamicServices.slice(0, 6).map((service) => (
|
||||
<li key={service.slug}>
|
||||
<Link href={`/services/${service.slug}`}>
|
||||
{service.title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Latest Jobs</h6>
|
||||
<ul className="footer-links">
|
||||
{jobsLoading ? (
|
||||
<>
|
||||
<li><Link href="/career">View All Jobs</Link></li>
|
||||
</>
|
||||
) : (
|
||||
jobs.slice(0, 4).map((job) => (
|
||||
<li key={job.slug}>
|
||||
<Link href={`/career/${job.slug}`}>
|
||||
{job.title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Support</h6>
|
||||
<ul className="footer-links">
|
||||
<li><Link href="/support-center">Support Center</Link></li>
|
||||
<li><Link href="/policy?type=privacy">Privacy Policy</Link></li>
|
||||
<li><Link href="/policy?type=terms">Terms of Use</Link></li>
|
||||
<li><Link href="/policy?type=support">Support Policy</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-4 col-md-12">
|
||||
<div className="footer-cta-section">
|
||||
<div className="cta-content">
|
||||
<h6 className="text-white fm fw-6 mb-24">Ready to Transform?</h6>
|
||||
<p className="text-white mb-30">Start your software journey with our incident management and custom development solutions.</p>
|
||||
<Link href="contact-us" className="btn-anim">
|
||||
Start Your Software Journey
|
||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
||||
<span></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="footer__inner pt-60">
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="footer__inner-single">
|
||||
<div className="thumb">
|
||||
<Image src={location} alt="Image" width={24} height={24} />
|
||||
</div>
|
||||
<div className="content">
|
||||
<h5 className="mt-8 fm fw-6 text-white mb-24">
|
||||
Location
|
||||
</h5>
|
||||
<p className="text-quinary">
|
||||
<Link
|
||||
href="https://maps.google.com/?q=42.496781103070504,27.4758968970689"
|
||||
target="_blank"
|
||||
>
|
||||
GNX Soft Ltd.<br />
|
||||
Tsar Simeon I, 56<br />
|
||||
Burgas, Burgas 8000<br />
|
||||
Bulgaria
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="footer__inner-single">
|
||||
<div className="thumb">
|
||||
<Image src={phone} alt="Image" width={24} height={24} />
|
||||
</div>
|
||||
<div className="content">
|
||||
<h5 className="mt-8 fm fw-6 text-white mb-24">Phone</h5>
|
||||
<p className="text-quinary mb-12">
|
||||
<Link href="tel:+359897338147">+359 897 338 147</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-lg-4">
|
||||
<div className="footer__inner-single">
|
||||
<div className="thumb">
|
||||
<Image src={gmail} alt="Image" width={24} height={24} />
|
||||
</div>
|
||||
<div className="content">
|
||||
<h5 className="mt-8 fm fw-6 text-white mb-24">Email</h5>
|
||||
<p className="text-quinary mb-12 text-lowercase">
|
||||
<Link href="mailto:info@gnxsoft.com">
|
||||
info@gnxsoft.com
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="footer-copyright">
|
||||
<div className="row align-items-center vertical-column-gap">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="footer__copyright-text text-center text-lg-start">
|
||||
<p className="text-quinary mt-8">
|
||||
© <span id="copyrightYear">{currentYear}</span>{" "}
|
||||
<Link href="/" className="fw-6">
|
||||
GNX
|
||||
</Link>
|
||||
. All rights reserved. GNX Software Solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="social justify-content-center justify-content-lg-end">
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
266
frontEnd/components/shared/layout/header/Header.tsx
Normal file
266
frontEnd/components/shared/layout/header/Header.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import OffcanvasMenu from "./OffcanvasMenu";
|
||||
import { OffcanvasData } from "@/public/data/offcanvas-data";
|
||||
import { useNavigationServices } from "@/lib/hooks/useServices";
|
||||
|
||||
const Header = () => {
|
||||
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
// Fetch services from API
|
||||
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
|
||||
|
||||
// Create dynamic navigation data with services from API
|
||||
const navigationData = useMemo(() => {
|
||||
const baseNavigation = [...OffcanvasData];
|
||||
|
||||
// Find the Services menu item and update its submenu with API data
|
||||
const servicesIndex = baseNavigation.findIndex(item => item.title === "Services");
|
||||
if (servicesIndex !== -1 && apiServices.length > 0) {
|
||||
baseNavigation[servicesIndex] = {
|
||||
...baseNavigation[servicesIndex],
|
||||
submenu: apiServices.map(service => ({
|
||||
id: service.id + 1000, // Offset to avoid conflicts with existing IDs
|
||||
title: service.title,
|
||||
path: `/services/${service.slug}`,
|
||||
parent_id: baseNavigation[servicesIndex].id,
|
||||
display_order: service.display_order,
|
||||
created_at: service.created_at,
|
||||
updated_at: service.updated_at
|
||||
}))
|
||||
} as any;
|
||||
}
|
||||
|
||||
return baseNavigation;
|
||||
}, [apiServices]);
|
||||
|
||||
// Static header data
|
||||
const headerData = {
|
||||
title: "EnterpriseSoft Solutions",
|
||||
logoUrl: "/images/logo.png",
|
||||
logoLightUrl: "/images/logo-light.png",
|
||||
navigationType: "both",
|
||||
headerClass: "tp-header",
|
||||
scrolledClass: "navbar-active",
|
||||
buttonText: "Support Center",
|
||||
buttonUrl: "/support-center",
|
||||
buttonClass: "btn btn-primary d-none d-sm-flex",
|
||||
isActive: true,
|
||||
displayOrder: 1,
|
||||
metaData: JSON.stringify({
|
||||
mobileBreakpoint: 992,
|
||||
scrollThreshold: 50,
|
||||
hideOnMobile: false,
|
||||
mobileFirst: true,
|
||||
hamburgerMenu: true
|
||||
})
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setTimeout(() => {
|
||||
setIsOffcanvasOpen(false);
|
||||
}, 900);
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
const handleDropdownToggle = (index: number) => {
|
||||
setOpenDropdown(openDropdown === index ? null : index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollPosition = window.scrollY;
|
||||
if (scrollPosition > 50) {
|
||||
setScrolled(true);
|
||||
} else {
|
||||
setScrolled(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [scrolled]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobile(window.innerWidth < 992);
|
||||
setTimeout(() => {
|
||||
setIsOffcanvasOpen(false);
|
||||
}, 900);
|
||||
setIsActive(false);
|
||||
};
|
||||
|
||||
handleResize(); // Check on mount
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
// Use static data
|
||||
let logoSrc: string = headerData.logoUrl;
|
||||
let headerClass = headerData.headerClass;
|
||||
let buttonText = headerData.buttonText;
|
||||
let buttonUrl = headerData.buttonUrl;
|
||||
let buttonClass = headerData.buttonClass;
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
// Override logo based on pathname if needed (maintain existing behavior)
|
||||
if (
|
||||
pathname === "/career" ||
|
||||
pathname === "/" ||
|
||||
pathname === "/index" ||
|
||||
pathname === "/services" ||
|
||||
pathname === "/service-single"
|
||||
) {
|
||||
logoSrc = headerData.logoLightUrl;
|
||||
}
|
||||
|
||||
const handleOffCanvas = () => {
|
||||
setIsOffcanvasOpen(true);
|
||||
setIsActive(true);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={headerClass}>
|
||||
<div className={"primary-navbar" + (scrolled ? " navbar-active" : " ")}>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<nav className="navbar p-0">
|
||||
<div className="navbar__logo">
|
||||
<Link href="/" aria-label="go to home" className="logo-img">
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt="Logo"
|
||||
width={160}
|
||||
height={120}
|
||||
priority
|
||||
className="logo-image"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation Menu */}
|
||||
<div className="navbar__menu d-none d-lg-flex">
|
||||
<ul>
|
||||
{navigationData.map((item) =>
|
||||
item.title === "Support Center" ? null : item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children"
|
||||
key={item.id}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
|
||||
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
|
||||
>
|
||||
<button
|
||||
aria-label="dropdown menu"
|
||||
className={
|
||||
"navbar__dropdown-label" +
|
||||
(openDropdown === item.id
|
||||
? " navbar__item-active"
|
||||
: " ")
|
||||
}
|
||||
onClick={() => isMobile && handleDropdownToggle(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
{item.title === "Services" && servicesLoading && (
|
||||
<span className="loading-indicator">⏳</span>
|
||||
)}
|
||||
</button>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}>
|
||||
{item.title === "Services" && servicesLoading ? (
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
</li>
|
||||
) : item.title === "Services" && servicesError ? (
|
||||
<li>
|
||||
<span className="text-danger">Failed to load services</span>
|
||||
</li>
|
||||
) : (
|
||||
item.submenu.map((subItem, subIndex) => (
|
||||
<li key={subIndex}>
|
||||
<Link
|
||||
href={subItem.path || "#"}
|
||||
className={
|
||||
pathname === subItem.path
|
||||
? " active-current-sub"
|
||||
: " "
|
||||
}
|
||||
>
|
||||
{subItem.title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<li
|
||||
className="navbar__item"
|
||||
key={item.id}
|
||||
>
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
pathname === item.path ? " active-current-link" : " "
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="navbar__options">
|
||||
<Link href={buttonUrl} className={buttonClass}>
|
||||
{buttonText}
|
||||
</Link>
|
||||
<button
|
||||
className="open-offcanvas-nav d-lg-none"
|
||||
aria-label="toggle mobile menu"
|
||||
title="open offcanvas menu"
|
||||
onClick={handleOffCanvas}
|
||||
>
|
||||
<span className="icon-bar top-bar"></span>
|
||||
<span className="icon-bar middle-bar"></span>
|
||||
<span className="icon-bar bottom-bar"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<OffcanvasMenu
|
||||
isOffcanvasOpen={isOffcanvasOpen}
|
||||
handleClick={handleClick}
|
||||
isActive={isActive}
|
||||
navigationData={navigationData}
|
||||
servicesLoading={servicesLoading}
|
||||
servicesError={servicesError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
204
frontEnd/components/shared/layout/header/OffcanvasMenu.tsx
Normal file
204
frontEnd/components/shared/layout/header/OffcanvasMenu.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { OffcanvasData } from "@/public/data/offcanvas-data";
|
||||
import logoLight from "@/public/images/logo-light.png";
|
||||
|
||||
interface OffcanvasMenuProps {
|
||||
isOffcanvasOpen: boolean;
|
||||
isActive: boolean;
|
||||
handleClick: () => void;
|
||||
navigationData?: any[];
|
||||
servicesLoading?: boolean;
|
||||
servicesError?: string | null;
|
||||
}
|
||||
|
||||
const OffcanvasMenu = ({
|
||||
isOffcanvasOpen,
|
||||
isActive,
|
||||
handleClick,
|
||||
navigationData = OffcanvasData,
|
||||
servicesLoading = false,
|
||||
servicesError = null
|
||||
}: OffcanvasMenuProps) => {
|
||||
const [openDropdown, setOpenDropdown] = useState(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
const handleDropdownToggle = (index: any) => {
|
||||
setOpenDropdown((prev) => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const parentItems = document.querySelectorAll(
|
||||
".navbar__item--has-children"
|
||||
);
|
||||
|
||||
parentItems.forEach((parentItem) => {
|
||||
const childItems = parentItem.querySelectorAll(".active-current-sub");
|
||||
|
||||
if (childItems.length > 0) {
|
||||
parentItem.classList.add("active-current-parent");
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="offcanvas-nav">
|
||||
<div
|
||||
className={
|
||||
"offcanvas-menu" + (isOffcanvasOpen ? " show-offcanvas-menu" : " ")
|
||||
}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<nav
|
||||
className={
|
||||
"offcanvas-menu__wrapper" + (isActive ? " " : " nav-fade-active")
|
||||
}
|
||||
data-lenis-prevent
|
||||
>
|
||||
<div className="offcanvas-menu__header nav-fade">
|
||||
<div className="logo">
|
||||
<Link href="/" className="logo-img">
|
||||
<Image src={logoLight} priority alt="Image" title="Logo" width={160} height={60} />
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
aria-label="close offcanvas menu"
|
||||
className="close-offcanvas-menu"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<i className="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="offcanvas-menu__list">
|
||||
<div className="navbar__menu">
|
||||
<ul>
|
||||
{navigationData.map((item, index) =>
|
||||
item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children nav-fade"
|
||||
key={index}
|
||||
>
|
||||
<button
|
||||
aria-label="dropdown menu"
|
||||
className={
|
||||
"navbar__dropdown-label" +
|
||||
(openDropdown === index
|
||||
? " navbar__item-active"
|
||||
: " ")
|
||||
}
|
||||
onClick={() => handleDropdownToggle(index)}
|
||||
>
|
||||
{item.title}
|
||||
{item.title === "Services" && servicesLoading && (
|
||||
<span className="loading-indicator">⏳</span>
|
||||
)}
|
||||
</button>
|
||||
<AnimateHeight
|
||||
duration={400}
|
||||
height={openDropdown === index ? "auto" : 0}
|
||||
>
|
||||
<ul className="navbar__sub-menu">
|
||||
{item.title === "Services" && servicesLoading ? (
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
</li>
|
||||
) : item.title === "Services" && servicesError ? (
|
||||
<li>
|
||||
<span className="text-danger">Failed to load services</span>
|
||||
</li>
|
||||
) : (
|
||||
item.submenu.map((subItem: any, subIndex: number) => (
|
||||
<li key={subIndex}>
|
||||
<Link
|
||||
href={subItem.path || "#"}
|
||||
className={
|
||||
mounted && pathname === subItem.path
|
||||
? " active-current-sub"
|
||||
: " "
|
||||
}
|
||||
>
|
||||
{subItem.title}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</AnimateHeight>
|
||||
</li>
|
||||
) : (
|
||||
<li className="navbar__item nav-fade" key={index}>
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
mounted && pathname === item.path ? " active-current-link" : " "
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="offcanvas-menu__enterprise-info nav-fade">
|
||||
<div className="enterprise-contact">
|
||||
<h4>Get in Touch</h4>
|
||||
<p>Ready to transform your business?</p>
|
||||
<div className="contact-methods">
|
||||
<a href="tel:+359896138030" className="contact-item">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>+359896138030</span>
|
||||
</a>
|
||||
<a href="mailto:info@gnxsoft.com" className="contact-item">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
<span>info@gnxsoft.com</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="enterprise-social nav-fade">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="Connect with us on LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="View our code on GitHub"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="anime">
|
||||
<span className="nav-fade"></span>
|
||||
<span className="nav-fade"></span>
|
||||
<span className="nav-fade"></span>
|
||||
<span className="nav-fade"></span>
|
||||
<span className="nav-fade"></span>
|
||||
<span className="nav-fade"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OffcanvasMenu;
|
||||
378
frontEnd/components/shared/seo/StructuredData.tsx
Normal file
378
frontEnd/components/shared/seo/StructuredData.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
'use client';
|
||||
|
||||
import Script from 'next/script';
|
||||
import { SITE_CONFIG } from '@/lib/seo/metadata';
|
||||
|
||||
// Organization Schema
|
||||
export function OrganizationSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
legalName: `${SITE_CONFIG.name} LLC`,
|
||||
url: SITE_CONFIG.url,
|
||||
logo: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
foundingDate: SITE_CONFIG.foundedYear.toString(),
|
||||
description: SITE_CONFIG.description,
|
||||
email: SITE_CONFIG.email,
|
||||
telephone: SITE_CONFIG.phone,
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: SITE_CONFIG.address.street,
|
||||
addressLocality: SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
postalCode: SITE_CONFIG.address.zip,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
sameAs: [
|
||||
SITE_CONFIG.social.linkedin,
|
||||
SITE_CONFIG.social.github,
|
||||
],
|
||||
contactPoint: [
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Customer Service',
|
||||
email: SITE_CONFIG.email,
|
||||
availableLanguage: ['English'],
|
||||
areaServed: 'Worldwide',
|
||||
},
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Sales',
|
||||
email: `sales@${SITE_CONFIG.email.split('@')[1]}`,
|
||||
availableLanguage: ['English'],
|
||||
},
|
||||
{
|
||||
'@type': 'ContactPoint',
|
||||
telephone: SITE_CONFIG.phone,
|
||||
contactType: 'Technical Support',
|
||||
email: `support@${SITE_CONFIG.email.split('@')[1]}`,
|
||||
availableLanguage: ['English'],
|
||||
areaServed: 'Worldwide',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="organization-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Website Schema
|
||||
export function WebsiteSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: SITE_CONFIG.name,
|
||||
url: SITE_CONFIG.url,
|
||||
description: SITE_CONFIG.description,
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
},
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: `${SITE_CONFIG.url}/search?q={search_term_string}`,
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="website-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Breadcrumb Schema
|
||||
interface BreadcrumbItem {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbSchemaProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
item: `${SITE_CONFIG.url}${item.url}`,
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="breadcrumb-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Service Schema
|
||||
interface ServiceSchemaProps {
|
||||
service: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
category?: { name: string };
|
||||
duration?: string;
|
||||
technologies?: string;
|
||||
deliverables?: string;
|
||||
image?: string | File;
|
||||
image_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ServiceSchema({ service }: ServiceSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: service.title,
|
||||
description: service.description,
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
url: SITE_CONFIG.url,
|
||||
},
|
||||
serviceType: service.category?.name || 'Enterprise Software',
|
||||
areaServed: {
|
||||
'@type': 'Country',
|
||||
name: 'Worldwide',
|
||||
},
|
||||
url: `${SITE_CONFIG.url}/services/${service.slug}`,
|
||||
image: service.image_url ||
|
||||
(typeof service.image === 'string' ? `${SITE_CONFIG.url}${service.image}` : `${SITE_CONFIG.url}/images/service/default.png`),
|
||||
serviceOutput: service.deliverables,
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
ratingCount: '127',
|
||||
bestRating: '5',
|
||||
worstRating: '1',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`service-schema-${service.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Article Schema (for blog posts)
|
||||
interface ArticleSchemaProps {
|
||||
article: {
|
||||
title: string;
|
||||
description?: string;
|
||||
excerpt?: string;
|
||||
slug: string;
|
||||
image?: string;
|
||||
published_at?: string;
|
||||
updated_at?: string;
|
||||
author?: { name: string; image?: string };
|
||||
category?: { name: string };
|
||||
};
|
||||
}
|
||||
|
||||
export function ArticleSchema({ article }: ArticleSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: article.title,
|
||||
description: article.description || article.excerpt,
|
||||
image: article.image
|
||||
? `${SITE_CONFIG.url}${article.image}`
|
||||
: `${SITE_CONFIG.url}/images/blog/default.png`,
|
||||
datePublished: article.published_at,
|
||||
dateModified: article.updated_at || article.published_at,
|
||||
author: {
|
||||
'@type': 'Person',
|
||||
name: article.author?.name || SITE_CONFIG.name,
|
||||
image: article.author?.image,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
},
|
||||
articleSection: article.category?.name,
|
||||
url: `${SITE_CONFIG.url}/insights/${article.slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`article-schema-${article.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// FAQ Schema
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSchemaProps {
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
export function FAQSchema({ faqs }: FAQSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="faq-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Job Posting Schema
|
||||
interface JobPostingSchemaProps {
|
||||
job: {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
location?: string;
|
||||
employment_type?: string;
|
||||
salary_min?: number;
|
||||
salary_max?: number;
|
||||
posted_at?: string;
|
||||
valid_through?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function JobPostingSchema({ job }: JobPostingSchemaProps) {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'JobPosting',
|
||||
title: job.title,
|
||||
description: job.description,
|
||||
datePosted: job.posted_at || new Date().toISOString(),
|
||||
validThrough: job.valid_through,
|
||||
employmentType: job.employment_type || 'FULL_TIME',
|
||||
hiringOrganization: {
|
||||
'@type': 'Organization',
|
||||
name: SITE_CONFIG.name,
|
||||
sameAs: SITE_CONFIG.url,
|
||||
logo: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
},
|
||||
jobLocation: {
|
||||
'@type': 'Place',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: job.location || SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
},
|
||||
baseSalary: job.salary_min &&
|
||||
job.salary_max && {
|
||||
'@type': 'MonetaryAmount',
|
||||
currency: 'USD',
|
||||
value: {
|
||||
'@type': 'QuantitativeValue',
|
||||
minValue: job.salary_min,
|
||||
maxValue: job.salary_max,
|
||||
unitText: 'YEAR',
|
||||
},
|
||||
},
|
||||
url: `${SITE_CONFIG.url}/career/${job.slug}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id={`job-posting-schema-${job.slug}`}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Local Business Schema
|
||||
export function LocalBusinessSchema() {
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfessionalService',
|
||||
name: SITE_CONFIG.name,
|
||||
image: `${SITE_CONFIG.url}/images/logo.png`,
|
||||
'@id': SITE_CONFIG.url,
|
||||
url: SITE_CONFIG.url,
|
||||
telephone: SITE_CONFIG.phone,
|
||||
priceRange: '$$$$',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
streetAddress: SITE_CONFIG.address.street,
|
||||
addressLocality: SITE_CONFIG.address.city,
|
||||
addressRegion: SITE_CONFIG.address.state,
|
||||
postalCode: SITE_CONFIG.address.zip,
|
||||
addressCountry: SITE_CONFIG.address.country,
|
||||
},
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
},
|
||||
openingHoursSpecification: {
|
||||
'@type': 'OpeningHoursSpecification',
|
||||
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||
opens: '09:00',
|
||||
closes: '18:00',
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.9',
|
||||
reviewCount: '127',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="local-business-schema"
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user