This commit is contained in:
Iliyan Angelov
2025-12-10 01:36:00 +02:00
parent 2f6dca736a
commit 6a9e823402
84 changed files with 5293 additions and 1836 deletions

View File

@@ -227,10 +227,10 @@ const AboutBanner = () => {
{/* Social Links */}
<div className="social-links">
<Link href="https://www.linkedin.com/company/gnxtech" target="_blank" className="social-link">
<Link href="https://www.linkedin.com" target="_blank" className="social-link">
<i className="fa-brands fa-linkedin-in"></i>
</Link>
<Link href="https://github.com/gnxtech" target="_blank" className="social-link">
<Link href="https://github.com" target="_blank" className="social-link">
<i className="fa-brands fa-github"></i>
</Link>
</div>

View File

@@ -4,6 +4,7 @@ import Image from "next/legacy/image";
import Link from "next/link";
import { useBlogPost } from "@/lib/hooks/useBlog";
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
import { sanitizeHTML } from "@/lib/security/sanitize";
const BlogSingle = () => {
const params = useParams();
@@ -184,7 +185,7 @@ const BlogSingle = () => {
{post.content && (
<div
className="article-content enterprise-content"
dangerouslySetInnerHTML={{ __html: post.content }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(post.content) }}
/>
)}
</div>
@@ -199,7 +200,7 @@ const BlogSingle = () => {
</h6>
<div className="social-share">
<Link
href={`https://www.linkedin.com/shareArticle?mini=true&url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&title=${encodeURIComponent(post.title)}`}
href="https://linkedin.com"
target="_blank"
rel="noopener noreferrer"
className="share-btn share-linkedin"

View File

@@ -7,7 +7,10 @@ import Link from "next/link";
const CareerBanner = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
if (document.querySelector(".career-banner")) {
const careerBanner = document.querySelector(".career-banner");
const cpBannerThumb = document.querySelector(".cp-banner-thumb");
if (careerBanner && cpBannerThumb) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".career-banner",
@@ -114,7 +117,7 @@ const CareerBanner = () => {
<ul className="social">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
href="https://www.linkedin.com"
target="_blank"
aria-label="connect with us on linkedin"
>
@@ -123,7 +126,7 @@ const CareerBanner = () => {
</li>
<li>
<Link
href="https://github.com/gnxtech"
href="https://github.com"
target="_blank"
aria-label="view our code on github"
>

View File

@@ -1,8 +1,4 @@
import Image from "next/legacy/image";
import time from "@/public/images/time.png";
import trans from "@/public/images/trans.png";
import support from "@/public/images/support.png";
import skill from "@/public/images/skill.png";
const Thrive = () => {
return (
@@ -20,7 +16,7 @@ const Thrive = () => {
<div className="row vertical-column-gap-lg mt-60">
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={time} alt="Image" width={80} height={80} />
<Image src="/images/time.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
@@ -35,7 +31,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={trans} alt="Image" width={80} height={80} />
<Image src="/images/trans.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
@@ -50,7 +46,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={support} alt="Image" width={80} height={80} />
<Image src="/images/support.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
@@ -63,7 +59,7 @@ const Thrive = () => {
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={skill} alt="Image" width={80} height={80} />
<Image src="/images/skill.png" alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">

View File

@@ -3,7 +3,6 @@ import Image from "next/legacy/image";
import Link from "next/link";
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
const CaseItems = () => {
const { caseStudies, loading: casesLoading } = useCaseStudies();
@@ -56,7 +55,7 @@ const CaseItems = () => {
<div className="thumb mb-24">
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
<Image
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : "/images/case/one.png"}
className="w-100 mh-300"
alt={caseStudy.title}
width={600}

View File

@@ -5,8 +5,7 @@ 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 { sanitizeHTML } from "@/lib/security/sanitize";
interface CaseSingleProps {
slug: string;
@@ -204,12 +203,12 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
{caseStudy.project_overview ? (
<div
className="content-html"
dangerouslySetInnerHTML={{ __html: caseStudy.project_overview }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(caseStudy.project_overview) }}
/>
) : (
<div
className="content-html"
dangerouslySetInnerHTML={{ __html: caseStudy.description || '' }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(caseStudy.description || '') }}
/>
)}
@@ -217,7 +216,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
{caseStudy.description && (
<div
className="content-html full-description mt-40"
dangerouslySetInnerHTML={{ __html: caseStudy.description }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(caseStudy.description) }}
/>
)}
</div>
@@ -345,7 +344,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
<h2 className="section-title">Site Map & Process</h2>
<div
className="content-html"
dangerouslySetInnerHTML={{ __html: caseStudy.site_map_content }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(caseStudy.site_map_content) }}
/>
</div>
</div>

View File

@@ -12,6 +12,8 @@ const Process = ({ slug }: ProcessProps) => {
return null;
}
const processSteps = caseStudy.process_steps;
return (
<section className="case-study-process luxury-process pt-120 pb-120">
<div className="container">
@@ -28,7 +30,7 @@ const Process = ({ slug }: ProcessProps) => {
</div>
<div className="col-12 col-lg-7">
<div className="process-steps-list">
{caseStudy.process_steps.map((step, index) => (
{processSteps.map((step, index) => (
<div key={step.id} className="process-step-item">
<div className="step-number">
{String(step.step_number).padStart(2, '0')}
@@ -37,7 +39,7 @@ const Process = ({ slug }: ProcessProps) => {
<h4 className="step-title">{step.title}</h4>
<p className="step-description">{step.description}</p>
</div>
{index < caseStudy.process_steps.length - 1 && (
{index < processSteps.length - 1 && (
<div className="step-connector"></div>
)}
</div>

View File

@@ -3,7 +3,6 @@ import Image from "next/image";
import Link from "next/link";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
interface RelatedCaseProps {
slug: string;
@@ -34,7 +33,7 @@ const RelatedCase = ({ slug }: RelatedCaseProps) => {
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
<div className="case-image-wrapper">
<Image
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : "/images/case/one.png"}
className="case-image"
alt={relatedCase.title}
width={400}

View File

@@ -2,7 +2,6 @@
import { usePathname } from "next/navigation";
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";
const ContactSection = () => {
@@ -32,10 +31,39 @@ const ContactSection = () => {
message: string;
}>({ type: null, message: '' });
// Math Captcha state
const [captcha, setCaptcha] = useState({ num1: 0, num2: 0, operator: '+', answer: 0 });
const [captchaAnswer, setCaptchaAnswer] = useState('');
const [captchaError, setCaptchaError] = useState('');
// Refs for scrolling to status messages
const statusRef = useRef<HTMLDivElement>(null);
const formRef = useRef<HTMLFormElement>(null);
// Generate math captcha
const generateCaptcha = () => {
const operators = ['+', '-'];
const operator = operators[Math.floor(Math.random() * operators.length)];
let num1 = Math.floor(Math.random() * 10) + 1; // 1-10
let num2 = Math.floor(Math.random() * 10) + 1; // 1-10
// Ensure subtraction doesn't result in negative numbers
if (operator === '-' && num1 < num2) {
[num1, num2] = [num2, num1];
}
const answer = operator === '+' ? num1 + num2 : num1 - num2;
setCaptcha({ num1, num2, operator, answer });
setCaptchaAnswer('');
setCaptchaError('');
};
// Generate captcha on component mount
useEffect(() => {
generateCaptcha();
}, []);
// Scroll to status message when it appears
useEffect(() => {
if (submitStatus.type && statusRef.current) {
@@ -62,6 +90,16 @@ const ContactSection = () => {
e.preventDefault();
setIsSubmitting(true);
setSubmitStatus({ type: null, message: '' });
setCaptchaError('');
// Validate captcha
const userAnswer = parseInt(captchaAnswer.trim());
if (isNaN(userAnswer) || userAnswer !== captcha.answer) {
setCaptchaError('Incorrect answer. Please try again.');
setIsSubmitting(false);
generateCaptcha(); // Generate new captcha on error
return;
}
try {
// Transform form data to match API requirements
@@ -108,6 +146,10 @@ const ContactSection = () => {
privacy: false
});
// Reset captcha
setCaptchaAnswer('');
generateCaptcha();
} catch (error) {
setSubmitStatus({
type: 'error',
@@ -408,6 +450,49 @@ const ContactSection = () => {
</label>
</div>
</div>
{/* Math Captcha */}
<div className="input-single compact-input captcha-container">
<label htmlFor="captcha">
Security Verification *
<span className="captcha-hint">(Please solve the math problem)</span>
</label>
<div className="captcha-wrapper">
<div className="captcha-question">
<span className="captcha-numbers">
{captcha.num1} {captcha.operator} {captcha.num2} = ?
</span>
<button
type="button"
className="captcha-refresh"
onClick={generateCaptcha}
title="Generate new question"
aria-label="Refresh captcha"
>
<i className="fa-solid fa-rotate"></i>
</button>
</div>
<input
type="number"
name="captcha"
id="captcha"
value={captchaAnswer}
onChange={(e) => {
setCaptchaAnswer(e.target.value);
setCaptchaError('');
}}
placeholder="Enter answer"
required
className={captchaError ? 'error' : ''}
/>
{captchaError && (
<span className="captcha-error">
<i className="fa-solid fa-exclamation-circle"></i>
{captchaError}
</span>
)}
</div>
</div>
</div>
{/* Status Message */}

View File

@@ -5,14 +5,15 @@ import Link from "next/link";
import { useMemo } from "react";
import { useServices } from "@/lib/hooks/useServices";
import { serviceUtils } from "@/lib/api/serviceService";
import one from "@/public/images/overview/one.png";
import two from "@/public/images/overview/two.png";
import three from "@/public/images/overview/three.png";
import four from "@/public/images/overview/four.png";
import five from "@/public/images/overview/five.png";
// Default images array for fallback
const defaultImages = [one, two, three, four, five];
// Default images array for fallback - use string paths
const defaultImages = [
"/images/overview/one.png",
"/images/overview/two.png",
"/images/overview/three.png",
"/images/overview/four.png",
"/images/overview/five.png"
];
const Overview = () => {
// Memoize the parameters to prevent infinite re-renders

View File

@@ -1,6 +1,5 @@
import Image from "next/legacy/image";
import Link from "next/link";
import thumb from "@/public/images/leading.jpg";
const ServiceIntro = () => {
return (
@@ -11,7 +10,7 @@ const ServiceIntro = () => {
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
<Link href="services">
<Image
src={thumb}
src="/images/leading.jpg"
alt="Enterprise Software Solutions"
width={400}
height={500}

View File

@@ -273,7 +273,9 @@ const Story = () => {
</Link>
</p>
<h5 className="fw-4 mt-12 mb-12 text-white">
{item.title}
<Link href={`/case-study/${item.slug}`} className="text-white">
{item.title}
</Link>
</h5>
<p className="text-xs">{item.excerpt || item.description?.substring(0, 150) + '...'}</p>
</div>
@@ -300,32 +302,34 @@ const Story = () => {
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;
});
}
}}
/>
<Link href={`/case-study/${item.slug}`} className="w-100">
<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;
});
}
}}
/>
</Link>
</div>
);
})}

View File

@@ -84,7 +84,7 @@ const ServicesBanner = () => {
<ul className="social">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
href="https://www.linkedin.com"
target="_blank"
aria-label="connect with us on linkedin"
>
@@ -93,7 +93,7 @@ const ServicesBanner = () => {
</li>
<li>
<Link
href="https://github.com/gnxtech"
href="https://github.com"
target="_blank"
aria-label="view our code on github"
>

View File

@@ -3,8 +3,6 @@ import { useEffect } from "react";
import Image from "next/legacy/image";
import gsap from "gsap";
import ScrollTrigger from "gsap/dist/ScrollTrigger";
import thumb from "@/public/images/transform-thumb.png";
import teamThumb from "@/public/images/team-thumb.png";
import { Service } from "@/lib/api/serviceService";
import { serviceUtils } from "@/lib/api/serviceService";
@@ -16,20 +14,25 @@ const Transform = ({ service }: TransformProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
gsap.set(".foot-fade", {
x: -100,
opacity: 0,
});
// Check if elements exist before animating
const footFadeElements = document.querySelectorAll(".foot-fade");
if (footFadeElements.length > 0) {
gsap.set(".foot-fade", {
x: -100,
opacity: 0,
});
ScrollTrigger.batch(".foot-fade", {
start: "-100px bottom",
onEnter: (elements) =>
gsap.to(elements, {
x: 0,
opacity: 1,
stagger: 0.3,
}),
});
ScrollTrigger.batch(".foot-fade", {
start: "-100px bottom",
onEnter: (elements) =>
gsap.to(elements, {
x: 0,
opacity: 1,
stagger: 0.3,
}),
});
}
}, []);
return (
@@ -55,7 +58,7 @@ const Transform = ({ service }: TransformProps) => {
<div className="transform__thumb">
<div className="enterprise-image-wrapper">
<Image
src={serviceUtils.getServiceImageUrl(service) || thumb}
src={serviceUtils.getServiceImageUrl(service) || "/images/transform-thumb.png"}
className="enterprise-service-image"
alt={service.title}
width={600}

View File

@@ -27,7 +27,7 @@ const KnowledgeBase = () => {
const filtered = allArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
(article.content && article.content.toLowerCase().includes(searchTerm.toLowerCase()))
);
return {
displayArticles: filtered,

View File

@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
import { markArticleHelpful } from '@/lib/api/supportService';
import { sanitizeHTML } from '@/lib/security/sanitize';
interface KnowledgeBaseArticleModalProps {
slug: string;
@@ -94,7 +95,7 @@ const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalP
<div className="article-body">
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
dangerouslySetInnerHTML={{ __html: sanitizeHTML(article.content || article.summary) }}
/>
</div>

View File

@@ -1,4 +1,5 @@
"use client";
import Link from "next/link";
type ModalType = 'create' | 'knowledge' | 'status' | null;
@@ -102,7 +103,7 @@ const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
</div>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
<Link
href="/policy?type=privacy"
className="feature-item clickable link-item"
>
@@ -111,10 +112,10 @@ const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
</div>
<h3>Privacy Policy</h3>
<p>Learn about data protection</p>
</a>
</Link>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
<Link
href="/policy?type=terms"
className="feature-item clickable link-item"
>
@@ -123,10 +124,10 @@ const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
</div>
<h3>Terms of Use</h3>
<p>Review our service terms</p>
</a>
</Link>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
<Link
href="/policy?type=support"
className="feature-item clickable link-item"
>
@@ -135,7 +136,7 @@ const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
</div>
<h3>Support Policy</h3>
<p>Understand our support coverage</p>
</a>
</Link>
</div>
</div>
</div>
@@ -148,4 +149,3 @@ const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
};
export default SupportCenterHero;

View File

@@ -70,7 +70,6 @@ const SmoothScroll = () => {
gestureOrientation: 'vertical',
smoothWheel: true,
wheelMultiplier: 1,
smoothTouch: false,
touchMultiplier: 2,
infinite: false,
});

View File

@@ -289,7 +289,7 @@ const Footer = () => {
<div className="col-12 col-lg-6">
<div className="social-links justify-content-center justify-content-lg-end">
<Link
href="https://www.linkedin.com/company/gnxtech"
href="https://www.linkedin.com"
target="_blank"
rel="noopener noreferrer"
title="LinkedIn"
@@ -298,7 +298,7 @@ const Footer = () => {
<i className="fa-brands fa-linkedin-in"></i>
</Link>
<Link
href="https://github.com/gnxtech"
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
title="GitHub"

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 logoLight from "@/public/images/logo-light.png";
interface OffcanvasMenuProps {
isOffcanvasOpen: boolean;
@@ -67,7 +66,7 @@ const OffcanvasMenu = ({
<div className="offcanvas-menu__header nav-fade">
<div className="logo">
<Link href="/" className="logo-img">
<Image src={logoLight} priority alt="Image" title="Logo" width={160} height={60} />
<Image src="/images/logo-light.png" priority alt="Image" title="Logo" width={160} height={60} />
</Link>
</div>
<button
@@ -176,7 +175,7 @@ const OffcanvasMenu = ({
<ul className="enterprise-social nav-fade">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
href="https://www.linkedin.com"
target="_blank"
aria-label="Connect with us on LinkedIn"
>
@@ -185,7 +184,7 @@ const OffcanvasMenu = ({
</li>
<li>
<Link
href="https://github.com/gnxtech"
href="https://github.com"
target="_blank"
aria-label="View our code on GitHub"
>