This commit is contained in:
Iliyan Angelov
2025-11-24 08:18:18 +02:00
parent 366f28677a
commit 136f75a859
133 changed files with 14977 additions and 3350 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
</>
);
};

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
</>
);
};

View File

@@ -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>

View File

@@ -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>
))}

View File

@@ -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&apos;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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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">
&copy; <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>

View File

@@ -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>
) : (

View File

@@ -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>

View File

@@ -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: {

View 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();

View 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,
};
};

View File

@@ -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],

View File

@@ -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"
}
];

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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