update
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Metadata } from "next";
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import CaseSingle from "@/components/pages/case-study/CaseSingle";
|
||||
import Process from "@/components/pages/case-study/Process";
|
||||
@@ -5,6 +6,8 @@ import RelatedCase from "@/components/pages/case-study/RelatedCase";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
import CaseStudyScrollProgressButton from "@/components/pages/case-study/CaseStudyScrollProgressButton";
|
||||
import CaseStudyInitAnimations from "@/components/pages/case-study/CaseStudyInitAnimations";
|
||||
import { generateCaseStudyMetadata } from "@/lib/seo/metadata";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{
|
||||
@@ -12,6 +15,44 @@ interface PageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 3600 }, // Revalidate every hour
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const caseStudy = await response.json();
|
||||
|
||||
return generateCaseStudyMetadata({
|
||||
title: caseStudy.title,
|
||||
description: caseStudy.meta_description || caseStudy.excerpt || caseStudy.description,
|
||||
excerpt: caseStudy.excerpt,
|
||||
slug: caseStudy.slug,
|
||||
image: caseStudy.featured_image || caseStudy.poster_image || caseStudy.thumbnail,
|
||||
client_name: caseStudy.client?.name || caseStudy.client_name,
|
||||
});
|
||||
} catch (error) {
|
||||
// Return default metadata if case study not found
|
||||
return {
|
||||
title: 'Case Study | GNX Soft',
|
||||
description: 'Enterprise case study and success story',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const page = async ({ params }: PageProps) => {
|
||||
const { slug } = await params;
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"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();
|
||||
@@ -16,11 +12,11 @@ const AboutServiceComponent = () => {
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="text-center py-4">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3">Loading service content...</p>
|
||||
<p className="mt-2 text-muted">Loading service content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +31,7 @@ const AboutServiceComponent = () => {
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="text-center py-4">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Error Loading Content</h4>
|
||||
<p>{error}</p>
|
||||
@@ -51,154 +47,103 @@ const AboutServiceComponent = () => {
|
||||
const serviceData = data?.service as AboutServiceType | undefined;
|
||||
const processData = data?.process as AboutProcess | undefined;
|
||||
|
||||
const features = serviceData?.features && serviceData.features.length > 0
|
||||
? serviceData.features
|
||||
: [
|
||||
{ icon: "fa-solid fa-shield-halved", title: "Enterprise Security", description: "Defense-Grade Protection" },
|
||||
{ icon: "fa-solid fa-cloud", title: "Cloud Native", description: "AWS, Azure, GCP Partners" }
|
||||
];
|
||||
|
||||
const steps = processData?.steps && processData.steps.length > 0
|
||||
? processData.steps
|
||||
: [
|
||||
{ step_number: "01", title: "Discovery & Planning", description: "Comprehensive analysis and architecture design" },
|
||||
{ step_number: "02", title: "Development & Testing", description: "Agile development with continuous testing" }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="about-service-section" suppressHydrationWarning>
|
||||
<section className="hero-banner about-service-section" suppressHydrationWarning>
|
||||
<div className="container">
|
||||
<div className="row g-5 align-items-start">
|
||||
{/* Image Column */}
|
||||
<div className="row align-items-center g-4">
|
||||
{/* Left Column - Text Content */}
|
||||
<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>
|
||||
<div className="service-content">
|
||||
{/* Badge */}
|
||||
<div className="hero-badge">
|
||||
<div className="badge-icon">
|
||||
<i className={serviceData?.badge_icon || "fa-solid fa-users"}></i>
|
||||
</div>
|
||||
<span>{serviceData?.badge_text || "About Our Company"}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="luxury-title">
|
||||
|
||||
{/* Main Title */}
|
||||
<h1 className="hero-title">
|
||||
{serviceData?.title || "GNX Soft Ltd. - Software Excellence"}
|
||||
</h2>
|
||||
|
||||
<p className="luxury-description">
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="hero-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">
|
||||
{/* CTA Buttons */}
|
||||
<div className="hero-actions">
|
||||
<Link href={serviceData?.cta_link || "service-single"} className="btn-primary">
|
||||
<span>{serviceData?.cta_text || "Explore Our Solutions"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Feature Boxes */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="service-features-wrapper">
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="service-feature-box">
|
||||
<div className="feature-icon">
|
||||
<i className={feature.icon}></i>
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<div className="feature-title">{feature.title}</div>
|
||||
<div className="feature-description">{feature.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-process-section">
|
||||
<section className="hero-banner 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>
|
||||
<div className="row align-items-center g-4">
|
||||
{/* Left Column - Text Content */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="process-content">
|
||||
{/* Badge */}
|
||||
<div className="hero-badge">
|
||||
<div className="badge-icon">
|
||||
<i className={processData?.badge_icon || "fa-solid fa-cogs"}></i>
|
||||
</div>
|
||||
<span>{processData?.badge_text || "Our Methodology"}</span>
|
||||
</div>
|
||||
|
||||
<h2 className="luxury-title">
|
||||
|
||||
{/* Main Title */}
|
||||
<h1 className="hero-title">
|
||||
{processData?.title || "Enterprise Development Process"}
|
||||
</h2>
|
||||
|
||||
<p className="luxury-description">
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="hero-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">
|
||||
{/* CTA Buttons */}
|
||||
<div className="hero-actions">
|
||||
<Link href={processData?.cta_link || "service-single"} className="btn-primary">
|
||||
<span>{processData?.cta_text || "View Our Services"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
@@ -206,29 +151,20 @@ const AboutServiceComponent = () => {
|
||||
</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>
|
||||
{/* Right Column - Process Step Boxes */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="process-steps-wrapper">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="process-step-box">
|
||||
<div className="step-number-icon">
|
||||
<span>{step.step_number}</span>
|
||||
</div>
|
||||
<div className="step-content">
|
||||
<div className="step-title">{step.title}</div>
|
||||
<div className="step-description">{step.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,326 +172,405 @@ const AboutServiceComponent = () => {
|
||||
</section>
|
||||
|
||||
<style>{`
|
||||
/* Section Base Styles */
|
||||
.about-service-section,
|
||||
.about-process-section {
|
||||
padding: 120px 0;
|
||||
/* Section Base Styles - Transparent Background - Seamless Continuous Flow */
|
||||
.about-service-section {
|
||||
padding: 100px 0 50px 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;
|
||||
background: transparent;
|
||||
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);
|
||||
margin-bottom: 0;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.about-service-section * {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.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-process-section {
|
||||
padding: 50px 0 100px 0;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
margin-top: -1px;
|
||||
padding-top: 51px;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.about-process-section * {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.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-service-section.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.about-image-container:hover .about-image {
|
||||
transform: scale(1.05);
|
||||
.about-process-section.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image-overlay {
|
||||
.about-service-section::before,
|
||||
.about-process-section::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Service/Process Content - Left Column */
|
||||
.service-content,
|
||||
.process-content {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.hero-badge {
|
||||
margin: 0 0 1.25rem 0;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
color: #000000 !important;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.badge-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #8b5cf6 100%);
|
||||
|
||||
i {
|
||||
font-size: 0.7rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: left;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #000000 !important;
|
||||
margin: 0 0 1.5rem 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: flex-start;
|
||||
margin-top: 0;
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.75rem;
|
||||
font-size: 0.875rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
border-color: rgba(212, 175, 55, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(212, 175, 55, 0.2);
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Service Features Wrapper - Right Column */
|
||||
.service-features-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
/* Service Feature Box - Compact Luxury */
|
||||
.service-feature-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.875rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.05) 0%, rgba(139, 92, 246, 0.03) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
border-radius: 10px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.service-feature-box::before {
|
||||
content: '';
|
||||
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;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #d4af37 0%, #8b5cf6 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
/* Content Wrapper - Perfect Alignment */
|
||||
.about-content-wrapper {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
.service-feature-box:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.about-content-wrapper {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
.service-feature-box:hover {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08) 0%, rgba(139, 92, 246, 0.06) 100%);
|
||||
border-color: rgba(212, 175, 55, 0.4);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 20px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
.feature-icon {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #8b5cf6 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.4);
|
||||
border-radius: 8px;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
.feature-icon-wrapper i {
|
||||
font-size: 1.5rem;
|
||||
.service-feature-box:hover .feature-icon {
|
||||
background: linear-gradient(135deg, #f4d03f 0%, #9d6ef7 100%);
|
||||
border-color: rgba(212, 175, 55, 0.6);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
.feature-icon i {
|
||||
font-size: 1rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.125rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #000000 !important;
|
||||
margin: 0 0 0.25rem 0;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: #000000 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Process Steps */
|
||||
.luxury-process-steps {
|
||||
/* Process Steps Wrapper - Right Column */
|
||||
.process-steps-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
margin: 1rem 0;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.luxury-process-step {
|
||||
/* Process Step Box - Compact Luxury */
|
||||
.process-step-box {
|
||||
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);
|
||||
gap: 0.875rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.05) 0%, rgba(139, 92, 246, 0.03) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
border-radius: 10px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
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);
|
||||
.process-step-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #d4af37 0%, #8b5cf6 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.luxury-btn i {
|
||||
transition: transform 0.3s ease;
|
||||
.process-step-box:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.luxury-btn:hover i {
|
||||
.process-step-box:hover {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08) 0%, rgba(139, 92, 246, 0.06) 100%);
|
||||
border-color: rgba(212, 175, 55, 0.4);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 20px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
.step-number-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #8b5cf6 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.4);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
.process-step-box:hover .step-number-icon {
|
||||
background: linear-gradient(135deg, #f4d03f 0%, #9d6ef7 100%);
|
||||
border-color: rgba(212, 175, 55, 0.6);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
.step-number-icon span {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #000000 !important;
|
||||
margin: 0 0 0.25rem 0;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: #000000 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 991px) {
|
||||
.about-service-section,
|
||||
.about-service-section {
|
||||
padding: 80px 0 40px 0;
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
padding: 80px 0;
|
||||
padding: 40px 0 80px 0;
|
||||
padding-top: 41px;
|
||||
}
|
||||
|
||||
.about-image-wrapper {
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.service-content,
|
||||
.process-content {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
.hero-title,
|
||||
.hero-description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-content-wrapper {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
@media (max-width: 767px) {
|
||||
.about-service-section {
|
||||
padding: 60px 0 30px 0;
|
||||
}
|
||||
|
||||
.luxury-features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
.about-process-section {
|
||||
padding: 30px 0 60px 0;
|
||||
padding-top: 31px;
|
||||
}
|
||||
|
||||
.service-content,
|
||||
.process-content {
|
||||
.hero-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.service-feature-box,
|
||||
.process-step-box {
|
||||
padding: 0.875rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-icon,
|
||||
.step-number-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.feature-icon i {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.feature-title,
|
||||
.step-title {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.feature-description,
|
||||
.step-description {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.luxury-title {
|
||||
font-size: 1.75rem;
|
||||
.about-service-section {
|
||||
padding: 50px 0 25px 0;
|
||||
}
|
||||
|
||||
.luxury-description {
|
||||
font-size: 1rem;
|
||||
|
||||
.about-process-section {
|
||||
padding: 25px 0 50px 0;
|
||||
padding-top: 26px;
|
||||
}
|
||||
|
||||
.luxury-feature-card,
|
||||
.luxury-process-step {
|
||||
padding: 1.25rem;
|
||||
|
||||
.service-feature-box,
|
||||
.process-step-box {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-icon,
|
||||
.step-number-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
@@ -564,3 +579,4 @@ const AboutServiceComponent = () => {
|
||||
};
|
||||
|
||||
export default AboutServiceComponent;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"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();
|
||||
@@ -15,11 +12,11 @@ const AboutStarter = () => {
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="text-center py-4">
|
||||
<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>
|
||||
<p className="mt-2 text-muted">Loading journey content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +31,7 @@ const AboutStarter = () => {
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<div className="text-center py-4">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<h4 className="alert-heading">Error Loading Content</h4>
|
||||
<p>{error}</p>
|
||||
@@ -48,101 +45,329 @@ const AboutStarter = () => {
|
||||
}
|
||||
|
||||
const journeyData = data?.journey as AboutJourney | undefined;
|
||||
const milestones = journeyData?.milestones && journeyData.milestones.length > 0
|
||||
? journeyData.milestones
|
||||
: [
|
||||
{ year: "2020", title: "Company Founded", description: "GNX Soft Ltd established in Burgas, Bulgaria" },
|
||||
{ year: "2021", title: "Industry Specialization", description: "Specialized in 8 mission-critical industries" }
|
||||
];
|
||||
|
||||
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>
|
||||
<>
|
||||
<section className="hero-banner about-journey-section" suppressHydrationWarning>
|
||||
<div className="container">
|
||||
<div className="row align-items-center g-4">
|
||||
{/* Left Column - Text Content */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="journey-content">
|
||||
{/* Badge */}
|
||||
<div className="hero-badge">
|
||||
<div className="badge-icon">
|
||||
<i className={journeyData?.badge_icon || "fa-solid fa-rocket"}></i>
|
||||
</div>
|
||||
<span>{journeyData?.badge_text || "Our Journey"}</span>
|
||||
</div>
|
||||
|
||||
{/* Main Title */}
|
||||
<h1 className="hero-title">
|
||||
{journeyData?.title || "Building Enterprise Excellence Since 2020"}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="hero-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>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="hero-actions">
|
||||
<Link href={journeyData?.cta_link || "services"} className="btn-primary">
|
||||
<span>{journeyData?.cta_text || "Explore Solutions"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
</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>
|
||||
{/* Right Column - Milestone Boxes */}
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="journey-milestones-wrapper">
|
||||
{milestones.map((milestone, index) => (
|
||||
<div key={index} className="journey-milestone-box">
|
||||
<div className="milestone-year-icon">
|
||||
<span>{milestone.year}</span>
|
||||
</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 className="milestone-content">
|
||||
<div className="milestone-title">{milestone.title}</div>
|
||||
<div className="milestone-description">{milestone.description}</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>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style>{`
|
||||
/* Section Base Styles - Transparent Background - Seamless Continuous Flow */
|
||||
.about-journey-section {
|
||||
padding: 100px 0;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.about-journey-section * {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.about-journey-section::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about-journey-section.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Journey Content - Left Column */
|
||||
.journey-content {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.hero-badge {
|
||||
margin: 0 0 1.25rem 0;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
color: #000000 !important;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.badge-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #8b5cf6 100%);
|
||||
|
||||
i {
|
||||
font-size: 0.7rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(1.75rem, 3vw, 2.25rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: left;
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
color: #000000 !important;
|
||||
margin: 0 0 1.5rem 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: flex-start;
|
||||
margin-top: 0;
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.75rem;
|
||||
font-size: 0.875rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
border-color: rgba(212, 175, 55, 0.5);
|
||||
box-shadow: 0 8px 24px rgba(212, 175, 55, 0.2);
|
||||
color: #000000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Journey Milestones Wrapper - Right Column */
|
||||
.journey-milestones-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
/* Milestone Box - Compact Luxury */
|
||||
.journey-milestone-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.875rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.05) 0%, rgba(139, 92, 246, 0.03) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
border-radius: 10px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.journey-milestone-box::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #d4af37 0%, #8b5cf6 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.journey-milestone-box:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.journey-milestone-box:hover {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08) 0%, rgba(139, 92, 246, 0.06) 100%);
|
||||
border-color: rgba(212, 175, 55, 0.4);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 20px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
.milestone-year-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #d4af37 0%, #8b5cf6 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.4);
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
.journey-milestone-box:hover .milestone-year-icon {
|
||||
background: linear-gradient(135deg, #f4d03f 0%, #9d6ef7 100%);
|
||||
border-color: rgba(212, 175, 55, 0.6);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
.milestone-year-icon span {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.milestone-content {
|
||||
flex: 1;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.milestone-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #000000 !important;
|
||||
margin: 0 0 0.25rem 0;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.milestone-description {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: #000000 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 991px) {
|
||||
.about-journey-section {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.journey-content {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
.hero-title,
|
||||
.hero-description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.about-journey-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.journey-content {
|
||||
.hero-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.journey-milestone-box {
|
||||
padding: 0.875rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.milestone-year-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.milestone-title {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.milestone-description {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.about-journey-section {
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.journey-milestone-box {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.milestone-year-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent, ChangeEvent } from "react";
|
||||
import { useState, FormEvent, ChangeEvent, useEffect, useRef } from "react";
|
||||
import { JobPosition, careerService } from "@/lib/api/careerService";
|
||||
|
||||
interface JobApplicationFormProps {
|
||||
@@ -62,6 +62,71 @@ const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
type: null,
|
||||
text: "",
|
||||
});
|
||||
|
||||
// Refs for scrolling to messages
|
||||
const messageRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for scroll buttons visibility
|
||||
const [showScrollUp, setShowScrollUp] = useState(false);
|
||||
const [showScrollDown, setShowScrollDown] = useState(false);
|
||||
|
||||
// Check scroll position and update button visibility
|
||||
const checkScrollPosition = () => {
|
||||
if (scrollableRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
|
||||
setShowScrollUp(scrollTop > 100);
|
||||
setShowScrollDown(scrollTop < scrollHeight - clientHeight - 100);
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll functions
|
||||
const scrollUp = () => {
|
||||
if (scrollableRef.current) {
|
||||
scrollableRef.current.scrollBy({
|
||||
top: -300,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const scrollDown = () => {
|
||||
if (scrollableRef.current) {
|
||||
scrollableRef.current.scrollBy({
|
||||
top: 300,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll to message when it appears
|
||||
useEffect(() => {
|
||||
if (message.type && messageRef.current) {
|
||||
setTimeout(() => {
|
||||
messageRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
// Add scroll listener
|
||||
useEffect(() => {
|
||||
const scrollable = scrollableRef.current;
|
||||
if (scrollable) {
|
||||
checkScrollPosition();
|
||||
scrollable.addEventListener('scroll', checkScrollPosition);
|
||||
// Check on resize
|
||||
window.addEventListener('resize', checkScrollPosition);
|
||||
|
||||
return () => {
|
||||
scrollable.removeEventListener('scroll', checkScrollPosition);
|
||||
window.removeEventListener('resize', checkScrollPosition);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
@@ -243,7 +308,7 @@ const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
|
||||
{/* Message */}
|
||||
{message.type && (
|
||||
<div className={`alert alert-${message.type}`}>
|
||||
<div ref={messageRef} className={`alert alert-${message.type}`}>
|
||||
<div className="alert-icon">
|
||||
{message.type === "success" ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -262,8 +327,8 @@ const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="application-form">
|
||||
<div className="form-scrollable">
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="application-form">
|
||||
<div className="form-scrollable" ref={scrollableRef}>
|
||||
{/* Required Fields Section */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
@@ -622,6 +687,33 @@ const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Scroll Navigation Buttons */}
|
||||
{showScrollUp && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollUp}
|
||||
className="scroll-button scroll-button-up"
|
||||
aria-label="Scroll up"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15l-6-6-6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showScrollDown && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollDown}
|
||||
className="scroll-button scroll-button-down"
|
||||
aria-label="Scroll down"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@@ -1120,6 +1212,68 @@ const JobApplicationForm = ({ job, onClose }: JobApplicationFormProps) => {
|
||||
.form-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Scroll Navigation Buttons */
|
||||
.scroll-button {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
animation: fadeInButton 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInButton {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-button:hover {
|
||||
background: #5a6fd8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
|
||||
.scroll-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.scroll-button-up {
|
||||
top: 120px;
|
||||
}
|
||||
|
||||
.scroll-button-down {
|
||||
bottom: 120px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for scroll buttons */
|
||||
@media (max-width: 768px) {
|
||||
.scroll-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.scroll-button-up {
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.scroll-button-down {
|
||||
bottom: 100px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
"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 { useCaseStudies } 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) {
|
||||
if (casesLoading) {
|
||||
return (
|
||||
<section className="fix-top pb-120 c-study">
|
||||
<div className="container">
|
||||
@@ -56,41 +45,12 @@ const CaseItems = () => {
|
||||
</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="c-tab-single active-tab">
|
||||
<div className="row vertical-column-gap-lg">
|
||||
{caseStudiesData.map((caseStudy) => (
|
||||
{caseStudies.map((caseStudy) => (
|
||||
<div key={caseStudy.id} className="col-12 col-lg-6">
|
||||
<div className="c-study-single">
|
||||
<div className="thumb mb-24">
|
||||
@@ -117,63 +77,13 @@ const CaseItems = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{caseStudiesData.length === 0 && (
|
||||
{caseStudies.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>
|
||||
|
||||
@@ -1,26 +1,57 @@
|
||||
"use client";
|
||||
import { use } from 'react';
|
||||
import Image from "next/legacy/image";
|
||||
import { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
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 router = useRouter();
|
||||
const { caseStudy, loading, error } = useCaseStudy(slug);
|
||||
|
||||
const handleGoBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
console.error('Error loading case study:', error);
|
||||
}
|
||||
if (caseStudy) {
|
||||
console.log('Case Study Data:', {
|
||||
title: caseStudy.title,
|
||||
hasCategory: !!caseStudy.category,
|
||||
hasClient: !!caseStudy.client,
|
||||
processStepsCount: caseStudy.process_steps?.length || 0,
|
||||
relatedCaseStudiesCount: caseStudy.related_case_studies?.length || 0,
|
||||
hasPosterImage: !!caseStudy.poster_image,
|
||||
hasProjectImage: !!caseStudy.project_image,
|
||||
hasFeaturedImage: !!caseStudy.featured_image,
|
||||
hasProjectOverview: !!caseStudy.project_overview,
|
||||
hasSiteMapContent: !!caseStudy.site_map_content,
|
||||
fullData: caseStudy
|
||||
});
|
||||
}
|
||||
}, [caseStudy, error]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<section className="case-study-details luxury-case-study fix-top">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center">Loading case study...</p>
|
||||
<div className="loading-state">
|
||||
<div className="loading-spinner"></div>
|
||||
<p className="text-center">Loading case study...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,11 +61,17 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
|
||||
|
||||
if (error || !caseStudy) {
|
||||
return (
|
||||
<section className="c-details fix-top pb-120">
|
||||
<section className="case-study-details luxury-case-study fix-top">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<p className="text-center text-danger">Case study not found.</p>
|
||||
<div className="error-state">
|
||||
<h2>Case Study Not Found</h2>
|
||||
<p>The case study you're looking for doesn't exist or has been removed.</p>
|
||||
<Link href="/case-study" className="btn btn-primary">
|
||||
View All Case Studies
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,100 +80,323 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
<>
|
||||
{/* Hero Banner Section */}
|
||||
<section className="case-study-hero luxury-case-hero fix-top">
|
||||
<div className="hero-background-overlay"></div>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="case-study-hero-content">
|
||||
{/* Category Badge */}
|
||||
{caseStudy.category && (
|
||||
<div className="hero-badge">
|
||||
<i className="fa-solid fa-folder-open"></i>
|
||||
<span>{caseStudy.category.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="hero-title">
|
||||
{caseStudy.title}
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
{caseStudy.subtitle && (
|
||||
<h2 className="hero-subtitle">
|
||||
{caseStudy.subtitle}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{/* Meta Information */}
|
||||
<div className="hero-meta">
|
||||
{caseStudy.client && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
<span>{caseStudy.client.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.published_at && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-calendar"></i>
|
||||
<span>{new Date(caseStudy.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.views_count !== undefined && (
|
||||
<div className="meta-item">
|
||||
<i className="fa-solid fa-eye"></i>
|
||||
<span>{caseStudy.views_count} views</span>
|
||||
</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>
|
||||
|
||||
{/* Hero Image */}
|
||||
{caseStudy.featured_image && (
|
||||
<div className="hero-image-container">
|
||||
<div className="hero-image-wrapper">
|
||||
<Image
|
||||
src={getImageUrl(caseStudy.featured_image)}
|
||||
alt={caseStudy.title}
|
||||
fill
|
||||
className="hero-image"
|
||||
priority
|
||||
sizes="100vw"
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="hero-image-overlay"></div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Main Content Section */}
|
||||
<section className="case-study-details luxury-case-study">
|
||||
<div className="container">
|
||||
{/* Back Button - Top */}
|
||||
<div className="row pt-60">
|
||||
<div className="col-12">
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="btn btn-outline mb-40"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '14px 28px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
border: '2px solid #007bff',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#ffffff',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 2px 8px rgba(0, 123, 255, 0.3)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#0056b3';
|
||||
e.currentTarget.style.borderColor = '#0056b3';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.4)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#007bff';
|
||||
e.currentTarget.style.borderColor = '#007bff';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 123, 255, 0.3)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left"></i>
|
||||
<span>Back to Case Studies</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Overview Section */}
|
||||
<div className="row vertical-column-gap-lg align-items-start">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="case-study-content">
|
||||
<h2 className="section-title">Project Overview</h2>
|
||||
{caseStudy.project_overview ? (
|
||||
<div
|
||||
className="content-html"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.project_overview }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="content-html"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.description || '' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Full Description Content */}
|
||||
{caseStudy.description && (
|
||||
<div
|
||||
className="content-html full-description mt-40"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.description }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="case-study-sidebar">
|
||||
{/* Project Image */}
|
||||
{caseStudy.project_image && (
|
||||
<div className="sidebar-image mb-40">
|
||||
<div className="image-wrapper">
|
||||
<Image
|
||||
src={getImageUrl(caseStudy.project_image)}
|
||||
alt={`${caseStudy.title} - Project`}
|
||||
width={600}
|
||||
height={500}
|
||||
className="project-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Info Card */}
|
||||
<div className="info-card">
|
||||
<h3 className="info-card-title">Project Details</h3>
|
||||
<div className="info-list">
|
||||
{caseStudy.category && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">
|
||||
<i className="fa-solid fa-tag"></i>
|
||||
Category
|
||||
</span>
|
||||
<span className="info-value">{caseStudy.category.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.client && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">
|
||||
<i className="fa-solid fa-building"></i>
|
||||
Client
|
||||
</span>
|
||||
<span className="info-value">{caseStudy.client.name}</span>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.published_at && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">
|
||||
<i className="fa-solid fa-calendar"></i>
|
||||
Published
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{new Date(caseStudy.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{caseStudy.featured && (
|
||||
<div className="info-item">
|
||||
<span className="info-label">
|
||||
<i className="fa-solid fa-star"></i>
|
||||
Status
|
||||
</span>
|
||||
<span className="info-value featured-badge">Featured</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{caseStudy.client?.website && (
|
||||
<div className="info-card-footer">
|
||||
<a
|
||||
href={caseStudy.client.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-outline"
|
||||
>
|
||||
<i className="fa-solid fa-external-link"></i>
|
||||
Visit Client Website
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics/Metrics Section */}
|
||||
{caseStudy.description && caseStudy.description.includes('Key Results') && (
|
||||
<div className="row pt-60">
|
||||
<div className="col-12">
|
||||
<div className="case-study-stats">
|
||||
<h2 className="section-title text-center mb-50">Key Achievements</h2>
|
||||
<div className="stats-grid">
|
||||
{(() => {
|
||||
// Extract metrics from description
|
||||
const resultsMatch = caseStudy.description.match(/<h3>Key Results<\/h3>[\s\S]*?<ul>([\s\S]*?)<\/ul>/i);
|
||||
if (resultsMatch) {
|
||||
const items = resultsMatch[1].match(/<li>([^<]+)<\/li>/g);
|
||||
if (items && items.length > 0) {
|
||||
return items.slice(0, 4).map((item, index) => {
|
||||
const text = item.replace(/<[^>]+>/g, '');
|
||||
const numberMatch = text.match(/(\d+[%$]?)/);
|
||||
const number = numberMatch ? numberMatch[1] : '';
|
||||
const description = text.replace(/\d+[%$]?\s*/, '').trim();
|
||||
return (
|
||||
<div key={index} className="stat-card">
|
||||
<div className="stat-number">{number}</div>
|
||||
<div className="stat-label">{description}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Site Map / Process Section */}
|
||||
{caseStudy.site_map_content && (
|
||||
<div className="row pt-60">
|
||||
<div className="col-12">
|
||||
<div className="case-study-section">
|
||||
<h2 className="section-title">Site Map & Process</h2>
|
||||
<div
|
||||
className="content-html"
|
||||
dangerouslySetInnerHTML={{ __html: caseStudy.site_map_content }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Back Button - Bottom */}
|
||||
<div className="row pt-60 pb-60">
|
||||
<div className="col-12">
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className="btn btn-outline"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '14px 28px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
border: '2px solid #007bff',
|
||||
backgroundColor: '#007bff',
|
||||
color: '#ffffff',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 2px 8px rgba(0, 123, 255, 0.3)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#0056b3';
|
||||
e.currentTarget.style.borderColor = '#0056b3';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.4)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#007bff';
|
||||
e.currentTarget.style.borderColor = '#007bff';
|
||||
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 123, 255, 0.3)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left"></i>
|
||||
<span>Back to Case Studies</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,32 +13,33 @@ const Process = ({ slug }: ProcessProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 tp-process bg-black sticky-wrapper">
|
||||
<section className="case-study-process luxury-process pt-120 pb-120">
|
||||
<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
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="process-content">
|
||||
<h2 className="process-title">
|
||||
Implementation Process
|
||||
</h2>
|
||||
<p className="cur-lg text-quinary">
|
||||
{caseStudy.excerpt}
|
||||
<p className="process-description">
|
||||
{caseStudy.excerpt || 'Our systematic approach ensures successful project delivery.'}
|
||||
</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">
|
||||
<div className="col-12 col-lg-7">
|
||||
<div className="process-steps-list">
|
||||
{caseStudy.process_steps.map((step, index) => (
|
||||
<div key={step.id} className="process-step-item">
|
||||
<div className="step-number">
|
||||
{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 className="step-content">
|
||||
<h4 className="step-title">{step.title}</h4>
|
||||
<p className="step-description">{step.description}</p>
|
||||
</div>
|
||||
{index < caseStudy.process_steps.length - 1 && (
|
||||
<div className="step-connector"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import Image from "next/legacy/image";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
@@ -17,44 +17,45 @@ const RelatedCase = ({ slug }: RelatedCaseProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pt-120 pb-120 c-study fade-wrapper">
|
||||
<section className="related-case-studies luxury-related pt-120 pb-120">
|
||||
<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 className="col-12">
|
||||
<div className="section-header">
|
||||
<h2 className="section-title">Related Case Studies</h2>
|
||||
<p className="section-subtitle">Explore similar projects and success stories</p>
|
||||
</div>
|
||||
</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>
|
||||
{caseStudy.related_case_studies.slice(0, 3).map((relatedCase) => (
|
||||
<div key={relatedCase.id} className="col-12 col-md-6 col-lg-4">
|
||||
<div className="related-case-card">
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
|
||||
<div className="case-image-wrapper">
|
||||
<Image
|
||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
|
||||
className="case-image"
|
||||
alt={relatedCase.title}
|
||||
width={400}
|
||||
height={300}
|
||||
/>
|
||||
<div className="image-overlay">
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</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 className="case-content">
|
||||
{relatedCase.category_name && (
|
||||
<span className="case-category">
|
||||
{relatedCase.category_name}
|
||||
</span>
|
||||
)}
|
||||
<h3 className="case-title">{relatedCase.title}</h3>
|
||||
{relatedCase.excerpt && (
|
||||
<p className="case-excerpt">{relatedCase.excerpt}</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import thumb from "@/public/images/contact-thumb.png";
|
||||
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
|
||||
@@ -31,6 +31,22 @@ const ContactSection = () => {
|
||||
type: 'success' | 'error' | null;
|
||||
message: string;
|
||||
}>({ type: null, message: '' });
|
||||
|
||||
// Refs for scrolling to status messages
|
||||
const statusRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// Scroll to status message when it appears
|
||||
useEffect(() => {
|
||||
if (submitStatus.type && statusRef.current) {
|
||||
setTimeout(() => {
|
||||
statusRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [submitStatus]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
@@ -105,13 +121,13 @@ const ContactSection = () => {
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
"tp-contact pb-120 fade-wrapper" +
|
||||
(isServiceSingle ? " pt-120" : " fix-top")
|
||||
"tp-contact pb-60 fade-wrapper luxury-contact" +
|
||||
(isServiceSingle ? " pt-60" : " fix-top")
|
||||
}
|
||||
>
|
||||
<div className="container">
|
||||
{/* Contact Information Cards */}
|
||||
<div className="row mb-40">
|
||||
<div className="row mb-30">
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="contact-info-card">
|
||||
<div className="contact-info-icon">
|
||||
@@ -119,8 +135,8 @@ const ContactSection = () => {
|
||||
</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>
|
||||
<a href="tel:+359896138030">+359 896 13 80 30</a>
|
||||
<a href="tel:+359897338147">Emergency: +359 897 338 147</a>
|
||||
<span className="contact-hours">Available 24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,20 +169,19 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row vertical-column-gap-md justify-content-between mt-40">
|
||||
<div className="row vertical-column-gap-md justify-content-between mt-30">
|
||||
<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="form-header-compact mb-20">
|
||||
<h2 className="luxury-title">Request Enterprise Consultation</h2>
|
||||
<p className="luxury-subtitle">Fill out the form below to get started</p>
|
||||
</div>
|
||||
<div className="contact-form">
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="enterprise-form luxury-form">
|
||||
<div className="form-section compact-section">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="firstName">First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -180,7 +195,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="lastName">Last Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -196,7 +211,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="email">Business Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
@@ -210,7 +225,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="phone">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
@@ -223,16 +238,9 @@ const ContactSection = () => {
|
||||
</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">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="company">Company Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -246,7 +254,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="jobTitle">Job Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -262,7 +270,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="industry">Industry</label>
|
||||
<select
|
||||
name="industry"
|
||||
@@ -283,7 +291,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="companySize">Company Size</label>
|
||||
<select
|
||||
name="companySize"
|
||||
@@ -301,16 +309,9 @@ const ContactSection = () => {
|
||||
</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">
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="projectType">Project Type</label>
|
||||
<select
|
||||
name="projectType"
|
||||
@@ -318,7 +319,7 @@ const ContactSection = () => {
|
||||
value={formData.projectType}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Project Type</option>
|
||||
<option value="">Select Type</option>
|
||||
<option value="software-development">Software Development</option>
|
||||
<option value="cloud-migration">Cloud Migration</option>
|
||||
<option value="digital-transformation">Digital Transformation</option>
|
||||
@@ -329,9 +330,9 @@ const ContactSection = () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<div className="input-single">
|
||||
<label htmlFor="timeline">Project Timeline</label>
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="timeline">Timeline</label>
|
||||
<select
|
||||
name="timeline"
|
||||
id="timeline"
|
||||
@@ -347,18 +348,16 @@ const ContactSection = () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="input-single">
|
||||
<label htmlFor="budget">Project Budget Range</label>
|
||||
<div className="col-12 col-md-4">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="budget">Budget Range</label>
|
||||
<select
|
||||
name="budget"
|
||||
id="budget"
|
||||
value={formData.budget}
|
||||
onChange={handleInputChange}
|
||||
>
|
||||
<option value="">Select Budget Range</option>
|
||||
<option value="">Select Budget</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>
|
||||
@@ -370,7 +369,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="input-single">
|
||||
<div className="input-single compact-input">
|
||||
<label htmlFor="message">Project Description *</label>
|
||||
<textarea
|
||||
name="message"
|
||||
@@ -378,18 +377,11 @@ const ContactSection = () => {
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Please describe your project requirements, current challenges, and expected outcomes..."
|
||||
rows={5}
|
||||
rows={4}
|
||||
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-group compact-checkbox">
|
||||
<div className="checkbox-single">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -399,7 +391,7 @@ const ContactSection = () => {
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<label htmlFor="newsletter">
|
||||
Subscribe to our newsletter for industry insights and product updates
|
||||
Subscribe to newsletter
|
||||
</label>
|
||||
</div>
|
||||
<div className="checkbox-single">
|
||||
@@ -412,7 +404,7 @@ const ContactSection = () => {
|
||||
required
|
||||
/>
|
||||
<label htmlFor="privacy">
|
||||
I agree to the <a href="/policy?type=privacy">Privacy Policy</a> and consent to being contacted by our team *
|
||||
I agree to the <a href="/policy?type=privacy">Privacy Policy</a> *
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -420,7 +412,10 @@ const ContactSection = () => {
|
||||
|
||||
{/* Status Message */}
|
||||
{submitStatus.type && (
|
||||
<div className={`form-status mt-30 ${submitStatus.type === 'success' ? 'success' : 'error'}`}>
|
||||
<div
|
||||
ref={statusRef}
|
||||
className={`form-status mt-20 ${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>
|
||||
@@ -436,7 +431,7 @@ const ContactSection = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-actions mt-40">
|
||||
<div className="form-actions mt-30">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary enterprise-btn"
|
||||
@@ -465,106 +460,81 @@ const ContactSection = () => {
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-contact__sidebar">
|
||||
<div className="contact-sidebar-card">
|
||||
<div className="tp-contact__sidebar luxury-sidebar">
|
||||
<div className="contact-sidebar-card compact-sidebar-card">
|
||||
<div className="sidebar-header">
|
||||
<h3>Why Choose Our Software Solutions?</h3>
|
||||
<h3>Enterprise Solutions</h3>
|
||||
<div className="enterprise-badge">
|
||||
<i className="fa-solid fa-award"></i>
|
||||
<span>Enterprise Grade</span>
|
||||
<span>Premium</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sidebar-features">
|
||||
<div className="feature-item">
|
||||
<div className="sidebar-features compact-features">
|
||||
<div className="feature-item compact-feature">
|
||||
<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>
|
||||
<p>Real-time monitoring and automated response capabilities.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-item compact-feature">
|
||||
<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>
|
||||
<p>Tailored solutions with dedicated development teams.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<div className="feature-item compact-feature">
|
||||
<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>
|
||||
<h4>System Integrations</h4>
|
||||
<p>Seamless API integration for unified workflows.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-sidebar-card">
|
||||
<h3>Next Steps</h3>
|
||||
<div className="next-steps">
|
||||
<div className="step-item">
|
||||
<div className="contact-sidebar-card compact-sidebar-card">
|
||||
<h3>Quick Process</h3>
|
||||
<div className="next-steps compact-steps">
|
||||
<div className="step-item compact-step">
|
||||
<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>
|
||||
<h4>Consultation</h4>
|
||||
<p>24-hour response time</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-item">
|
||||
<div className="step-item compact-step">
|
||||
<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>
|
||||
<p>Personalized demonstration</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="step-item">
|
||||
<div className="step-item compact-step">
|
||||
<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>
|
||||
<h4>Proposal</h4>
|
||||
<p>Detailed pricing & timeline</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact-sidebar-card">
|
||||
<h3>Find Us</h3>
|
||||
<div className="company-map">
|
||||
<div className="contact-sidebar-card compact-sidebar-card">
|
||||
<h3>Location</h3>
|
||||
<div className="company-map compact-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"
|
||||
height="150"
|
||||
style={{ border: 0, borderRadius: '8px' }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
@@ -572,22 +542,20 @@ const ContactSection = () => {
|
||||
title="Company Location"
|
||||
/>
|
||||
</div>
|
||||
<div className="map-info">
|
||||
<div className="map-info compact-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>
|
||||
<p>Tsar Simeon I, 56, Burgas 8000</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>
|
||||
<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>
|
||||
Directions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useHomeBanners } from "@/lib/hooks/useHome";
|
||||
|
||||
const HomeBanner = () => {
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState(0);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const { data: banners, loading, error } = useHomeBanners();
|
||||
|
||||
// Fix viewport height for mobile browsers (especially iOS Safari)
|
||||
useEffect(() => {
|
||||
@@ -28,70 +30,22 @@ const HomeBanner = () => {
|
||||
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()
|
||||
}
|
||||
];
|
||||
|
||||
// Transform API data to match component format
|
||||
const carouselTexts = useMemo(() => {
|
||||
if (!banners || banners.length === 0) return [];
|
||||
|
||||
return banners.map(banner => ({
|
||||
icon: banner.icon,
|
||||
badge: banner.badge,
|
||||
heading: banner.heading,
|
||||
highlight: banner.highlight,
|
||||
subheading: banner.subheading,
|
||||
description: banner.description,
|
||||
button_text: banner.button_text,
|
||||
button_url: banner.button_url,
|
||||
}));
|
||||
}, [banners]);
|
||||
|
||||
// Carousel rotation effect
|
||||
useEffect(() => {
|
||||
@@ -113,8 +67,57 @@ const HomeBanner = () => {
|
||||
|
||||
const currentText = carouselTexts[currentTextIndex];
|
||||
|
||||
if (!currentText) {
|
||||
return null;
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="modern-banner">
|
||||
<div className="container">
|
||||
<div className="banner-content">
|
||||
<div className="content-center">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p className="mt-3">Loading banner content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<section className="modern-banner">
|
||||
<div className="container">
|
||||
<div className="banner-content">
|
||||
<div className="content-center">
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show no data message when there's no banner content
|
||||
if (!currentText || carouselTexts.length === 0) {
|
||||
return (
|
||||
<section className="modern-banner">
|
||||
<div className="container">
|
||||
<div className="banner-content">
|
||||
<div className="content-center">
|
||||
<h1 className="main-heading">No data available</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -393,7 +396,7 @@ const HomeBanner = () => {
|
||||
<span>{currentText.button_text || "Learn More"}</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
</Link>
|
||||
<Link href="contact-us" className="cta-secondary">
|
||||
<Link href="/contact-us" className="cta-secondary">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>Contact Sales</span>
|
||||
</Link>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||
import { getImageUrl } 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);
|
||||
const [imagesLoaded, setImagesLoaded] = useState<Set<number>>(new Set());
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const itemsRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fetch case studies from API with ordering and limit
|
||||
const params = useMemo(() => ({
|
||||
@@ -18,86 +21,236 @@ const Story = () => {
|
||||
|
||||
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 only API data - no hardcoded fallback
|
||||
const storyData = caseStudies;
|
||||
|
||||
// Preload all images to prevent blinking
|
||||
useEffect(() => {
|
||||
if (storyData.length === 0) return;
|
||||
|
||||
// Use API data if available, otherwise use static data
|
||||
const storyData = caseStudies.length > 0 ? caseStudies : staticStoryData;
|
||||
const preloadImages = () => {
|
||||
// Mark first image as loaded immediately
|
||||
setImagesLoaded((prev) => new Set(prev).add(0));
|
||||
|
||||
storyData.forEach((item, index) => {
|
||||
if (index === 0) return; // Skip first image as it's already marked
|
||||
|
||||
const imageUrl = item.thumbnail ? getImageUrl(item.thumbnail) : '/images/case/one.png';
|
||||
const img = new window.Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
// Small delay to ensure image is fully decoded
|
||||
setTimeout(() => {
|
||||
setImagesLoaded((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(index);
|
||||
return newSet;
|
||||
});
|
||||
}, 50);
|
||||
};
|
||||
img.onerror = () => {
|
||||
// Still mark as loaded to prevent infinite waiting
|
||||
setImagesLoaded((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(index);
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
img.src = imageUrl;
|
||||
});
|
||||
};
|
||||
|
||||
preloadImages();
|
||||
}, [storyData]);
|
||||
|
||||
// Update active image when it becomes loaded
|
||||
useEffect(() => {
|
||||
if (imagesLoaded.has(activeIndex) && activeImageIndex !== activeIndex) {
|
||||
setActiveImageIndex(activeIndex);
|
||||
}
|
||||
}, [imagesLoaded, activeIndex, activeImageIndex]);
|
||||
|
||||
// Log when API data is loaded
|
||||
useEffect(() => {
|
||||
if (caseStudies.length > 0) {
|
||||
if (error) {
|
||||
console.error('Error loading case studies:', error);
|
||||
}
|
||||
}, [caseStudies]);
|
||||
if (caseStudies.length > 0) {
|
||||
console.log('Case studies loaded:', caseStudies.length);
|
||||
}
|
||||
}, [caseStudies, error]);
|
||||
|
||||
// Handle scroll-based active index update and image positioning
|
||||
useEffect(() => {
|
||||
if (!sectionRef.current || storyData.length === 0) return;
|
||||
|
||||
const updateActiveItem = () => {
|
||||
const section = sectionRef.current;
|
||||
const imageContainer = imageContainerRef.current;
|
||||
if (!section || !imageContainer) return;
|
||||
|
||||
// Find which item is most visible
|
||||
let mostVisibleIndex = activeIndex;
|
||||
let maxVisibility = 0;
|
||||
|
||||
itemsRef.current.forEach((item, index) => {
|
||||
if (!item) return;
|
||||
const rect = item.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const itemCenter = rect.top + rect.height / 2;
|
||||
const viewportCenter = viewportHeight / 2;
|
||||
const distanceFromCenter = Math.abs(itemCenter - viewportCenter);
|
||||
const visibility = 1 - (distanceFromCenter / viewportHeight);
|
||||
|
||||
if (visibility > maxVisibility && rect.top < viewportHeight && rect.bottom > 0) {
|
||||
maxVisibility = visibility;
|
||||
mostVisibleIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (mostVisibleIndex !== activeIndex) {
|
||||
setActiveIndex(mostVisibleIndex);
|
||||
// Only switch image if it's loaded
|
||||
if (imagesLoaded.has(mostVisibleIndex) || mostVisibleIndex === 0) {
|
||||
setActiveImageIndex(mostVisibleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Position image container to align with active item
|
||||
const activeItem = itemsRef.current[mostVisibleIndex];
|
||||
if (activeItem && imageContainer) {
|
||||
const sectionTop = section.offsetTop;
|
||||
const itemTop = activeItem.offsetTop;
|
||||
const itemHeight = activeItem.offsetHeight;
|
||||
const containerHeight = imageContainer.offsetHeight || 400;
|
||||
|
||||
// Calculate the offset to center the image with the item
|
||||
// Get the parent container to calculate relative position
|
||||
const contentContainer = activeItem.closest('.tp-story__content');
|
||||
if (contentContainer) {
|
||||
const contentTop = (contentContainer as HTMLElement).offsetTop;
|
||||
const relativeItemTop = itemTop - contentTop;
|
||||
|
||||
// Align image center with item center
|
||||
const offset = relativeItemTop + (itemHeight / 2) - (containerHeight / 2);
|
||||
|
||||
// Apply transform to move the image container
|
||||
imageContainer.style.transform = `translateY(${Math.max(0, offset)}px)`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use Intersection Observer as backup
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-20% 0px -20% 0px',
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1]
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
let maxRatio = 0;
|
||||
let mostVisibleIndex = activeIndex;
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const itemIndex = itemsRef.current.indexOf(entry.target as HTMLDivElement);
|
||||
if (itemIndex !== -1 && entry.intersectionRatio > maxRatio) {
|
||||
maxRatio = entry.intersectionRatio;
|
||||
mostVisibleIndex = itemIndex;
|
||||
}
|
||||
});
|
||||
|
||||
if (mostVisibleIndex !== activeIndex && maxRatio > 0.1) {
|
||||
setActiveIndex(mostVisibleIndex);
|
||||
// Only switch image if it's loaded
|
||||
if (imagesLoaded.has(mostVisibleIndex) || mostVisibleIndex === 0) {
|
||||
setActiveImageIndex(mostVisibleIndex);
|
||||
}
|
||||
}
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all story items
|
||||
itemsRef.current.forEach((item) => {
|
||||
if (item) observer.observe(item);
|
||||
});
|
||||
|
||||
// Update on scroll
|
||||
window.addEventListener('scroll', updateActiveItem, { passive: true });
|
||||
updateActiveItem(); // Initial call
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateActiveItem);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [storyData.length, activeIndex]);
|
||||
|
||||
const handleMouseEnter = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
setActiveImageIndex(index);
|
||||
// Only switch image if it's loaded, otherwise wait a bit
|
||||
if (imagesLoaded.has(index) || index === 0) {
|
||||
setActiveImageIndex(index);
|
||||
} else {
|
||||
// Wait for image to load before switching
|
||||
const checkLoaded = setInterval(() => {
|
||||
if (imagesLoaded.has(index)) {
|
||||
setActiveImageIndex(index);
|
||||
clearInterval(checkLoaded);
|
||||
}
|
||||
}, 50);
|
||||
// Timeout after 1 second to prevent infinite waiting
|
||||
setTimeout(() => {
|
||||
clearInterval(checkLoaded);
|
||||
setActiveImageIndex(index);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
|
||||
<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 case studies...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error or no data state
|
||||
if (error || !storyData || storyData.length === 0) {
|
||||
return (
|
||||
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="text-center py-5">
|
||||
<h2 className="mt-8 title-anim text-white fw-7">
|
||||
Enterprise Case Studies
|
||||
</h2>
|
||||
<p className="text-white mt-3">No data available</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
|
||||
<section ref={sectionRef} 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="row vertical-column-gap-md tp-story-row">
|
||||
<div className="col-12 col-lg-5">
|
||||
<div className="tp-story__content sticky-item">
|
||||
<div className="tp-story__content">
|
||||
<h2 className="mt-8 title-anim text-white fw-7">
|
||||
Enterprise Case Studies
|
||||
</h2>
|
||||
@@ -105,7 +258,10 @@ const Story = () => {
|
||||
{storyData.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
key={item.id || index}
|
||||
ref={(el) => {
|
||||
itemsRef.current[index] = el;
|
||||
}}
|
||||
className={`tp-story__single fade-top ${
|
||||
index === activeIndex ? "active" : ""
|
||||
}`}
|
||||
@@ -113,13 +269,13 @@ const Story = () => {
|
||||
>
|
||||
<p className="fw-6 mt-8">
|
||||
<Link href={`/case-study/${item.slug}`}>
|
||||
{item.category_name || "Case Study"}
|
||||
{item.category_name || 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>
|
||||
<p className="text-xs">{item.excerpt || item.description?.substring(0, 150) + '...'}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -127,43 +283,53 @@ const Story = () => {
|
||||
</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 className="tp-story__thumbs">
|
||||
<div
|
||||
ref={imageContainerRef}
|
||||
className="tp-story-thumb-wrapper"
|
||||
>
|
||||
{storyData.map((item, index) => {
|
||||
// Get the image URL using the utility function
|
||||
const imageUrl = item.thumbnail ? getImageUrl(item.thumbnail) : '/images/case/one.png';
|
||||
const isActive = index === activeImageIndex;
|
||||
const isLoaded = imagesLoaded.has(index);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`image-${item.id || index}`}
|
||||
className={`tp-story-thumb ${isActive ? "thumb-active" : ""}`}
|
||||
data-loaded={isLoaded}
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
width={600}
|
||||
height={300}
|
||||
className="w-100 mh-300"
|
||||
alt={item.title || "Case Study"}
|
||||
priority={index === 0}
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
objectFit: 'cover',
|
||||
opacity: isLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease'
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (!isLoaded) {
|
||||
setImagesLoaded((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(index);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useState, FormEvent, useEffect, useRef } from 'react';
|
||||
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
|
||||
import { useTicketCategories } from '@/lib/hooks/useSupport';
|
||||
|
||||
@@ -26,6 +26,49 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||
const [ticketNumber, setTicketNumber] = useState<string>('');
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Refs for scrolling to messages
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
const successRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
// Scroll to error/success messages when they appear
|
||||
useEffect(() => {
|
||||
if (submitError && errorRef.current) {
|
||||
setTimeout(() => {
|
||||
errorRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [submitError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (submitSuccess && successRef.current) {
|
||||
setTimeout(() => {
|
||||
successRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [submitSuccess]);
|
||||
|
||||
// Scroll to first validation error
|
||||
useEffect(() => {
|
||||
if (Object.keys(fieldErrors).length > 0 && formRef.current) {
|
||||
const firstErrorField = formRef.current.querySelector('.field-error');
|
||||
if (firstErrorField) {
|
||||
setTimeout(() => {
|
||||
firstErrorField.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [fieldErrors]);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
@@ -72,6 +115,16 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
// Scroll to first error after validation
|
||||
setTimeout(() => {
|
||||
const firstErrorField = formRef.current?.querySelector('.field-error');
|
||||
if (firstErrorField) {
|
||||
firstErrorField.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,7 +178,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
|
||||
if (submitSuccess) {
|
||||
return (
|
||||
<div className="ticket-success">
|
||||
<div className="ticket-success" ref={successRef}>
|
||||
<div className="success-icon">
|
||||
<i className="fa-solid fa-circle-check"></i>
|
||||
</div>
|
||||
@@ -204,7 +257,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
</div>
|
||||
|
||||
{submitError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<div className="alert-enterprise alert-error" ref={errorRef}>
|
||||
<i className="fa-solid fa-triangle-exclamation"></i>
|
||||
<div>
|
||||
<strong>Submission Error</strong>
|
||||
@@ -213,7 +266,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="support-form-enterprise">
|
||||
{/* Personal Information */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
|
||||
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
|
||||
|
||||
@@ -9,39 +9,137 @@ const KnowledgeBase = () => {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
|
||||
|
||||
// Refs for scrolling to results
|
||||
const articlesRef = useRef<HTMLDivElement>(null);
|
||||
const emptyStateRef = useRef<HTMLDivElement>(null);
|
||||
const articlesListRef = useRef<HTMLDivElement>(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`;
|
||||
}
|
||||
// Determine which articles to display using useMemo for reactivity
|
||||
const { displayArticles, isLoading, headerText } = useMemo(() => {
|
||||
if (searchTerm) {
|
||||
// If searching, filter all articles by search term
|
||||
const filtered = allArticles.filter(article =>
|
||||
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
return {
|
||||
displayArticles: filtered,
|
||||
isLoading: allArticlesLoading,
|
||||
headerText: 'Search Results'
|
||||
};
|
||||
} else if (selectedCategory) {
|
||||
// If a category is selected, filter articles by that category
|
||||
const filtered = allArticles.filter(article => article.category_slug === selectedCategory);
|
||||
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
|
||||
return {
|
||||
displayArticles: filtered,
|
||||
isLoading: allArticlesLoading,
|
||||
headerText: `${categoryName} Articles`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
displayArticles: featuredArticles,
|
||||
isLoading: featuredLoading,
|
||||
headerText: 'Featured Articles'
|
||||
};
|
||||
}
|
||||
}, [searchTerm, selectedCategory, allArticles, featuredArticles, allArticlesLoading, featuredLoading, categories]);
|
||||
|
||||
// Helper function to find the scrollable parent container
|
||||
const findScrollableParent = (element: HTMLElement | null): HTMLElement | null => {
|
||||
if (!element) return null;
|
||||
|
||||
let parent = element.parentElement;
|
||||
while (parent) {
|
||||
const style = window.getComputedStyle(parent);
|
||||
const overflowY = style.overflowY || style.overflow;
|
||||
const maxHeight = style.maxHeight;
|
||||
|
||||
// Check if this element is scrollable (has overflow and max-height)
|
||||
if ((overflowY === 'auto' || overflowY === 'scroll') && maxHeight && maxHeight !== 'none') {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Scroll to results when category is selected or search changes
|
||||
useEffect(() => {
|
||||
// Only scroll if we have a search term or selected category and articles are loaded
|
||||
if ((searchTerm || selectedCategory) && !isLoading) {
|
||||
// Wait for React to render the articles - use longer delay for category clicks
|
||||
const scrollTimeout = setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (articlesRef.current) {
|
||||
// Try to find the articles list first (most specific), then empty state, then first article item, then section
|
||||
const articlesList = articlesRef.current.querySelector('.articles-list');
|
||||
const firstArticle = articlesRef.current.querySelector('.article-item');
|
||||
const emptyState = articlesRef.current.querySelector('.empty-state');
|
||||
const targetElement = articlesList || firstArticle || emptyState || articlesRef.current;
|
||||
|
||||
if (targetElement) {
|
||||
// Find the scrollable parent container (the modal)
|
||||
const scrollableContainer = findScrollableParent(targetElement as HTMLElement);
|
||||
|
||||
if (scrollableContainer) {
|
||||
// Calculate position relative to the scrollable container
|
||||
const containerRect = scrollableContainer.getBoundingClientRect();
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
|
||||
// Calculate the scroll position within the container
|
||||
const scrollTop = scrollableContainer.scrollTop;
|
||||
const relativeTop = targetRect.top - containerRect.top;
|
||||
const offset = 20; // Small offset from top of container
|
||||
|
||||
scrollableContainer.scrollTo({
|
||||
top: scrollTop + relativeTop - offset,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else {
|
||||
// Fallback to window scroll if no scrollable container found
|
||||
const elementTop = targetElement.getBoundingClientRect().top + window.pageYOffset;
|
||||
const offsetPosition = elementTop - 100;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 600); // Longer delay to ensure React has fully rendered the articles
|
||||
|
||||
return () => clearTimeout(scrollTimeout);
|
||||
}
|
||||
}, [searchTerm, selectedCategory, isLoading, displayArticles.length]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// The search is already being performed by the hook
|
||||
};
|
||||
|
||||
const handleCategoryClick = (categorySlug: string) => {
|
||||
setSelectedCategory(categorySlug);
|
||||
// Clear search when category is selected
|
||||
if (searchTerm) {
|
||||
setSearchTerm('');
|
||||
}
|
||||
// Scroll will be handled by useEffect after articles render
|
||||
};
|
||||
|
||||
const filteredCategories = selectedCategory
|
||||
? categories.filter(cat => cat.slug === selectedCategory)
|
||||
: categories;
|
||||
@@ -95,7 +193,7 @@ const KnowledgeBase = () => {
|
||||
<div key={category.id} className="col-md-6 col-lg-4">
|
||||
<div
|
||||
className="category-card"
|
||||
onClick={() => setSelectedCategory(category.slug)}
|
||||
onClick={() => handleCategoryClick(category.slug)}
|
||||
style={{ borderLeftColor: category.color }}
|
||||
>
|
||||
<div
|
||||
@@ -122,7 +220,7 @@ const KnowledgeBase = () => {
|
||||
)}
|
||||
|
||||
{/* Featured/Search Results Articles */}
|
||||
<div className="kb-articles">
|
||||
<div ref={articlesRef} className="kb-articles">
|
||||
<div className="articles-header">
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3>{headerText}</h3>
|
||||
@@ -150,7 +248,7 @@ const KnowledgeBase = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : displayArticles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div ref={emptyStateRef} className="empty-state">
|
||||
<i className="fa-solid fa-search empty-icon"></i>
|
||||
<h4>No articles found</h4>
|
||||
<p>
|
||||
@@ -160,7 +258,7 @@ const KnowledgeBase = () => {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="articles-list">
|
||||
<div ref={articlesListRef} className="articles-list">
|
||||
{Array.isArray(displayArticles) && displayArticles.map(article => (
|
||||
<div
|
||||
key={article.id}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useState, FormEvent, useEffect, useRef } from 'react';
|
||||
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
|
||||
|
||||
const TicketStatusCheck = () => {
|
||||
@@ -7,6 +7,34 @@ const TicketStatusCheck = () => {
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<SupportTicket | null>(null);
|
||||
|
||||
// Refs for scrolling to results
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
const ticketRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to error when it appears
|
||||
useEffect(() => {
|
||||
if (searchError && errorRef.current) {
|
||||
setTimeout(() => {
|
||||
errorRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [searchError]);
|
||||
|
||||
// Scroll to ticket results when they appear
|
||||
useEffect(() => {
|
||||
if (ticket && ticketRef.current) {
|
||||
setTimeout(() => {
|
||||
ticketRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [ticket]);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -136,7 +164,7 @@ const TicketStatusCheck = () => {
|
||||
</form>
|
||||
|
||||
{searchError && (
|
||||
<div className="alert-enterprise alert-error">
|
||||
<div ref={errorRef} className="alert-enterprise alert-error">
|
||||
<i className="fa-solid fa-exclamation-circle"></i>
|
||||
<div>
|
||||
<strong>Ticket Not Found</strong>
|
||||
@@ -146,7 +174,7 @@ const TicketStatusCheck = () => {
|
||||
)}
|
||||
|
||||
{ticket && (
|
||||
<div className="ticket-details-enterprise">
|
||||
<div ref={ticketRef} className="ticket-details-enterprise">
|
||||
{/* Header Section */}
|
||||
<div className="ticket-header-enterprise">
|
||||
<div className="ticket-number-section">
|
||||
|
||||
@@ -339,28 +339,7 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
</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>
|
||||
|
||||
@@ -36,16 +36,17 @@ export const CookieConsentBanner: React.FC = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.showBanner) {
|
||||
if (state.showBanner && !state.showSettings) {
|
||||
// Small delay to ensure smooth animation
|
||||
const timer = setTimeout(() => setIsVisible(true), 100);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, [state.showBanner]);
|
||||
}, [state.showBanner, state.showSettings]);
|
||||
|
||||
if (!state.showBanner || !isVisible) return null;
|
||||
// Hide banner when settings modal is open
|
||||
if (!state.showBanner || !isVisible || state.showSettings) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -205,6 +206,9 @@ export const CookieSettingsModal: React.FC = () => {
|
||||
transition={{ duration: 0.2 }}
|
||||
className="cookie-settings-overlay"
|
||||
onClick={hideSettings}
|
||||
style={{
|
||||
zIndex: 10001, // Higher than banner overlay (10000)
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"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";
|
||||
|
||||
@@ -39,11 +36,14 @@ const Footer = () => {
|
||||
|
||||
return (
|
||||
<footer className="footer position-relative overflow-x-clip">
|
||||
{/* Decorative background elements */}
|
||||
<div className="footer-bg-decoration"></div>
|
||||
|
||||
<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="footer-logo-section text-center pt-60 pb-50">
|
||||
<div className="enterprise-logo-container">
|
||||
<div className="enterprise-security-badges">
|
||||
{/* Left Badge */}
|
||||
@@ -60,18 +60,19 @@ const Footer = () => {
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt="Logo"
|
||||
width={120}
|
||||
height={90}
|
||||
width={140}
|
||||
height={100}
|
||||
className="footer-logo-image"
|
||||
/>
|
||||
</Link>
|
||||
<p className="footer-tagline">Transforming Ideas Into Digital Excellence</p>
|
||||
</div>
|
||||
|
||||
{/* Right Badge */}
|
||||
<div className="security-badges-right">
|
||||
<div className="security-badge">
|
||||
<i className="fa-solid fa-shield-halved"></i>
|
||||
<span>Incident Management</span>
|
||||
<i className="fa-solid fa-brain"></i>
|
||||
<span>AI & Innovation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,27 +80,22 @@ const Footer = () => {
|
||||
</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="row vertical-column-gap-lg pt-60 pb-50">
|
||||
<div className="col-12 col-lg-2 col-md-6 col-sm-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Company</h6>
|
||||
<h6 className="footer-section-title">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>
|
||||
<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="/insights">Insights & Blog</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="col-12 col-lg-2 col-md-6 col-sm-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Services</h6>
|
||||
<h6 className="footer-section-title">Services</h6>
|
||||
<ul className="footer-links">
|
||||
{servicesLoading ? (
|
||||
<>
|
||||
@@ -117,9 +113,9 @@ const Footer = () => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="col-12 col-lg-2 col-md-6 col-sm-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Latest Jobs</h6>
|
||||
<h6 className="footer-section-title">Latest Jobs</h6>
|
||||
<ul className="footer-links">
|
||||
{jobsLoading ? (
|
||||
<>
|
||||
@@ -137,9 +133,9 @@ const Footer = () => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-2 col-md-6">
|
||||
<div className="col-12 col-lg-2 col-md-6 col-sm-6">
|
||||
<div className="footer-section">
|
||||
<h6 className="text-white fm fw-6 mb-24">Support</h6>
|
||||
<h6 className="footer-section-title">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>
|
||||
@@ -151,12 +147,16 @@ const Footer = () => {
|
||||
<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>
|
||||
<div className="cta-badge">
|
||||
<i className="fa-solid fa-sparkles"></i>
|
||||
<span>Get Started Today</span>
|
||||
</div>
|
||||
<h6 className="cta-title">Ready to Transform Your Business?</h6>
|
||||
<p className="cta-description">Start your software journey with our enterprise solutions, incident management, and custom development services.</p>
|
||||
<Link href="/contact-us" className="btn-luxury-cta">
|
||||
<span>Start Your Journey</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
<div className="btn-shine"></div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,21 +164,22 @@ const Footer = () => {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="footer__inner pt-60">
|
||||
<div className="row vertical-column-gap-lg">
|
||||
<div className="footer__inner pt-60 pb-50">
|
||||
<div className="row vertical-column-gap-lg g-4">
|
||||
<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 className="contact-icon-wrapper">
|
||||
<div className="contact-icon">
|
||||
<i className="fa-solid fa-location-dot"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content">
|
||||
<h5 className="mt-8 fm fw-6 text-white mb-24">
|
||||
Location
|
||||
</h5>
|
||||
<p className="text-quinary">
|
||||
<h5 className="contact-title">Location</h5>
|
||||
<p className="contact-text">
|
||||
<Link
|
||||
href="https://maps.google.com/?q=42.496781103070504,27.4758968970689"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GNX Soft Ltd.<br />
|
||||
Tsar Simeon I, 56<br />
|
||||
@@ -191,30 +192,33 @@ const Footer = () => {
|
||||
</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 className="contact-icon-wrapper">
|
||||
<div className="contact-icon">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
</div>
|
||||
</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>
|
||||
<h5 className="contact-title">Phone</h5>
|
||||
<p className="contact-text">
|
||||
<Link href="tel:+359896138030">+359 896 13 80 30</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 className="contact-icon-wrapper">
|
||||
<div className="contact-icon">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
</div>
|
||||
</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">
|
||||
<h5 className="contact-title">Email</h5>
|
||||
<p className="contact-text">
|
||||
<Link href="mailto:info@gnxsoft.com">
|
||||
info@gnxsoft.com
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,32 +229,36 @@ const Footer = () => {
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="footer-copyright">
|
||||
<div className="row align-items-center vertical-column-gap">
|
||||
<div className="row align-items-center vertical-column-gap g-3">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="footer__copyright-text text-center text-lg-start">
|
||||
<p className="text-quinary mt-8">
|
||||
<p className="copyright-text">
|
||||
© <span id="copyrightYear">{currentYear}</span>{" "}
|
||||
<Link href="/" className="fw-6">
|
||||
GNX
|
||||
<Link href="/" className="copyright-link">
|
||||
GNX Soft Ltd.
|
||||
</Link>
|
||||
. All rights reserved. GNX Software Solutions.
|
||||
{" "}All rights reserved. Built with excellence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="social justify-content-center justify-content-lg-end">
|
||||
<div className="social-links justify-content-center justify-content-lg-end">
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin"></i>
|
||||
className="social-link"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="GitHub"
|
||||
>
|
||||
className="social-link"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 = () => {
|
||||
@@ -17,26 +16,65 @@ const Header = () => {
|
||||
// Fetch services from API
|
||||
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
|
||||
|
||||
// Create dynamic navigation data with services from API
|
||||
// Create dynamic navigation data - only use API data, no hardcoded fallback
|
||||
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;
|
||||
}
|
||||
const baseNavigation = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Home",
|
||||
path: "/",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "About Us",
|
||||
path: "/about-us",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Services",
|
||||
path: "/services",
|
||||
submenu: apiServices.length > 0
|
||||
? apiServices.map(service => ({
|
||||
id: service.id + 1000,
|
||||
title: service.title,
|
||||
path: `/services/${service.slug}`,
|
||||
display_order: service.display_order,
|
||||
}))
|
||||
: null,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Case Studies",
|
||||
path: "/case-study",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Insights",
|
||||
path: "/insights",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Career",
|
||||
path: "/career",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Support Center",
|
||||
path: "/support-center",
|
||||
submenu: null,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Contact Us",
|
||||
path: "/contact-us",
|
||||
submenu: null,
|
||||
},
|
||||
];
|
||||
|
||||
return baseNavigation;
|
||||
}, [apiServices]);
|
||||
@@ -189,11 +227,11 @@ const Header = () => {
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
</li>
|
||||
) : item.title === "Services" && servicesError ? (
|
||||
) : item.title === "Services" && (servicesError || !item.submenu || item.submenu.length === 0) ? (
|
||||
<li>
|
||||
<span className="text-danger">Failed to load services</span>
|
||||
<span className="text-muted">No data available</span>
|
||||
</li>
|
||||
) : (
|
||||
) : item.submenu ? (
|
||||
item.submenu.map((subItem, subIndex) => (
|
||||
<li key={subIndex}>
|
||||
<Link
|
||||
@@ -208,7 +246,7 @@ const Header = () => {
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
) : null}
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 {
|
||||
@@ -20,7 +19,7 @@ const OffcanvasMenu = ({
|
||||
isOffcanvasOpen,
|
||||
isActive,
|
||||
handleClick,
|
||||
navigationData = OffcanvasData,
|
||||
navigationData = [],
|
||||
servicesLoading = false,
|
||||
servicesError = null
|
||||
}: OffcanvasMenuProps) => {
|
||||
@@ -81,74 +80,80 @@ const OffcanvasMenu = ({
|
||||
</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)}
|
||||
{navigationData.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-muted">No data available</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul>
|
||||
{navigationData.map((item, index) =>
|
||||
item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children nav-fade"
|
||||
key={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>
|
||||
))
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
</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 || !item.submenu || item.submenu.length === 0) ? (
|
||||
<li>
|
||||
<span className="text-muted">No data available</span>
|
||||
</li>
|
||||
) : item.submenu ? (
|
||||
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>
|
||||
))
|
||||
) : null}
|
||||
</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>
|
||||
@@ -159,7 +164,7 @@ const OffcanvasMenu = ({
|
||||
<div className="contact-methods">
|
||||
<a href="tel:+359896138030" className="contact-item">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>+359896138030</span>
|
||||
<span>+359 896 13 80 30</span>
|
||||
</a>
|
||||
<a href="mailto:info@gnxsoft.com" className="contact-item">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
|
||||
@@ -98,8 +98,8 @@ export const caseStudyService = {
|
||||
try {
|
||||
const queryString = params ? buildQueryString(params) : '';
|
||||
const url = queryString
|
||||
? `${API_CONFIG.BASE_URL}/api/case-studies/case-studies/?${queryString}`
|
||||
: `${API_CONFIG.BASE_URL}/api/case-studies/case-studies/`;
|
||||
? `${API_CONFIG.BASE_URL}/api/case-studies/?${queryString}`
|
||||
: `${API_CONFIG.BASE_URL}/api/case-studies/`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -123,7 +123,7 @@ export const caseStudyService = {
|
||||
getCaseStudyBySlug: async (slug: string): Promise<CaseStudy> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/`,
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -147,7 +147,7 @@ export const caseStudyService = {
|
||||
getFeaturedCaseStudies: async (): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/featured/`,
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/featured/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -171,7 +171,7 @@ export const caseStudyService = {
|
||||
getLatestCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/latest/?limit=${limit}`,
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/latest/?limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -195,7 +195,7 @@ export const caseStudyService = {
|
||||
getPopularCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/popular/?limit=${limit}`,
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/popular/?limit=${limit}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -219,7 +219,7 @@ export const caseStudyService = {
|
||||
getRelatedCaseStudies: async (slug: string): Promise<CaseStudy[]> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/related/`,
|
||||
`${API_CONFIG.BASE_URL}/api/case-studies/${slug}/related/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
71
frontEnd/lib/api/homeService.ts
Normal file
71
frontEnd/lib/api/homeService.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { API_BASE_URL } from '../config/api';
|
||||
|
||||
// Types for Home Banner data
|
||||
export interface HomeBanner {
|
||||
id: number;
|
||||
icon: string;
|
||||
badge: string;
|
||||
heading: string;
|
||||
highlight: string;
|
||||
subheading: string;
|
||||
description: string;
|
||||
button_text: string;
|
||||
button_url: string;
|
||||
display_order: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
class HomeServiceAPI {
|
||||
private baseUrl = `${API_BASE_URL}/api/home`;
|
||||
|
||||
/**
|
||||
* Get all home banners
|
||||
*/
|
||||
async getBanners(): Promise<HomeBanner[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/banner/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.results || data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific home banner by ID
|
||||
*/
|
||||
async getBanner(id: number): Promise<HomeBanner> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/banner/${id}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const homeService = new HomeServiceAPI();
|
||||
|
||||
43
frontEnd/lib/hooks/useHome.ts
Normal file
43
frontEnd/lib/hooks/useHome.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { homeService, HomeBanner } from '../api/homeService';
|
||||
|
||||
export interface UseHomeBannersReturn {
|
||||
data: HomeBanner[] | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch home banners
|
||||
*/
|
||||
export const useHomeBanners = (): UseHomeBannersReturn => {
|
||||
const [data, setData] = useState<HomeBanner[] | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const result = await homeService.getBanners();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +26,11 @@ const nextConfig = {
|
||||
port: '8080',
|
||||
pathname: '/images/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
pathname: '/**',
|
||||
},
|
||||
// Add your production domain when ready
|
||||
// {
|
||||
// protocol: 'https',
|
||||
@@ -33,6 +38,8 @@ const nextConfig = {
|
||||
// pathname: '/media/**',
|
||||
// },
|
||||
],
|
||||
// Legacy domains format for additional compatibility
|
||||
domains: ['images.unsplash.com'],
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
// Navigation data - Replace with API call to navigationAPI.getAll()
|
||||
export const OffcanvasData = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Home",
|
||||
path: "/",
|
||||
parent_id: null,
|
||||
display_order: 1,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "About Us",
|
||||
path: "/about-us",
|
||||
parent_id: null,
|
||||
display_order: 2,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Services",
|
||||
path: "/services",
|
||||
parent_id: null,
|
||||
display_order: 3,
|
||||
submenu: [
|
||||
{
|
||||
id: 4,
|
||||
title: "Enterprise Software Development",
|
||||
path: "/services/enterprise-software",
|
||||
parent_id: 3,
|
||||
display_order: 1,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "API Development & Integration",
|
||||
path: "/services/api-development",
|
||||
parent_id: 3,
|
||||
display_order: 2,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Cloud Solutions",
|
||||
path: "/services/cloud-solutions",
|
||||
parent_id: 3,
|
||||
display_order: 3,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "Digital Transformation",
|
||||
path: "/services/digital-transformation",
|
||||
parent_id: 3,
|
||||
display_order: 4,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Case Studies",
|
||||
path: "/case-study",
|
||||
parent_id: null,
|
||||
display_order: 4,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Insights",
|
||||
path: "/insights",
|
||||
parent_id: null,
|
||||
display_order: 5,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "Career",
|
||||
path: "/career",
|
||||
parent_id: null,
|
||||
display_order: 6,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: "Support Center",
|
||||
path: "/support-center",
|
||||
parent_id: null,
|
||||
display_order: 7,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
title: "Contact Us",
|
||||
path: "/contact-us",
|
||||
parent_id: null,
|
||||
display_order: 8,
|
||||
submenu: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z"
|
||||
}
|
||||
];
|
||||
@@ -93,19 +93,24 @@
|
||||
}
|
||||
|
||||
.modern-banner {
|
||||
padding-top: 80px; // Account for fixed navbar
|
||||
|
||||
.container {
|
||||
padding: 60px 0;
|
||||
padding-top: 30px; // Start content below navbar
|
||||
justify-content: flex-start; // Align to top on tablet
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-center {
|
||||
max-width: 550px;
|
||||
|
||||
.main-heading {
|
||||
font-size: clamp(1.5rem, 3vw, 2.5rem);
|
||||
font-size: clamp(1.3rem, 3vw, 2rem); // Reduced from 1.5rem-2.5rem to 1.3rem-2rem
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -386,13 +391,19 @@
|
||||
|
||||
.modern-banner {
|
||||
min-height: 100vh;
|
||||
padding-top: 100px; // Account for fixed navbar (increased)
|
||||
padding-bottom: 30px; // Add bottom padding
|
||||
|
||||
.container {
|
||||
padding: 40px 0;
|
||||
padding: 30px 0;
|
||||
padding-top: 20px; // Start content below navbar
|
||||
justify-content: flex-start; // Align to top on mobile
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
margin-bottom: 25px;
|
||||
margin-top: 0; // Remove any top margin
|
||||
align-items: flex-start; // Align to top
|
||||
}
|
||||
|
||||
.content-center {
|
||||
@@ -428,8 +439,10 @@
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 100%;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6; // Increased for better readability
|
||||
padding: 0 10px;
|
||||
word-wrap: break-word; // Ensure long words wrap
|
||||
overflow-wrap: break-word; // Additional word wrapping support
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
@@ -970,8 +983,16 @@
|
||||
|
||||
// Extra small mobile styles for modern banner
|
||||
.modern-banner {
|
||||
padding-top: 70px; // Account for fixed navbar (smaller on mobile)
|
||||
|
||||
.container {
|
||||
padding: 30px 0;
|
||||
padding-top: 15px; // Start content below navbar
|
||||
justify-content: flex-start; // Align to top
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-center {
|
||||
@@ -1089,8 +1110,18 @@
|
||||
|
||||
// Ultra small mobile styles for modern banner
|
||||
.modern-banner {
|
||||
padding-top: 90px; // Account for fixed navbar (increased)
|
||||
padding-bottom: 25px; // Add bottom padding
|
||||
|
||||
.container {
|
||||
padding: 25px 0;
|
||||
padding-top: 15px; // Start content below navbar
|
||||
justify-content: flex-start; // Align to top
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
margin-top: 0;
|
||||
align-items: flex-start; // Align to top
|
||||
}
|
||||
|
||||
.content-center {
|
||||
@@ -1124,8 +1155,10 @@
|
||||
.description {
|
||||
font-size: 12px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.3;
|
||||
line-height: 1.5; // Increased for better readability
|
||||
padding: 0;
|
||||
word-wrap: break-word; // Ensure long words wrap
|
||||
overflow-wrap: break-word; // Additional word wrapping support
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
@@ -1196,9 +1229,16 @@
|
||||
@media only screen and (max-width: 374.98px) {
|
||||
.modern-banner {
|
||||
min-height: 100vh;
|
||||
padding-top: 68px; // Account for fixed navbar (smaller on very small screens)
|
||||
|
||||
.container {
|
||||
padding: 20px 0;
|
||||
padding-top: 12px; // Start content below navbar
|
||||
justify-content: flex-start; // Align to top
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-center {
|
||||
@@ -1308,6 +1348,7 @@
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
padding: 20px 0;
|
||||
padding-top: 70px; // Account for fixed navbar
|
||||
|
||||
.container {
|
||||
padding: 20px 0;
|
||||
@@ -1425,6 +1466,7 @@
|
||||
@media only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) {
|
||||
.modern-banner {
|
||||
min-height: 90vh;
|
||||
padding-top: 80px; // Account for fixed navbar
|
||||
|
||||
.container {
|
||||
padding: 40px 0;
|
||||
@@ -1472,13 +1514,14 @@
|
||||
@media only screen and (min-width: 768px) and (max-width: 991.98px) and (orientation: portrait) {
|
||||
.modern-banner {
|
||||
min-height: 100vh;
|
||||
padding-top: 80px; // Account for fixed navbar
|
||||
|
||||
.content-center {
|
||||
max-width: 600px;
|
||||
padding: 0 30px;
|
||||
|
||||
.main-heading {
|
||||
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
||||
font-size: clamp(1.5rem, 4vw, 2rem); // Reduced from 1.8rem-2.5rem to 1.5rem-2rem
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -276,11 +276,18 @@ a {
|
||||
|
||||
.sticky-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sticky-item {
|
||||
position: sticky;
|
||||
top: 220px;
|
||||
z-index: 1;
|
||||
align-self: flex-start;
|
||||
|
||||
// Ensure sticky stops at bottom of container
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: var(--z-50);
|
||||
z-index: 10001 !important; // Higher than banner overlay (10000)
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,17 +10,18 @@
|
||||
// 4.02.01 modern banner styles start
|
||||
.modern-banner {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
||||
min-height: 100vh;
|
||||
min-height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
||||
min-height: -webkit-fill-available; // iOS viewport fix
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden; // Prevent horizontal scroll
|
||||
overflow-y: auto; // Allow vertical scroll if content is too long
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
justify-content: flex-start; // Align content to top
|
||||
padding: 0;
|
||||
padding-top: 100px; // Account for fixed navbar (increased from 80px)
|
||||
padding-bottom: 40px; // Add bottom padding for scroll indicator
|
||||
|
||||
.banner-background {
|
||||
position: absolute;
|
||||
@@ -614,11 +615,12 @@
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
min-height: auto; // Allow content to determine height
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 80px 0;
|
||||
justify-content: flex-start; // Align to top instead of center
|
||||
padding: 40px 0; // Reduced padding
|
||||
padding-top: 20px; // Start content below navbar
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -635,15 +637,19 @@
|
||||
max-width: 100%;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
padding-top: 20px; // Start content below navbar on mobile
|
||||
justify-content: flex-start; // Align content to top on mobile
|
||||
}
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-items: flex-start; // Align to top instead of center
|
||||
flex: 1;
|
||||
margin-bottom: 40px;
|
||||
padding-top: 20px; // Add top padding for spacing
|
||||
min-height: auto; // Allow content to determine height
|
||||
|
||||
.content-center {
|
||||
text-align: center;
|
||||
@@ -700,11 +706,13 @@
|
||||
}
|
||||
|
||||
.main-heading {
|
||||
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||
font-size: clamp(2rem, 4vw, 3.5rem); // Reduced from 2.5rem-4.5rem to 2rem-3.5rem
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
line-height: 1.2; // Slightly increased for better spacing
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word; // Ensure long words wrap
|
||||
overflow-wrap: break-word; // Additional word wrapping support
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #0ea5e9, #6366f1);
|
||||
@@ -728,12 +736,15 @@
|
||||
|
||||
.description {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.7; // Increased line height for better readability
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 32px;
|
||||
max-width: 650px;
|
||||
max-width: 700px; // Slightly increased max-width
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
word-wrap: break-word; // Ensure long words wrap
|
||||
overflow-wrap: break-word; // Additional word wrapping support
|
||||
hyphens: auto; // Enable automatic hyphenation
|
||||
|
||||
&.carousel-text {
|
||||
transition: opacity 1.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,131 @@
|
||||
// Add any specific styles for the about banner here
|
||||
}
|
||||
|
||||
// About Service Section - Transparent Background Style - Seamless Continuous Flow
|
||||
.about-service-section {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
padding: 120px 0 60px 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0;
|
||||
color: #000000 !important;
|
||||
|
||||
* {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
padding: 60px 0 120px 0;
|
||||
overflow: hidden;
|
||||
margin-top: -1px;
|
||||
padding-top: 61px;
|
||||
color: #000000 !important;
|
||||
|
||||
* {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-journey-section {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
padding: 120px 0;
|
||||
overflow: hidden;
|
||||
color: #000000 !important;
|
||||
|
||||
* {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hero-banner {
|
||||
background: transparent;
|
||||
min-height: auto;
|
||||
|
||||
.hero-background {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
// Journey Timeline Container - Special styling
|
||||
.journey-timeline-container {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.journey-milestone-item {
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
|
||||
.milestone-year-icon {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: var(--white) !important;
|
||||
|
||||
span {
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process Steps Container - Special styling
|
||||
.process-steps-container {
|
||||
.metric-item {
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
|
||||
.step-number-icon {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--white) !important;
|
||||
|
||||
span {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enterprise styling for about page
|
||||
.enterprise-badge {
|
||||
.enterprise-badge-content {
|
||||
@@ -160,7 +285,118 @@
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 991px) {
|
||||
.about-service-section {
|
||||
padding: 80px 0 40px 0;
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
padding: 40px 0 80px 0;
|
||||
padding-top: 41px;
|
||||
}
|
||||
|
||||
.about-journey-section {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.hero-metrics {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.journey-timeline-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.about-service-section {
|
||||
padding: 60px 0 30px 0;
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
padding: 30px 0 60px 0;
|
||||
padding-top: 31px;
|
||||
}
|
||||
|
||||
.about-journey-section {
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
margin: 2rem 0;
|
||||
|
||||
.metric-item {
|
||||
padding: 1.25rem;
|
||||
gap: 0.875rem;
|
||||
|
||||
.metric-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.journey-timeline-container {
|
||||
margin: 2rem 0;
|
||||
|
||||
.journey-milestone-item {
|
||||
padding: 1.25rem;
|
||||
gap: 0.875rem;
|
||||
|
||||
.metric-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
span {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.enterprise-stats {
|
||||
.stat-item {
|
||||
margin-bottom: 20px;
|
||||
@@ -185,3 +421,51 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.about-service-section {
|
||||
padding: 50px 0 25px 0;
|
||||
}
|
||||
|
||||
.about-process-section {
|
||||
padding: 25px 0 50px 0;
|
||||
padding-top: 26px;
|
||||
}
|
||||
|
||||
.about-journey-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
padding: 1rem;
|
||||
|
||||
.metric-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.journey-timeline-container {
|
||||
.journey-milestone-item {
|
||||
padding: 1rem;
|
||||
|
||||
.metric-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
span {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user