This commit is contained in:
Iliyan Angelov
2025-11-24 03:52:08 +02:00
parent dfcaebaf8c
commit 366f28677a
18241 changed files with 865352 additions and 567 deletions

3
frontEnd/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
frontEnd/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,35 @@
"use client";
import { useEffect } from 'react';
import Header from "@/components/shared/layout/header/Header";
import AboutBanner from "@/components/pages/about/AboutBanner";
import AboutServiceComponent from "@/components/pages/about/AboutService";
import Footer from "@/components/shared/layout/footer/Footer";
import AboutScrollProgressButton from "@/components/pages/about/AboutScrollProgressButton";
import AboutInitAnimations from "@/components/pages/about/AboutInitAnimations";
import AboutStarter from "@/components/pages/about/AboutStarter";
// Note: Since this is a client component, we'll set metadata via useEffect
const AboutUsPage = () => {
useEffect(() => {
document.title = "About Us - Enterprise Software Development Company | GNX Soft";
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
metaDescription.setAttribute('content', 'Learn about GNX Soft - a leading enterprise software development company with expertise in custom software, data replication, AI business intelligence, and comprehensive IT solutions.');
}
}, []);
return (
<div className="enterprise-about-page">
<Header />
<main>
<AboutBanner />
<AboutServiceComponent />
<AboutStarter />
</main>
<Footer />
<AboutScrollProgressButton />
<AboutInitAnimations />
</div>
);
};
export default AboutUsPage;

View File

@@ -0,0 +1,79 @@
"use client";
import { useParams } from "next/navigation";
import Header from "@/components/shared/layout/header/Header";
import JobSingle from "@/components/pages/career/JobSingle";
import Footer from "@/components/shared/layout/footer/Footer";
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
import { useJob } from "@/lib/hooks/useCareer";
const JobPage = () => {
const params = useParams();
const slug = params?.slug as string;
const { job, loading, error } = useJob(slug);
if (loading) {
return (
<div className="tp-app">
<Header />
<main>
<section className="pt-120 pb-120">
<div className="container">
<div className="row">
<div className="col-12 text-center">
<h2>Loading job details...</h2>
</div>
</div>
</div>
</section>
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
}
if (error || !job) {
return (
<div className="tp-app">
<Header />
<main>
<section className="pt-120 pb-120">
<div className="container">
<div className="row">
<div className="col-12 text-center">
<h2 className="text-danger">Job Not Found</h2>
<p className="mt-24">
The job position you are looking for does not exist or is no longer available.
</p>
<a href="/career" className="btn mt-40">
View All Positions
</a>
</div>
</div>
</div>
</section>
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
}
return (
<div className="tp-app">
<Header />
<main>
<JobSingle job={job} />
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
};
export default JobPage;

View File

@@ -0,0 +1,41 @@
import { Metadata } from 'next';
import Header from "@/components/shared/layout/header/Header";
import CareerBanner from "@/components/pages/career/CareerBanner";
import OpenPosition from "@/components/pages/career/OpenPosition";
import Thrive from "@/components/pages/career/Thrive";
import Footer from "@/components/shared/layout/footer/Footer";
import CareerScrollProgressButton from "@/components/pages/career/CareerScrollProgressButton";
import CareerInitAnimations from "@/components/pages/career/CareerInitAnimations";
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
export const metadata: Metadata = createMetadata({
title: "Careers - Join Our Team",
description: "Explore career opportunities at GNX Soft. Join our team of talented professionals working on cutting-edge enterprise software solutions. View open positions and apply today.",
keywords: [
"Careers",
"Job Openings",
"Software Development Jobs",
"Join Our Team",
"Tech Careers",
"Employment Opportunities",
],
url: "/career",
});
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<CareerBanner />
<OpenPosition />
<Thrive />
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,33 @@
import Header from "@/components/shared/layout/header/Header";
import CaseSingle from "@/components/pages/case-study/CaseSingle";
import Process from "@/components/pages/case-study/Process";
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";
interface PageProps {
params: Promise<{
slug: string;
}>;
}
const page = async ({ params }: PageProps) => {
const { slug } = await params;
return (
<div className="tp-app">
<Header />
<main>
<CaseSingle slug={slug} />
<Process slug={slug} />
<RelatedCase slug={slug} />
</main>
<Footer />
<CaseStudyScrollProgressButton />
<CaseStudyInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,37 @@
import { Metadata } from 'next';
import Header from "@/components/shared/layout/header/Header";
import CaseItems from "@/components/pages/case-study/CaseItems";
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 { generateMetadata as createMetadata } from "@/lib/seo/metadata";
export const metadata: Metadata = createMetadata({
title: "Case Studies - Success Stories & Client Projects",
description: "Explore our case studies showcasing successful enterprise software development projects, client success stories, and real-world implementations of our technology solutions.",
keywords: [
"Case Studies",
"Success Stories",
"Client Projects",
"Software Development Portfolio",
"Enterprise Solutions Examples",
"Client Testimonials",
],
url: "/case-study",
});
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<CaseItems />
</main>
<Footer />
<CaseStudyScrollProgressButton />
<CaseStudyInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,36 @@
import { Metadata } from 'next';
import Header from "@/components/shared/layout/header/Header";
import ContactSection from "@/components/pages/contact/ContactSection";
import Footer from "@/components/shared/layout/footer/Footer";
import ContactScrollProgressButton from "@/components/pages/contact/ContactScrollProgressButton";
import ContactInitAnimations from "@/components/pages/contact/ContactInitAnimations";
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
export const metadata: Metadata = createMetadata({
title: "Contact Us - Get in Touch with Our Team",
description: "Contact GNX Soft for enterprise software development solutions. Get a free consultation, discuss your project requirements, or request a quote for our services.",
keywords: [
"Contact GNX Soft",
"Software Development Quote",
"Enterprise Solutions Consultation",
"Custom Software Inquiry",
"Get in Touch",
],
url: "/contact-us",
});
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<ContactSection />
</main>
<Footer />
<ContactScrollProgressButton />
<ContactInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,23 @@
import Header from "@/components/shared/layout/header/Header";
import BlogSingle from "@/components/pages/blog/BlogSingle";
import LatestPost from "@/components/pages/blog/LatestPost";
import Footer from "@/components/shared/layout/footer/Footer";
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<BlogSingle />
<LatestPost />
</main>
<Footer />
<BlogScrollProgressButton />
<BlogInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,37 @@
import { Metadata } from 'next';
import Header from "@/components/shared/layout/header/Header";
import BlogItems from "@/components/pages/blog/BlogItems";
import Footer from "@/components/shared/layout/footer/Footer";
import BlogScrollProgressButton from "@/components/pages/blog/BlogScrollProgressButton";
import BlogInitAnimations from "@/components/pages/blog/BlogInitAnimations";
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
export const metadata: Metadata = createMetadata({
title: "Insights & Blog - Technology Trends & Best Practices",
description: "Stay updated with the latest insights on enterprise software development, technology trends, best practices, and industry news from GNX Soft's expert team.",
keywords: [
"Technology Blog",
"Software Development Insights",
"Tech Trends",
"Enterprise Software Blog",
"Development Best Practices",
"Industry News",
],
url: "/insights",
});
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<BlogItems />
</main>
<Footer />
<BlogScrollProgressButton />
<BlogInitAnimations />
</div>
);
};
export default page;

166
frontEnd/app/layout.tsx Normal file
View File

@@ -0,0 +1,166 @@
import type { Metadata } from "next";
import { Inter, Montserrat } from "next/font/google";
import "@/public/styles/main.scss";
import { CookieConsentProvider } from "@/components/shared/layout/CookieConsentContext";
import { CookieConsent } from "@/components/shared/layout/CookieConsent";
import LayoutWrapper from "@/components/shared/layout/LayoutWrapper";
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
import { OrganizationSchema, WebsiteSchema, LocalBusinessSchema } from "@/components/shared/seo/StructuredData";
const montserrat = Montserrat({
subsets: ["latin"],
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--mont",
fallback: [
"-apple-system",
"Segoe UI",
"Roboto",
"Ubuntu",
"Fira Sans",
"Arial",
"sans-serif",
],
});
const inter = Inter({
subsets: ["latin"],
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--inter",
fallback: [
"-apple-system",
"Segoe UI",
"Roboto",
"Ubuntu",
"Fira Sans",
"Arial",
"sans-serif",
],
});
// Enhanced SEO metadata for root layout
export const metadata: Metadata = createMetadata({
title: "Enterprise Software Development & IT Solutions",
description: "Leading enterprise software development company specializing in custom software, data replication, incident management, AI business intelligence, and comprehensive system integrations for modern businesses.",
keywords: [
"Enterprise Software Development",
"Custom Software Solutions",
"Data Replication Services",
"Incident Management SaaS",
"AI Business Intelligence",
"Backend Engineering",
"Frontend Engineering",
"Systems Integration",
],
url: "/",
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" style={{ scrollBehavior: 'auto', overflow: 'auto' }}>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
window.scrollTo(0, 0);
`,
}}
/>
{/* Content Protection Script */}
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
if (typeof window === 'undefined') return;
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
// Disable right-click
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
return false;
});
// Disable keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl+C, Ctrl+X, Ctrl+S, Ctrl+A, Ctrl+P, Ctrl+U, Ctrl+I, Ctrl+J
if ((e.ctrlKey || e.metaKey) && ['c','x','s','a','p','u','i','j','k'].includes(e.key)) {
e.preventDefault();
return false;
}
// F12
if (e.key === 'F12' || e.keyCode === 123) {
e.preventDefault();
return false;
}
// Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+Shift+C
if ((e.ctrlKey || e.metaKey) && e.shiftKey && ['I','J','C'].includes(e.key)) {
e.preventDefault();
return false;
}
});
// Disable text selection
document.addEventListener('selectstart', function(e) {
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
e.preventDefault();
return false;
}
});
// Disable image dragging
document.addEventListener('dragstart', function(e) {
e.preventDefault();
return false;
});
// Disable copy/cut
document.addEventListener('copy', function(e) {
e.preventDefault();
return false;
});
document.addEventListener('cut', function(e) {
e.preventDefault();
return false;
});
});
})();
`,
}}
/>
</head>
<body className={`${inter.variable} ${montserrat.variable} content-protected`} style={{ scrollBehavior: 'auto', overflow: 'auto' }}>
{/* Structured Data for SEO */}
<OrganizationSchema />
<WebsiteSchema />
<LocalBusinessSchema />
<CookieConsentProvider
config={{
companyName: "GNX Soft",
privacyPolicyUrl: "/policy?type=privacy",
cookiePolicyUrl: "/policy?type=privacy",
dataControllerEmail: "privacy@gnxsoft.com",
retentionPeriod: 365,
enableAuditLog: true,
enableDetailedSettings: true,
showPrivacyNotice: true,
}}
>
<LayoutWrapper>
{children}
</LayoutWrapper>
<CookieConsent />
</CookieConsentProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,31 @@
import Link from "next/link";
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
const page = () => {
return (
<div className="tp-app">
<div className="tp-error pt-120 pb-120 text-center">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-8">
<h1 className="fw-7 text-uppercase mt-8">404 ERROR</h1>
<p className="text-xl fw-5 mt-12">Page Not Found</p>
<div className="mt-40 d-flex justify-content-center">
<Link href="/" className="btn-anim btn-anim-light">
Go Home
<i className="fa-solid fa-arrow-trend-up"></i>
<span></span>
</Link>
</div>
</div>
</div>
</div>
</div>
<HomeScrollProgressButton />
<HomeInitAnimations />
</div>
);
};
export default page;

29
frontEnd/app/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
import Header from "@/components/shared/layout/header/Header";
import HomeBanner from "@/components/pages/home/HomeBanner";
import Overview from "@/components/pages/home/Overview";
import Story from "@/components/pages/home/Story";
import ServiceIntro from "@/components/pages/home/ServiceIntro";
import HomeLatestPost from "@/components/pages/home/HomeLatestPost";
import Footer from "@/components/shared/layout/footer/Footer";
import HomeScrollProgressButton from "@/components/pages/home/HomeScrollProgressButton";
import HomeInitAnimations from "@/components/pages/home/HomeInitAnimations";
const page = () => {
return (
<div className="tp-app">
<Header />
<main>
<HomeBanner />
<Overview />
<Story />
<ServiceIntro />
<HomeLatestPost />
</main>
<Footer />
<HomeScrollProgressButton />
<HomeInitAnimations />
</div>
);
};
export default page;

View File

@@ -0,0 +1,399 @@
"use client";
import { useSearchParams } from 'next/navigation';
import Header from "@/components/shared/layout/header/Header";
import Footer from "@/components/shared/layout/footer/Footer";
import { Suspense } from 'react';
import { usePolicy } from '@/lib/hooks/usePolicy';
const PolicyContent = () => {
const searchParams = useSearchParams();
const typeParam = searchParams.get('type') || 'privacy';
const type = typeParam as 'privacy' | 'terms' | 'support';
const { data: policy, isLoading, error } = usePolicy(type);
if (isLoading) {
return (
<section className="policy-section section-padding">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10">
<div className="loading-state">
<div className="spinner"></div>
<p>Loading policy...</p>
</div>
</div>
</div>
</div>
<style jsx>{`
.policy-section {
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
min-height: 100vh;
padding: 120px 0 80px;
}
.loading-state {
text-align: center;
padding: 4rem 2rem;
}
.spinner {
width: 50px;
height: 50px;
margin: 0 auto 1rem;
border: 4px solid #f3f3f3;
border-top: 4px solid #daa520;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
color: #64748b;
font-size: 1.1rem;
}
`}</style>
</section>
);
}
if (error || !policy) {
return (
<section className="policy-section section-padding">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10">
<div className="error-state">
<i className="fa-solid fa-exclamation-circle"></i>
<h2>Unable to Load Policy</h2>
<p>{error?.message || 'The requested policy could not be found.'}</p>
<a href="/support-center" className="btn btn-primary">Return to Support Center</a>
</div>
</div>
</div>
</div>
<style jsx>{`
.policy-section {
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
min-height: 100vh;
padding: 120px 0 80px;
}
.error-state {
text-align: center;
padding: 4rem 2rem;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.error-state i {
font-size: 4rem;
color: #ef4444;
margin-bottom: 1rem;
}
.error-state h2 {
font-size: 2rem;
color: #1e293b;
margin-bottom: 1rem;
}
.error-state p {
color: #64748b;
font-size: 1.1rem;
margin-bottom: 2rem;
}
.btn-primary {
display: inline-block;
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #daa520, #d4af37);
color: #0f172a;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(218, 165, 32, 0.3);
}
`}</style>
</section>
);
}
return (
<section className="policy-section section-padding">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10">
{/* Policy Header */}
<div className="policy-header">
<h1 className="policy-title">{policy.title}</h1>
<div className="policy-meta">
<p className="policy-updated">
Last Updated: {new Date(policy.last_updated).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
<p className="policy-version">Version {policy.version}</p>
<p className="policy-effective">
Effective Date: {new Date(policy.effective_date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
{policy.description && (
<p className="policy-description">{policy.description}</p>
)}
</div>
{/* Policy Content */}
<div className="policy-content">
{policy.sections.map((section) => (
<div key={section.id} className="policy-section-item">
<h2 className="policy-heading">{section.heading}</h2>
<div className="policy-text" dangerouslySetInnerHTML={{
__html: section.content
// First, handle main sections with (a), (b), etc.
.replace(/\(a\)/g, '<br/><br/><strong>(a)</strong>')
.replace(/\(b\)/g, '<br/><br/><strong>(b)</strong>')
.replace(/\(c\)/g, '<br/><br/><strong>(c)</strong>')
.replace(/\(d\)/g, '<br/><br/><strong>(d)</strong>')
.replace(/\(e\)/g, '<br/><br/><strong>(e)</strong>')
.replace(/\(f\)/g, '<br/><br/><strong>(f)</strong>')
.replace(/\(g\)/g, '<br/><br/><strong>(g)</strong>')
.replace(/\(h\)/g, '<br/><br/><strong>(h)</strong>')
.replace(/\(i\)/g, '<br/><br/><strong>(i)</strong>')
.replace(/\(j\)/g, '<br/><br/><strong>(j)</strong>')
.replace(/\(k\)/g, '<br/><br/><strong>(k)</strong>')
.replace(/\(l\)/g, '<br/><br/><strong>(l)</strong>')
// Handle pipe separators for contact information
.replace(/ \| /g, '<br/><strong>')
.replace(/: /g, ':</strong> ')
// Handle semicolon with parenthesis
.replace(/; \(/g, ';<br/><br/>(')
// Add spacing after periods in long sentences
.replace(/\. ([A-Z])/g, '.<br/><br/>$1')
}} />
</div>
))}
</div>
{/* Contact Section */}
<div className="policy-footer">
<div className="contact-box">
<h3>Questions?</h3>
<p>If you have any questions about this policy, please don't hesitate to contact us.</p>
<a href="/contact-us" className="btn btn-primary">Contact Us</a>
</div>
</div>
</div>
</div>
</div>
<style jsx>{`
.policy-section {
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
min-height: 100vh;
padding: 120px 0 80px;
}
.policy-header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 2px solid #e5e7eb;
}
.policy-title {
font-size: 2.5rem;
font-weight: 700;
color: #0f172a;
margin-bottom: 1rem;
}
.policy-meta {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.policy-meta p {
margin: 0;
}
.policy-updated,
.policy-version,
.policy-effective {
color: #64748b;
font-size: 0.95rem;
font-style: italic;
}
.policy-version {
color: #daa520;
font-weight: 600;
font-style: normal;
}
.policy-description {
color: #475569;
font-size: 1.1rem;
margin-top: 1rem;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.policy-content {
margin-bottom: 3rem;
}
.policy-section-item {
margin-bottom: 2.5rem;
padding: 2rem;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.policy-section-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.policy-heading {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 1rem;
}
.policy-text {
color: #475569;
font-size: 1rem;
line-height: 1.8;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.policy-text strong {
color: #1e293b;
font-weight: 600;
}
.policy-text :global(br) {
content: "";
display: block;
margin: 0.5rem 0;
}
.policy-footer {
margin-top: 4rem;
padding-top: 3rem;
border-top: 2px solid #e5e7eb;
}
.contact-box {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
padding: 3rem;
border-radius: 12px;
text-align: center;
color: #ffffff;
}
.contact-box h3 {
font-size: 1.75rem;
margin-bottom: 1rem;
color: #ffffff;
}
.contact-box p {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 1.5rem;
}
.btn-primary {
display: inline-block;
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #daa520, #d4af37);
color: #0f172a;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(218, 165, 32, 0.3);
}
@media (max-width: 768px) {
.policy-section {
padding: 100px 0 60px;
}
.policy-title {
font-size: 2rem;
}
.policy-meta {
flex-direction: column;
gap: 0.5rem;
}
.policy-section-item {
padding: 1.5rem;
}
.policy-heading {
font-size: 1.25rem;
}
.contact-box {
padding: 2rem;
}
.contact-box h3 {
font-size: 1.5rem;
}
}
`}</style>
</section>
);
};
const PolicyPage = () => {
return (
<div className="tp-app">
<Header />
<main>
<Suspense fallback={<div>Loading...</div>}>
<PolicyContent />
</Suspense>
</main>
<Footer />
</div>
);
};
export default PolicyPage;

35
frontEnd/app/robots.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://gnxsoft.com';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/admin/',
'/_next/',
'/private/',
'/*.json$',
'/*?*',
],
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/api/', '/admin/', '/private/'],
},
{
userAgent: 'Bingbot',
allow: '/',
disallow: ['/api/', '/admin/', '/private/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}

View File

@@ -0,0 +1,88 @@
import { notFound } from 'next/navigation';
import Header from "@/components/shared/layout/header/Header";
import ServiceDetailsBanner from "@/components/shared/banners/ServiceDetailsBanner";
import ServiceDetails from "@/components/pages/services/ServiceDetails";
import ServiceFeatures from "@/components/pages/services/ServiceFeatures";
import ServiceDeliverables from "@/components/pages/services/ServiceDeliverables";
import ServiceProcess from "@/components/pages/services/ServiceProcess";
import Transform from "@/components/pages/services/Transform";
import Footer from "@/components/shared/layout/footer/Footer";
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations";
import { serviceService, Service } from "@/lib/api/serviceService";
import { generateServiceMetadata } from "@/lib/seo/metadata";
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
interface ServicePageProps {
params: Promise<{
slug: string;
}>;
}
// Generate static params for all services (optional - for better performance)
export async function generateStaticParams() {
try {
const services = await serviceService.getServices();
return services.results.map((service: Service) => ({
slug: service.slug,
}));
} catch (error) {
return [];
}
}
// Generate enhanced metadata for each service page
export async function generateMetadata({ params }: ServicePageProps) {
try {
const { slug } = await params;
const service = await serviceService.getServiceBySlug(slug);
return generateServiceMetadata(service);
} catch (error) {
return {
title: 'Service Not Found - GNX',
description: 'The requested service could not be found.',
};
}
}
const ServicePage = async ({ params }: ServicePageProps) => {
let service: Service;
try {
const { slug } = await params;
service = await serviceService.getServiceBySlug(slug);
} catch (error) {
notFound();
}
// Breadcrumb data for structured data
const breadcrumbItems = [
{ name: 'Home', url: '/' },
{ name: 'Services', url: '/services' },
{ name: service.title, url: `/services/${service.slug}` },
];
return (
<div className="enterprise-app">
{/* SEO Structured Data */}
<ServiceSchema service={service} />
<BreadcrumbSchema items={breadcrumbItems} />
<Header />
<main className="enterprise-main">
<ServiceDetailsBanner service={service} />
<ServiceDetails service={service} />
<ServiceFeatures service={service} />
<ServiceDeliverables service={service} />
<Transform service={service} />
<ServiceProcess service={service} />
</main>
<Footer />
<ServicesScrollProgressButton />
<ServicesInitAnimations />
</div>
);
};
export default ServicePage;

View File

@@ -0,0 +1,41 @@
import { Metadata } from 'next';
import Header from "@/components/shared/layout/header/Header";
import ServicesBanner from "@/components/pages/services/ServicesBanner";
import ServiceMain from "@/components/pages/services/ServiceMain";
import Footer from "@/components/shared/layout/footer/Footer";
import ServicesScrollProgressButton from "@/components/pages/services/ServicesScrollProgressButton";
import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnimations";
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
export const metadata: Metadata = createMetadata({
title: "Our Services - Enterprise Software Development",
description: "Explore our comprehensive range of enterprise software development services including custom software, data replication, incident management, AI business intelligence, backend & frontend engineering, and systems integration.",
keywords: [
"Software Development Services",
"Custom Software Development",
"Data Replication",
"Incident Management SaaS",
"AI Business Intelligence",
"Backend Engineering Services",
"Frontend Development",
"Systems Integration Services",
],
url: "/services",
});
const page = () => {
return (
<div className="enterprise-app">
<Header />
<main className="enterprise-main">
<ServicesBanner />
<ServiceMain />
</main>
<Footer />
<ServicesScrollProgressButton />
<ServicesInitAnimations />
</div>
);
};
export default page;

136
frontEnd/app/sitemap.ts Normal file
View File

@@ -0,0 +1,136 @@
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://gnxsoft.com';
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: `${baseUrl}/about-us`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
{
url: `${baseUrl}/services`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
{
url: `${baseUrl}/case-study`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
{
url: `${baseUrl}/insights`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8,
},
{
url: `${baseUrl}/career`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
},
{
url: `${baseUrl}/contact-us`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/support-center`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
},
{
url: `${baseUrl}/policy`,
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.5,
},
];
try {
// Fetch dynamic services
const servicesResponse = await fetch(`${apiUrl}/services/`, {
next: { revalidate: 3600 }, // Revalidate every hour
});
let servicePages: MetadataRoute.Sitemap = [];
if (servicesResponse.ok) {
const services = await servicesResponse.json();
servicePages = services.map((service: any) => ({
url: `${baseUrl}/services/${service.slug}`,
lastModified: new Date(service.updated_at || service.created_at),
changeFrequency: 'weekly' as const,
priority: service.featured ? 0.9 : 0.7,
}));
}
// Fetch dynamic blog posts
const blogResponse = await fetch(`${apiUrl}/blog/`, {
next: { revalidate: 3600 },
});
let blogPages: MetadataRoute.Sitemap = [];
if (blogResponse.ok) {
const posts = await blogResponse.json();
blogPages = posts.map((post: any) => ({
url: `${baseUrl}/insights/${post.slug}`,
lastModified: new Date(post.updated_at || post.published_at),
changeFrequency: 'weekly' as const,
priority: 0.7,
}));
}
// Fetch dynamic case studies
const caseStudiesResponse = await fetch(`${apiUrl}/case-studies/`, {
next: { revalidate: 3600 },
});
let caseStudyPages: MetadataRoute.Sitemap = [];
if (caseStudiesResponse.ok) {
const caseStudies = await caseStudiesResponse.json();
caseStudyPages = caseStudies.map((study: any) => ({
url: `${baseUrl}/case-study/${study.slug}`,
lastModified: new Date(study.updated_at || study.created_at),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
}
// Fetch dynamic career postings
const careerResponse = await fetch(`${apiUrl}/career/jobs`, {
next: { revalidate: 3600 },
});
let careerPages: MetadataRoute.Sitemap = [];
if (careerResponse.ok) {
const positions = await careerResponse.json();
careerPages = positions.map((position: any) => ({
url: `${baseUrl}/career/${position.slug}`,
lastModified: new Date(position.updated_at || position.created_at),
changeFrequency: 'weekly' as const,
priority: 0.6,
}));
}
return [...staticPages, ...servicePages, ...blogPages, ...caseStudyPages, ...careerPages];
} catch (error) {
// Return at least static pages if API fails
return staticPages;
}
}

View File

@@ -0,0 +1,30 @@
"use client";
import Header from "@/components/shared/layout/header/Header";
import Footer from "@/components/shared/layout/footer/Footer";
import SupportCenterHero from "@/components/pages/support/SupportCenterHero";
import SupportCenterContent from "@/components/pages/support/SupportCenterContent";
import { useState } from "react";
type ModalType = 'create' | 'knowledge' | 'status' | null;
const SupportCenterPage = () => {
const [activeModal, setActiveModal] = useState<ModalType>(null);
return (
<div className="tp-app">
<Header />
<main>
<SupportCenterHero onFeatureClick={setActiveModal} />
<SupportCenterContent
activeModal={activeModal}
onClose={() => setActiveModal(null)}
onOpenModal={setActiveModal}
/>
</main>
<Footer />
</div>
);
};
export default SupportCenterPage;

View File

@@ -0,0 +1,37 @@
"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import "odometer/themes/odometer-theme-default.css";
import { useInView } from "react-intersection-observer";
const Odometer = dynamic(() => import("react-odometerjs"), {
ssr: false,
});
interface CounterProps {
value: number;
}
const Counter = ({ value }: CounterProps) => {
const [odometerValue, setOdometerValue] = useState(0);
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
setTimeout(() => {
setOdometerValue(value);
}, 1000);
}
}, [inView, value]);
return (
<span ref={ref}>
{inView ? (
<Odometer value={odometerValue} format="(,ddd)" theme="default" />
) : (
0
)}
</span>
);
};
export default Counter;

View File

@@ -0,0 +1,241 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useAbout } from "@/lib/hooks/useAbout";
const AboutBanner = () => {
const { data, loading, error } = useAbout();
const [currentMetric, setCurrentMetric] = useState(0);
useEffect(() => {
// Auto-rotate metrics
const interval = setInterval(() => {
setCurrentMetric(prev => (prev + 1) % 3);
}, 4000);
return () => clearInterval(interval);
}, []);
// Show loading state
if (loading) {
return (
<section className="about-banner fix-top pb-120 position-relative overflow-x-clip">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3">Loading about us content...</p>
</div>
</div>
</div>
</div>
</section>
);
}
// Show error state
if (error) {
return (
<section className="about-banner fix-top pb-120 position-relative overflow-x-clip">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="alert alert-danger" role="alert">
<h4 className="alert-heading">Error Loading Content</h4>
<p>{error}</p>
<hr />
<p className="mb-0">Please try refreshing the page or contact support if the problem persists.</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
// Use API data or fallback to default content
const bannerData = data?.banner;
const metrics = [
{ value: "8", label: "Industry Verticals", icon: "fa-industry", color: "#3b82f6" },
{ value: "99.9%", label: "Uptime SLA", icon: "fa-shield-halved", color: "#10b981" },
{ value: "24/7", label: "Enterprise Support", icon: "fa-headset", color: "#f59e0b" },
{ value: "5+", label: "Years of Operation", icon: "fa-award", color: "#8b5cf6" }
];
return (
<section className="hero-banner">
{/* Video-like Background */}
<div className="hero-background">
{/* Animated Code Lines */}
<div className="code-animation">
<div className="code-line line-1">
<span className="code-keyword">const</span> enterprise = <span className="code-string">&apos;mission-critical&apos;</span>;
</div>
<div className="code-line line-2">
<span className="code-keyword">if</span> (security.level === <span className="code-string">&apos;enterprise&apos;</span>) &#123;
</div>
<div className="code-line line-3">
&nbsp;&nbsp;deploy<span className="code-function">()</span>;
</div>
<div className="code-line line-4">
&#125;
</div>
</div>
{/* Floating Tech Icons */}
<div className="floating-tech">
<div className="tech-icon icon-1">
<i className="fa-solid fa-shield-halved"></i>
</div>
<div className="tech-icon icon-2">
<i className="fa-solid fa-cloud"></i>
</div>
<div className="tech-icon icon-3">
<i className="fa-solid fa-server"></i>
</div>
<div className="tech-icon icon-4">
<i className="fa-solid fa-lock"></i>
</div>
<div className="tech-icon icon-5">
<i className="fa-solid fa-chart-line"></i>
</div>
<div className="tech-icon icon-6">
<i className="fa-solid fa-database"></i>
</div>
</div>
{/* Enterprise Dashboard Elements */}
<div className="dashboard-elements">
<div className="dashboard-card card-1">
<div className="card-header">
<i className="fa-solid fa-shield-check"></i>
<span>Security Status</span>
</div>
<div className="card-content">
<div className="status-indicator resolved"></div>
<span>Enterprise Grade</span>
</div>
</div>
<div className="dashboard-card card-2">
<div className="card-header">
<i className="fa-solid fa-clock"></i>
<span>SLA Response</span>
</div>
<div className="card-content">
<div className="metric-value">99.9%</div>
<span>Uptime</span>
</div>
</div>
<div className="dashboard-card card-3">
<div className="card-header">
<i className="fa-solid fa-building"></i>
<span>Enterprise Clients</span>
</div>
<div className="card-content">
<div className="metric-value">500+</div>
<span>Fortune 500</span>
</div>
</div>
</div>
{/* Network Connection Lines */}
<div className="network-lines">
<div className="connection-line line-1"></div>
<div className="connection-line line-2"></div>
<div className="connection-line line-3"></div>
<div className="connection-line line-4"></div>
</div>
{/* Data Flow Particles */}
<div className="data-particles">
<div className="particle particle-1"></div>
<div className="particle particle-2"></div>
<div className="particle particle-3"></div>
<div className="particle particle-4"></div>
<div className="particle particle-5"></div>
<div className="particle particle-6"></div>
</div>
{/* Background Gradient Overlay */}
<div className="video-overlay"></div>
</div>
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10 col-xl-8">
<div className="hero-content text-center">
{/* Badge */}
<div className="hero-badge">
<div className="badge-icon">
<i className="fa-solid fa-shield-check"></i>
</div>
<span>Enterprise Security Certified</span>
</div>
{/* Main Title */}
<h1 className="hero-title">
{bannerData?.title || "Mission-Critical Software Solutions for Global Enterprises"}
</h1>
{/* Description */}
<p className="hero-description">
{bannerData?.description || "GNX Soft Ltd delivers enterprise-grade software solutions for mission-critical industries with 99.9% uptime SLA and 24/7 dedicated support. Our platforms power digital transformation across Defense & Aerospace, Healthcare, Banking, Telecommunication, and other highly regulated sectors."}
</p>
{/* Key Metrics */}
<div className="hero-metrics">
{metrics.map((metric, index) => (
<div
key={index}
className={`metric-item ${currentMetric === index ? 'active' : ''}`}
onClick={() => setCurrentMetric(index)}
>
<div className="metric-icon" style={{ backgroundColor: metric.color }}>
<i className={`fa-solid ${metric.icon}`}></i>
</div>
<div className="metric-content">
<div className="metric-value">{metric.value}</div>
<div className="metric-label">{metric.label}</div>
</div>
</div>
))}
</div>
{/* CTA Buttons */}
<div className="hero-actions">
<Link href={bannerData?.cta_link || "services"} className="btn-primary">
<span>View Enterprise Solutions</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
<Link href="contact-us" className="btn-secondary">
<span>Schedule Consultation</span>
<i className="fa-solid fa-calendar-check"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
{/* Social Links */}
<div className="social-links">
<Link href="https://www.linkedin.com/company/gnxtech" target="_blank" className="social-link">
<i className="fa-brands fa-linkedin-in"></i>
</Link>
<Link href="https://github.com/gnxtech" target="_blank" className="social-link">
<i className="fa-brands fa-github"></i>
</Link>
</div>
</section>
);
};
export default AboutBanner;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const AboutInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default AboutInitAnimations;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const AboutScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default AboutScrollProgressButton;

View File

@@ -0,0 +1,566 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useAbout } from "@/lib/hooks/useAbout";
import { AboutService as AboutServiceType, AboutProcess } from "@/lib/api/aboutService";
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
import thumb from "@/public/images/service/two.png";
import thumbTwo from "@/public/images/service/three.png";
const AboutServiceComponent = () => {
const { data, loading, error } = useAbout();
if (loading) {
return (
<section className="about-service-section">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3">Loading service content...</p>
</div>
</div>
</div>
</div>
</section>
);
}
if (error) {
return (
<section className="about-service-section">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="alert alert-danger" role="alert">
<h4 className="alert-heading">Error Loading Content</h4>
<p>{error}</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
const serviceData = data?.service as AboutServiceType | undefined;
const processData = data?.process as AboutProcess | undefined;
return (
<>
<section className="about-service-section" suppressHydrationWarning>
<div className="container">
<div className="row g-5 align-items-start">
{/* Image Column */}
<div className="col-12 col-lg-6">
<div className="about-image-wrapper">
<div className="about-image-container">
{serviceData?.image_url ? (
<img
src={getValidImageUrl(serviceData.image_url, FALLBACK_IMAGES.SERVICE)}
className="about-image"
alt="Enterprise Technology Solutions"
width={600}
height={700}
/>
) : (
<Image
src={thumb}
className="about-image"
alt="Enterprise Technology Solutions"
width={600}
height={700}
/>
)}
<div className="image-overlay"></div>
</div>
</div>
</div>
{/* Content Column */}
<div className="col-12 col-lg-6">
<div className="about-content-wrapper">
<div className="luxury-badge">
<i className={serviceData?.badge_icon || "fa-solid fa-users"}></i>
<span>{serviceData?.badge_text || "About Our Company"}</span>
</div>
<h2 className="luxury-title">
{serviceData?.title || "GNX Soft Ltd. - Software Excellence"}
</h2>
<p className="luxury-description">
{serviceData?.description || "Founded in 2020, GNX Soft Ltd. has emerged as a premier enterprise software company, delivering mission-critical software solutions across various industries."}
</p>
<div className="luxury-features-grid">
{serviceData?.features && serviceData.features.length > 0 ? (
serviceData.features.map((feature, index) => (
<div key={index} className="luxury-feature-card">
<div className="feature-icon-wrapper">
<i className={feature.icon}></i>
</div>
<div className="feature-text">
<h6 className="feature-title">{feature.title}</h6>
<p className="feature-description">{feature.description}</p>
</div>
</div>
))
) : (
<>
<div className="luxury-feature-card">
<div className="feature-icon-wrapper">
<i className="fa-solid fa-shield-halved"></i>
</div>
<div className="feature-text">
<h6 className="feature-title">Enterprise Security</h6>
<p className="feature-description">Defense-Grade Protection</p>
</div>
</div>
<div className="luxury-feature-card">
<div className="feature-icon-wrapper">
<i className="fa-solid fa-cloud"></i>
</div>
<div className="feature-text">
<h6 className="feature-title">Cloud Native</h6>
<p className="feature-description">AWS, Azure, GCP Partners</p>
</div>
</div>
</>
)}
</div>
<div className="luxury-cta">
<Link href={serviceData?.cta_link || "service-single"} className="luxury-btn">
<span>{serviceData?.cta_text || "Explore Our Solutions"}</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="about-process-section">
<div className="container">
<div className="row g-5 align-items-start">
{/* Content Column */}
<div className="col-12 col-lg-6 order-2 order-lg-1">
<div className="about-content-wrapper">
<div className="luxury-badge">
<i className={processData?.badge_icon || "fa-solid fa-cogs"}></i>
<span>{processData?.badge_text || "Our Methodology"}</span>
</div>
<h2 className="luxury-title">
{processData?.title || "Enterprise Development Process"}
</h2>
<p className="luxury-description">
{processData?.description || "Our proven enterprise development methodology combines agile practices with enterprise-grade security, scalability, and compliance requirements."}
</p>
<div className="luxury-process-steps">
{processData?.steps && processData.steps.length > 0 ? (
processData.steps.map((step, index) => (
<div key={index} className="luxury-process-step">
<div className="step-number-badge">{step.step_number}</div>
<div className="step-content-wrapper">
<h6 className="step-title">{step.title}</h6>
<p className="step-description">{step.description}</p>
</div>
</div>
))
) : (
<>
<div className="luxury-process-step">
<div className="step-number-badge">01</div>
<div className="step-content-wrapper">
<h6 className="step-title">Discovery & Planning</h6>
<p className="step-description">Comprehensive analysis and architecture design</p>
</div>
</div>
<div className="luxury-process-step">
<div className="step-number-badge">02</div>
<div className="step-content-wrapper">
<h6 className="step-title">Development & Testing</h6>
<p className="step-description">Agile development with continuous testing</p>
</div>
</div>
</>
)}
</div>
<div className="luxury-cta">
<Link href={processData?.cta_link || "service-single"} className="luxury-btn">
<span>{processData?.cta_text || "View Our Services"}</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
</div>
</div>
</div>
{/* Image Column */}
<div className="col-12 col-lg-6 order-1 order-lg-2">
<div className="about-image-wrapper">
<div className="about-image-container">
{processData?.image_url ? (
<img
src={getValidImageUrl(processData.image_url, FALLBACK_IMAGES.SERVICE)}
className="about-image"
alt="Enterprise Development Process"
width={600}
height={700}
/>
) : (
<Image
src={thumbTwo}
className="about-image"
alt="Enterprise Development Process"
width={600}
height={700}
/>
)}
<div className="image-overlay"></div>
</div>
</div>
</div>
</div>
</div>
</section>
<style>{`
/* Section Base Styles */
.about-service-section,
.about-process-section {
padding: 120px 0;
position: relative;
background: #0a0a0a;
}
.about-process-section {
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
/* Image Wrapper - Perfect Alignment */
.about-image-wrapper {
position: sticky;
top: 120px;
height: fit-content;
}
.about-image-container {
position: relative;
border-radius: 24px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.05);
background: rgba(255, 255, 255, 0.02);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.about-image-container:hover {
transform: translateY(-8px);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
.about-image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
border-radius: 24px;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.about-image-container:hover .about-image {
transform: scale(1.05);
}
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.3) 100%
);
pointer-events: none;
border-radius: 24px;
}
/* Content Wrapper - Perfect Alignment */
.about-content-wrapper {
padding: 0;
display: flex;
flex-direction: column;
gap: 2rem;
}
@media (min-width: 992px) {
.about-content-wrapper {
padding-left: 3rem;
}
}
/* Luxury Badge */
.luxury-badge {
display: inline-flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1.25rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 50px;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.5px;
color: rgba(255, 255, 255, 0.9);
width: fit-content;
backdrop-filter: blur(10px);
}
.luxury-badge i {
font-size: 0.875rem;
color: #fff;
}
/* Luxury Title */
.luxury-title {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
color: #ffffff;
margin: 0;
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 255, 255, 0.8) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Luxury Description */
.luxury-description {
font-size: 1.125rem;
line-height: 1.8;
color: rgba(255, 255, 255, 0.75);
margin: 0;
font-weight: 400;
}
/* Features Grid */
.luxury-features-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
margin: 1rem 0;
}
@media (min-width: 768px) {
.luxury-features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.luxury-feature-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1.5rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.luxury-feature-card:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-4px);
}
.feature-icon-wrapper {
flex-shrink: 0;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 14px;
}
.feature-icon-wrapper i {
font-size: 1.5rem;
color: #ffffff;
}
.feature-text {
flex: 1;
}
.feature-title {
font-size: 1.125rem;
font-weight: 600;
color: #ffffff;
margin: 0 0 0.5rem 0;
line-height: 1.4;
}
.feature-description {
font-size: 0.9375rem;
line-height: 1.6;
color: rgba(255, 255, 255, 0.65);
margin: 0;
}
/* Process Steps */
.luxury-process-steps {
display: flex;
flex-direction: column;
gap: 1.25rem;
margin: 1rem 0;
}
.luxury-process-step {
display: flex;
align-items: flex-start;
gap: 1.25rem;
padding: 1.75rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 18px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.luxury-process-step:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
border-color: rgba(255, 255, 255, 0.15);
transform: translateX(8px);
}
.step-number-badge {
flex-shrink: 0;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
font-size: 1.5rem;
font-weight: 700;
color: #ffffff;
}
.step-content-wrapper {
flex: 1;
padding-top: 0.25rem;
}
.step-title {
font-size: 1.25rem;
font-weight: 600;
color: #ffffff;
margin: 0 0 0.5rem 0;
line-height: 1.4;
}
.step-description {
font-size: 1rem;
line-height: 1.7;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
/* Luxury CTA Button */
.luxury-cta {
margin-top: 0.5rem;
}
.luxury-btn {
display: inline-flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 2rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
color: #ffffff;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
}
.luxury-btn:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.luxury-btn i {
transition: transform 0.3s ease;
}
.luxury-btn:hover i {
transform: translateX(4px);
}
/* Responsive Design */
@media (max-width: 991px) {
.about-service-section,
.about-process-section {
padding: 80px 0;
}
.about-image-wrapper {
position: relative;
top: 0;
margin-bottom: 3rem;
}
.about-content-wrapper {
padding-left: 0 !important;
padding-right: 0 !important;
}
.luxury-features-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 575px) {
.luxury-title {
font-size: 1.75rem;
}
.luxury-description {
font-size: 1rem;
}
.luxury-feature-card,
.luxury-process-step {
padding: 1.25rem;
}
}
`}</style>
</>
);
};
export default AboutServiceComponent;

View File

@@ -0,0 +1,149 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useAbout } from "@/lib/hooks/useAbout";
import { AboutJourney } from "@/lib/api/aboutService";
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
import thumb from "@/public/images/start-thumb.png";
const AboutStarter = () => {
const { data, loading, error } = useAbout();
if (loading) {
return (
<section className="about-journey-section">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="spinner-border text-light" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3 text-white">Loading journey content...</p>
</div>
</div>
</div>
</div>
</section>
);
}
if (error) {
return (
<section className="about-journey-section">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center py-5">
<div className="alert alert-danger" role="alert">
<h4 className="alert-heading">Error Loading Content</h4>
<p>{error}</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
const journeyData = data?.journey as AboutJourney | undefined;
return (
<section className="about-journey-section" suppressHydrationWarning>
<div className="container">
<div className="row g-3 align-items-start">
{/* Image Column */}
<div className="col-12 col-lg-6 order-1 order-lg-2">
<div className="about-image-wrapper">
<div className="about-image-container">
{journeyData?.image_url ? (
<img
src={getValidImageUrl(journeyData.image_url, FALLBACK_IMAGES.DEFAULT)}
className="about-image"
alt="Enterprise Journey"
width={600}
height={700}
/>
) : (
<Image
src={thumb}
className="about-image"
alt="Enterprise Journey"
width={600}
height={700}
/>
)}
<div className="image-overlay"></div>
</div>
</div>
</div>
{/* Content Column */}
<div className="col-12 col-lg-6 order-2 order-lg-1">
<div className="about-content-wrapper">
<div className="luxury-badge">
<i className={journeyData?.badge_icon || "fa-solid fa-rocket"}></i>
<span>{journeyData?.badge_text || "Our Journey"}</span>
</div>
<h2 className="luxury-title">
{journeyData?.title || "Building Enterprise Excellence Since 2020"}
</h2>
<p className="luxury-description">
{journeyData?.description || "Founded in 2020 in Burgas, Bulgaria, GNX Soft Ltd was established with a clear mission: to deliver world-class enterprise software solutions for mission-critical industries."}
</p>
<div className="luxury-milestones">
{journeyData?.milestones && journeyData.milestones.length > 0 ? (
journeyData.milestones.map((milestone, index) => (
<div key={index} className="luxury-milestone-card">
<div className="milestone-year-badge">
<span>{milestone.year}</span>
</div>
<div className="milestone-content-wrapper">
<h6 className="milestone-title">{milestone.title}</h6>
<p className="milestone-description">{milestone.description}</p>
</div>
</div>
))
) : (
<>
<div className="luxury-milestone-card">
<div className="milestone-year-badge">
<span>2020</span>
</div>
<div className="milestone-content-wrapper">
<h6 className="milestone-title">Company Founded</h6>
<p className="milestone-description">GNX Soft Ltd established in Burgas, Bulgaria</p>
</div>
</div>
<div className="luxury-milestone-card">
<div className="milestone-year-badge">
<span>2021</span>
</div>
<div className="milestone-content-wrapper">
<h6 className="milestone-title">Industry Specialization</h6>
<p className="milestone-description">Specialized in 8 mission-critical industries</p>
</div>
</div>
</>
)}
</div>
<div className="luxury-cta">
<Link href={journeyData?.cta_link || "services"} className="luxury-btn">
<span>{journeyData?.cta_text || "Explore Solutions"}</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default AboutStarter;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const BlogInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default BlogInitAnimations;

View File

@@ -0,0 +1,150 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import PostFilterItems from "./post-filter/PostFilterItems";
const BlogItems = () => {
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const postsPerPage = 6;
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
// Scroll to top of posts section
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const handleTotalPagesChange = (total: number) => {
setTotalPages(total);
};
// Generate page numbers to display
const getPageNumbers = () => {
const pages: (number | string)[] = [];
const maxPagesToShow = 5;
if (totalPages <= maxPagesToShow) {
// Show all pages if total is small
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Show first page
pages.push(1);
// Calculate range around current page
let startPage = Math.max(2, currentPage - 1);
let endPage = Math.min(totalPages - 1, currentPage + 1);
// Add ellipsis after first page if needed
if (startPage > 2) {
pages.push('...');
}
// Add pages around current page
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
// Add ellipsis before last page if needed
if (endPage < totalPages - 1) {
pages.push('...');
}
// Show last page
if (totalPages > 1) {
pages.push(totalPages);
}
}
return pages;
};
return (
<section className="fix-top pb-120 blog-m">
<div className="container">
<div className="row align-items-center vertical-column-gap">
<div className="col-12 col-lg-7">
<h2 className="mt-8 fw-7 text-secondary title-anim">Latest Company Insights</h2>
</div>
<div className="col-12 col-lg-5">
<form action="#" method="post" autoComplete="off">
<div className="search-form">
<input
type="search"
name="search-post"
id="searchPost"
placeholder="Search"
required
/>
<button
type="submit"
aria-label="search post"
title="search post"
>
<i className="fa-solid fa-magnifying-glass"></i>
</button>
</div>
</form>
</div>
</div>
<PostFilterItems
currentPage={currentPage}
onPageChange={handlePageChange}
onTotalPagesChange={handleTotalPagesChange}
postsPerPage={postsPerPage}
/>
{totalPages > 1 && (
<div className="row mt-60">
<div className="col-12">
<div className="section__cta">
<ul className="pagination">
<li>
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
style={{ opacity: currentPage === 1 ? 0.5 : 1, cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
>
<i className="fa-solid fa-angle-left"></i>
</button>
</li>
{getPageNumbers().map((page, index) => (
<li key={index}>
{typeof page === 'number' ? (
<button
onClick={() => handlePageChange(page)}
className={currentPage === page ? 'active' : ''}
aria-label={`Go to page ${page}`}
aria-current={currentPage === page ? 'page' : undefined}
>
{page}
</button>
) : (
<span style={{ padding: '0 10px' }}>{page}</span>
)}
</li>
))}
<li>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Next page"
style={{ opacity: currentPage === totalPages ? 0.5 : 1, cursor: currentPage === totalPages ? 'not-allowed' : 'pointer' }}
>
<i className="fa-solid fa-angle-right"></i>
</button>
</li>
</ul>
</div>
</div>
</div>
)}
</div>
</section>
);
};
export default BlogItems;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const BlogScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default BlogScrollProgressButton;

View File

@@ -0,0 +1,272 @@
"use client";
import { useParams } from "next/navigation";
import Image from "next/legacy/image";
import Link from "next/link";
import { useBlogPost } from "@/lib/hooks/useBlog";
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
const BlogSingle = () => {
const params = useParams();
const slug = params?.slug as string;
const { post, loading, error } = useBlogPost(slug);
if (loading) {
return (
<section className="blog-single-section fix-top pb-120">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10 col-xl-8">
<div className="loading-state text-center py-5">
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="text-tertiary">Loading insight...</p>
</div>
</div>
</div>
</div>
</section>
);
}
if (error || !post) {
return (
<section className="blog-single-section fix-top pb-120">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10 col-xl-8">
<div className="error-state text-center py-5">
<div className="error-icon mb-4">
<i className="fa-solid fa-exclamation-circle fa-4x text-tertiary"></i>
</div>
<h2 className="text-secondary mb-3">Insight Not Found</h2>
<p className="text-tertiary mb-4">
The insight you're looking for doesn't exist or has been removed.
</p>
<Link href="/insights" className="btn btn-primary">
<i className="fa-solid fa-arrow-left me-2"></i>
Back to Insights
</Link>
</div>
</div>
</div>
</div>
</section>
);
}
return (
<section className="blog-single-section fix-top pb-120">
<div className="container">
{/* Article Content */}
<div className="row">
<div className="col-12">
<article className="blog-single-article">
{/* Article Header */}
<header className="article-header">
{/* Top Meta Bar */}
<div className="article-top-meta d-flex flex-wrap align-items-center justify-content-between mb-4">
<div className="left-meta d-flex align-items-center gap-3">
{/* Category Badge */}
{post.category && (
<Link href={`/insights?category=${post.category.slug}`} className="category-badge">
<i className="fa-solid fa-folder-open me-2"></i>
{post.category.title}
</Link>
)}
{/* Date */}
<div className="meta-item d-flex align-items-center">
<i className="fa-solid fa-calendar me-2"></i>
<span>
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</span>
</div>
</div>
<div className="right-meta d-flex align-items-center gap-3">
{/* Reading Time */}
<div className="meta-item d-flex align-items-center">
<i className="fa-solid fa-clock me-2"></i>
<span>{post.reading_time} min</span>
</div>
{/* Views */}
<div className="meta-item d-flex align-items-center">
<i className="fa-solid fa-eye me-2"></i>
<span>{post.views_count}</span>
</div>
</div>
</div>
{/* Title */}
<h1 className="article-title">
{post.title}
</h1>
{/* Author and Tags Bar */}
<div className="article-bottom-meta d-flex flex-wrap align-items-center justify-content-between mt-4 pt-4">
{/* Author */}
<div className="author-meta d-flex align-items-center">
<div className="author-avatar me-3">
{post.author?.avatar ? (
<Image
src={post.author.avatar}
alt={post.author.name}
width={48}
height={48}
className="rounded-circle"
/>
) : (
<div className="avatar-placeholder">
<i className="fa-solid fa-user"></i>
</div>
)}
</div>
<div className="author-info">
<div className="author-label">Written by</div>
<div className="author-name">
{post.author?.name || post.author_name || 'Admin'}
</div>
</div>
</div>
{/* Tags */}
{post.tags && post.tags.length > 0 && (
<div className="article-tags d-flex flex-wrap align-items-center gap-2">
{post.tags.map((tag) => (
<Link
key={tag.id}
href={`/insights?tag=${tag.slug}`}
className="tag-badge"
>
#{tag.name}
</Link>
))}
</div>
)}
</div>
</header>
{/* Featured Image */}
{(post.featured_image || post.thumbnail) && (
<div className="article-featured-image">
<div className="image-wrapper">
<Image
src={getValidImageUrl(
post.featured_image || post.thumbnail,
FALLBACK_IMAGES.BLOG
)}
alt={getValidImageAlt(post.title)}
width={1200}
height={600}
layout="responsive"
className="featured-image"
/>
</div>
</div>
)}
{/* Article Body */}
<div className="article-body">
{/* Excerpt */}
{post.excerpt && (
<div className="article-excerpt">
<p className="lead">{post.excerpt}</p>
</div>
)}
{/* Content */}
{post.content && (
<div
className="article-content enterprise-content"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
)}
</div>
{/* Footer Section */}
<footer className="article-footer">
{/* Share Section */}
<div className="article-share">
<div className="share-container">
<h6 className="share-title">
Share this insight
</h6>
<div className="social-share">
<Link
href={`https://www.linkedin.com/shareArticle?mini=true&url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&title=${encodeURIComponent(post.title)}`}
target="_blank"
rel="noopener noreferrer"
className="share-btn share-linkedin"
aria-label="Share on LinkedIn"
>
<i className="fa-brands fa-linkedin-in"></i>
<span>LinkedIn</span>
</Link>
<button
onClick={() => {
if (typeof window !== 'undefined' && navigator.clipboard) {
navigator.clipboard.writeText(window.location.href);
alert('Link copied to clipboard!');
}
}}
className="share-btn share-copy"
aria-label="Copy link"
>
<i className="fa-solid fa-link"></i>
<span>Copy</span>
</button>
</div>
</div>
</div>
{/* Author Bio */}
{post.author && post.author.bio && (
<div className="author-bio-section">
<div className="author-bio-card">
<div className="author-avatar-container">
{post.author.avatar ? (
<Image
src={post.author.avatar}
alt={post.author.name}
width={90}
height={90}
className="rounded-circle"
/>
) : (
<div className="author-avatar-large">
<i className="fa-solid fa-user"></i>
</div>
)}
</div>
<div className="author-bio-content">
<div className="bio-label">About the Author</div>
<h6 className="author-name">{post.author.name}</h6>
<p className="author-bio-text">{post.author.bio}</p>
</div>
</div>
</div>
)}
{/* Navigation */}
<div className="article-navigation">
<Link href="/insights" className="btn-back-insights">
<i className="fa-solid fa-arrow-left me-2"></i>
Back to All Insights
</Link>
</div>
</footer>
</article>
</div>
</div>
</div>
</section>
);
};
export default BlogSingle;

View File

@@ -0,0 +1,117 @@
"use client";
import Image from "next/legacy/image";
import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules";
import "swiper/swiper-bundle.css";
import { useLatestPosts } from "@/lib/hooks/useBlog";
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
const LatestPost = () => {
const { posts, loading, error } = useLatestPosts(8);
if (loading) {
return (
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
<div className="container">
<div className="row">
<div className="col-12">
<p className="text-center">Loading latest posts...</p>
</div>
</div>
</div>
</section>
);
}
if (error || posts.length === 0) {
return null; // Don't show the section if there's an error or no posts
}
return (
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
<div className="container">
<div className="row vertical-column-gap align-items-center">
<div className="col-12 col-lg-7">
<div className="tp-lp-title text-center text-lg-start">
<h2 className="mt-8 fw-7 text-secondary title-anim">
Related Insights
</h2>
</div>
</div>
<div className="col-12 col-lg-5">
<div className="tp-lp-cta text-center text-lg-end">
<Link href="/insights" className="btn-line text-uppercase">
View All Insights
</Link>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="tp-lp-slider-wrapper mt-60">
<div className="tp-lp-slider-wrapper">
<Swiper
slidesPerView={"auto"}
spaceBetween={24}
slidesPerGroup={1}
freeMode={true}
speed={1200}
loop={posts.length > 3}
roundLengths={true}
modules={[Autoplay]}
autoplay={{
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true,
}}
className="tp-lp-slider"
>
{posts.map((post) => (
<SwiperSlide key={post.id}>
<div className="tp-lp-slider__single topy-tilt">
<div className="thumb mb-24">
<Link href={`/insights/${post.slug}`} className="w-100 overflow-hidden">
<Image
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
width={400}
height={220}
className="w-100 mh-220"
alt={getValidImageAlt(post.title)}
/>
</Link>
</div>
<div className="content">
<div className="tp-lp-post__meta mb-24 mt-8">
<p className="author text-xs text-tertiary">
{post.author_name || 'Admin'}
</p>
<span></span>
<p className="date text-xs text-tertiary">
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</p>
</div>
<h5 className="mt-8 fw-5 text-secondary">
<Link href={`/insights/${post.slug}`}>
{post.title}
</Link>
</h5>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default LatestPost;

View File

@@ -0,0 +1,74 @@
import { useState, useEffect } from "react";
import { useBlogCategories } from "@/lib/hooks/useBlog";
const PostFilterButtons = ({ handleClick, active }: any) => {
const { categories: apiCategories, loading, error } = useBlogCategories();
const [categories, setCategories] = useState<any[]>([]);
useEffect(() => {
if (!loading && apiCategories.length > 0) {
// Add "All" category at the beginning
const allCategory = {
id: 0,
title: "All",
slug: "all",
display_order: 0
};
setCategories([allCategory, ...apiCategories]);
}
}, [apiCategories, loading]);
if (loading) {
return (
<div className="row">
<div className="col-12">
<div className="post-filter__wrapper mt-80">
<p>Loading categories...</p>
</div>
</div>
</div>
);
}
if (error) {
// Fallback to showing "All" button only
return (
<div className="row">
<div className="col-12">
<div className="post-filter__wrapper mt-80">
<button
aria-label="Filter Post"
className="active"
onClick={() => handleClick("all")}
>
All
</button>
</div>
</div>
</div>
);
}
return (
<div className="row">
<div className="col-12">
<div className="post-filter__wrapper mt-80">
{categories.map((item) => {
return (
<button
aria-label="Filter Post"
key={item.id}
className={active === item.slug ? " active" : ""}
onClick={() => handleClick(item.slug)}
>
{item.title}
</button>
);
})}
</div>
</div>
</div>
);
};
export default PostFilterButtons;

View File

@@ -0,0 +1,139 @@
"use client";
import { useState, SetStateAction, useEffect } from "react";
import Image from "next/legacy/image";
import Link from "next/link";
import { AnimatePresence, motion } from "framer-motion";
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
import PostFilterButtons from "./PostFilterButtons";
import { useBlogPosts } from "@/lib/hooks/useBlog";
interface PostFilterItemsProps {
currentPage: number;
onPageChange: (page: number) => void;
onTotalPagesChange: (totalPages: number) => void;
postsPerPage?: number;
}
const PostFilterItems = ({ currentPage, onPageChange, onTotalPagesChange, postsPerPage = 10 }: PostFilterItemsProps) => {
const [active, setActive] = useState("all");
const { posts: allPosts, loading, error, totalCount } = useBlogPosts({
category: active === "all" ? undefined : active,
page: currentPage,
page_size: postsPerPage
});
const [displayData, setDisplayData] = useState<any[]>([]);
useEffect(() => {
if (!loading && allPosts.length > 0) {
setDisplayData(allPosts);
}
}, [allPosts, loading]);
// Calculate and update total pages when totalCount changes
useEffect(() => {
if (totalCount !== undefined && totalCount !== null) {
const calculatedTotalPages = Math.ceil(totalCount / postsPerPage);
onTotalPagesChange(calculatedTotalPages);
}
}, [totalCount, postsPerPage, onTotalPagesChange]);
const handleCategoryClick = (category: SetStateAction<string>) => {
if (category === active) return;
setActive(category);
setDisplayData([]);
onPageChange(1); // Reset to page 1 when category changes
// The API call will be triggered by the change in active state
// which will update allPosts and trigger the useEffect above
};
if (loading && displayData.length === 0) {
return (
<>
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
<div className="row mt-60">
<div className="col-12">
<p className="text-center">Loading posts...</p>
</div>
</div>
</>
);
}
if (error) {
return (
<>
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
<div className="row mt-60">
<div className="col-12">
<p className="text-center text-danger">Error loading posts. Please try again later.</p>
</div>
</div>
</>
);
}
return (
<>
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
<motion.div className="row mt-60 masonry-grid" layout>
<AnimatePresence>
{displayData.map((item) => {
return (
<motion.div
className="col-12 col-lg-6 grid-item-main"
key={item.id}
layout
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.6 }}
transition={{ duration: 0.6 }}
>
<div className="tp-lp-slider__single topy-tilt">
<div className="thumb mb-24">
<Link
href={`/insights/${item.slug}`}
className="w-100 overflow-hidden d-block"
>
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={getValidImageUrl(item.thumbnail, FALLBACK_IMAGES.BLOG)}
className="w-100 mh-220 parallax-image"
alt={getValidImageAlt(item.title)}
width={400}
height={220}
/>
</div>
</div>
</Link>
</div>
<div className="content">
<div className="tp-lp-post__meta mb-24 mt-8">
<p className="author text-xs text-tertiary">
{item.author_name || 'Admin'}
</p>
<span></span>
<p className="date text-xs text-tertiary">
{new Date(item.published_at || item.created_at).toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</p>
</div>
<h5 className="mt-8 fw-5 text-secondary">
<Link href={`/insights/${item.slug}`}>{item.title}</Link>
</h5>
</div>
</div>
</motion.div>
);
})}
</AnimatePresence>
</motion.div>
</>
);
};
export default PostFilterItems;

View File

@@ -0,0 +1,144 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Link from "next/link";
const CareerBanner = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
if (document.querySelector(".career-banner")) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".career-banner",
start: "center center",
end: "+=100%",
scrub: true,
pin: false,
},
});
tl.to(".cp-banner-thumb", {
opacity: 0.1,
y: "40%",
duration: 2,
});
tl.to(
".career-banner",
{
"--scale": 3,
duration: 2,
},
0
);
}
}, []);
return (
<section className="career-banner fix-top bg-black position-relative overflow-hidden" style={{ minHeight: '50vh', paddingTop: '100px', paddingBottom: '60px' }}>
<div className="container">
<div className="row justify-content-center align-items-center">
<div className="col-12 col-lg-10">
<div className="cp-banner__content text-center">
<h2 className="mt-8 fw-7 text-xxl title-anim text-white mb-5">
Build Your Career with Enterprise Excellence
</h2>
<p className="text-quinary fs-5 mb-5" style={{ maxWidth: '700px', margin: '0 auto 2rem' }}>
Join our global team of innovators, problem-solvers, and tech leaders building the future of digital solutions
</p>
</div>
</div>
</div>
{/* Job Categories with Icons */}
<div className="row justify-content-center mt-5 pt-4">
<div className="col-6 col-md-3 text-center mb-4">
<div className="job-category-card" style={{
padding: '2rem 1rem',
background: 'rgba(255,255,255,0.05)',
borderRadius: '12px',
border: '1px solid rgba(255,255,255,0.1)',
transition: 'all 0.3s ease'
}}>
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#00d4ff' }}>
<i className="fa-solid fa-code"></i>
</div>
<h5 className="text-white fw-6 mb-2">Engineering</h5>
</div>
</div>
<div className="col-6 col-md-3 text-center mb-4">
<div className="job-category-card" style={{
padding: '2rem 1rem',
background: 'rgba(255,255,255,0.05)',
borderRadius: '12px',
border: '1px solid rgba(255,255,255,0.1)',
transition: 'all 0.3s ease'
}}>
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#ff6b9d' }}>
<i className="fa-solid fa-palette"></i>
</div>
<h5 className="text-white fw-6 mb-2">Design</h5>
</div>
</div>
<div className="col-6 col-md-3 text-center mb-4">
<div className="job-category-card" style={{
padding: '2rem 1rem',
background: 'rgba(255,255,255,0.05)',
borderRadius: '12px',
border: '1px solid rgba(255,255,255,0.1)',
transition: 'all 0.3s ease'
}}>
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#ffd93d' }}>
<i className="fa-solid fa-chart-line"></i>
</div>
<h5 className="text-white fw-6 mb-2">Business</h5>
</div>
</div>
<div className="col-6 col-md-3 text-center mb-4">
<div className="job-category-card" style={{
padding: '2rem 1rem',
background: 'rgba(255,255,255,0.05)',
borderRadius: '12px',
border: '1px solid rgba(255,255,255,0.1)',
transition: 'all 0.3s ease'
}}>
<div className="icon-wrapper mb-3" style={{ fontSize: '3rem', color: '#a78bfa' }}>
<i className="fa-solid fa-users"></i>
</div>
<h5 className="text-white fw-6 mb-2">Operations</h5>
</div>
</div>
</div>
</div>
<ul className="social">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
target="_blank"
aria-label="connect with us on linkedin"
>
<i className="fa-brands fa-linkedin-in"></i>
</Link>
</li>
<li>
<Link
href="https://github.com/gnxtech"
target="_blank"
aria-label="view our code on github"
>
<i className="fa-brands fa-github"></i>
</Link>
</li>
</ul>
<Link href="#scroll-to" className="scroll-to">
Scroll
<span className="arrow"></span>
</Link>
<span className="sm-c"></span>
<span className="sm-c sm-cl"></span>
</section>
);
};
export default CareerBanner;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const CareerInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default CareerInitAnimations;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const CareerScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default CareerScrollProgressButton;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,681 @@
"use client";
import { useState, useEffect } from "react";
import { JobPosition } from "@/lib/api/careerService";
import JobApplicationForm from "./JobApplicationForm";
interface JobSingleProps {
job: JobPosition;
}
const JobSingle = ({ job }: JobSingleProps) => {
const [showApplicationForm, setShowApplicationForm] = useState(false);
// Prevent body scroll when modal is open
useEffect(() => {
if (showApplicationForm) {
// Get scrollbar width to prevent layout shift
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
// Save current scroll position
const scrollY = window.scrollY;
// Prevent background scroll
document.body.style.position = 'fixed';
document.body.style.top = `-${scrollY}px`;
document.body.style.left = '0';
document.body.style.right = '0';
document.body.style.overflow = 'hidden';
if (scrollbarWidth > 0) {
document.body.style.paddingRight = `${scrollbarWidth}px`;
}
} else {
// Get the scroll position from body top
const scrollY = parseInt(document.body.style.top || '0') * -1;
// Restore scroll
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
// Restore scroll position
window.scrollTo(0, scrollY);
}
// Cleanup on unmount
return () => {
const scrollY = parseInt(document.body.style.top || '0') * -1;
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
if (scrollY > 0) {
window.scrollTo(0, scrollY);
}
};
}, [showApplicationForm]);
const formatSalary = () => {
if (job.salary_min && job.salary_max) {
return `${job.salary_currency} ${job.salary_min}-${job.salary_max} ${job.salary_period}`;
} else if (job.salary_min) {
return `From ${job.salary_currency} ${job.salary_min} ${job.salary_period}`;
} else if (job.salary_max) {
return `Up to ${job.salary_currency} ${job.salary_max} ${job.salary_period}`;
}
return "Competitive";
};
const scrollToForm = () => {
setShowApplicationForm(true);
setTimeout(() => {
const formElement = document.getElementById('application-form');
if (formElement) {
formElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
};
return (
<>
{/* Job Header Banner */}
<section className="job-header pt-80 pt-md-100 pt-lg-120 pb-60 pb-md-70 pb-lg-80" style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
position: 'relative'
}}>
<div className="container">
<div className="row">
<div className="col-12">
<div className="job-header-content" style={{ color: '#ffffff' }}>
<div className="mb-12 mb-md-16">
<span className="badge" style={{
backgroundColor: 'rgba(255,255,255,0.2)',
color: '#ffffff',
padding: '6px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: '500',
textTransform: 'none',
letterSpacing: '1px',
display: 'inline-block'
}}>
{job.department || 'Career Opportunity'}
</span>
</div>
<h1 className="fw-7 mb-16 mb-md-20 mb-lg-24" style={{
fontSize: 'clamp(1.75rem, 5vw, 3.5rem)',
lineHeight: '1.2',
color: '#ffffff'
}}>
{job.title}
</h1>
<div className="job-meta d-flex flex-wrap" style={{
fontSize: 'clamp(13px, 2vw, 16px)',
gap: 'clamp(12px, 2vw, 16px)'
}}>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>location_on</span>
<span>{job.location}</span>
</div>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>work</span>
<span className="d-none d-sm-inline">{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
<span className="d-sm-none">
{job.employment_type.split('-')[0].charAt(0).toUpperCase() + job.employment_type.split('-')[0].slice(1)}
</span>
</div>
<div className="meta-item d-flex align-items-center">
<span className="material-symbols-outlined me-1 me-md-2" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>group</span>
<span className="d-none d-sm-inline">{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'}</span>
<span className="d-sm-none">{job.open_positions} {job.open_positions === 1 ? 'Pos' : 'Pos'}</span>
</div>
{job.experience_required && (
<div className="meta-item d-flex align-items-center d-none d-md-flex">
<span className="material-symbols-outlined me-2">school</span>
<span>{job.experience_required}</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</section>
{/* Job Content Section */}
<section className="job-single pb-80 pb-md-100 pb-lg-120 sticky-wrapper" style={{ marginTop: 'clamp(-30px, -5vw, -40px)' }}>
<div className="container">
<div className="row vertical-column-gap">
<div className="col-12 col-lg-8 mb-4 mb-lg-0">
<div className="j-d-content" style={{
backgroundColor: 'white',
borderRadius: 'clamp(8px, 2vw, 12px)',
padding: 'clamp(20px, 4vw, 40px)',
boxShadow: '0 10px 40px rgba(0,0,0,0.08)'
}}>
<div className="intro" style={{
borderBottom: '2px solid #f0f0f0',
paddingBottom: 'clamp(20px, 3vw, 30px)',
marginBottom: 'clamp(20px, 3vw, 30px)'
}}>
<h3 className="fw-6 mb-12 mb-md-16 text-secondary" style={{ fontSize: 'clamp(18px, 3vw, 24px)' }}>
About This Position
</h3>
{job.short_description && (
<p style={{
color: '#666',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
{job.short_description}
</p>
)}
</div>
{job.about_role && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(22px, 4vw, 28px)'
}}>info</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
About This Role
</h4>
</div>
<p style={{
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>{job.about_role}</p>
</div>
)}
{job.requirements && job.requirements.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(22px, 4vw, 28px)'
}}>task_alt</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Requirements
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.requirements.map((req, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{req}
</li>
))}
</ul>
</div>
)}
{job.responsibilities && job.responsibilities.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>assignment</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Key Responsibilities
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.responsibilities.map((resp, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{resp}
</li>
))}
</ul>
</div>
)}
{job.qualifications && job.qualifications.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>workspace_premium</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Qualifications
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.qualifications.map((qual, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{qual}
</li>
))}
</ul>
</div>
)}
{job.bonus_points && job.bonus_points.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>stars</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
Nice to Have
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.bonus_points.map((bonus, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{bonus}
</li>
))}
</ul>
</div>
)}
{job.benefits && job.benefits.length > 0 && (
<div className="group mb-32 mb-md-40">
<div className="d-flex align-items-center mb-16 mb-md-20">
<span className="material-symbols-outlined me-2" style={{ color: '#667eea', fontSize: 'clamp(22px, 4vw, 28px)' }}>card_giftcard</span>
<h4 className="mt-8 text-secondary fw-6 mb-0" style={{ fontSize: 'clamp(16px, 3vw, 20px)' }}>
What We Offer
</h4>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{job.benefits.map((benefit, index) => (
<li key={index} className="mb-2" style={{
paddingLeft: 'clamp(20px, 4vw, 30px)',
position: 'relative',
color: '#555',
lineHeight: '1.8',
fontSize: 'clamp(14px, 2vw, 16px)'
}}>
<span style={{
position: 'absolute',
left: '0',
top: 'clamp(6px, 1.5vw, 8px)',
width: 'clamp(5px, 1vw, 6px)',
height: 'clamp(5px, 1vw, 6px)',
backgroundColor: '#667eea',
borderRadius: '50%'
}}></span>
{benefit}
</li>
))}
</ul>
</div>
)}
</div>
</div>
<div className="col-12 col-lg-4">
<div className="j-d-sidebar" style={{
backgroundColor: 'white',
borderRadius: 'clamp(8px, 2vw, 12px)',
padding: 'clamp(20px, 4vw, 30px)',
boxShadow: '0 10px 40px rgba(0,0,0,0.08)',
position: 'sticky',
top: '20px'
}}>
<div className="intro mb-20 mb-md-30" style={{
borderBottom: '2px solid #f0f0f0',
paddingBottom: 'clamp(16px, 3vw, 20px)'
}}>
<span className="text-uppercase" style={{
color: '#667eea',
fontSize: 'clamp(11px, 2vw, 12px)',
fontWeight: '600',
letterSpacing: '2px'
}}>
JOB DETAILS
</span>
</div>
<div className="content">
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>payments</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Salary Range</p>
</div>
<p className="fw-5 mb-0" style={{
color: '#667eea',
fontSize: 'clamp(16px, 3vw, 18px)'
}}>
{formatSalary()}
</p>
{job.salary_additional && (
<p className="mt-6 mt-md-8" style={{
color: '#666',
fontSize: 'clamp(12px, 2vw, 14px)'
}}>
{job.salary_additional}
</p>
)}
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>work</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Employment Type</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>
{job.employment_type.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>location_on</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Location</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>{job.location}</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>event</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Start Date</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>{job.start_date}</p>
</div>
<div className="detail-item mb-16 mb-md-24">
<div className="d-flex align-items-center mb-6 mb-md-8">
<span className="material-symbols-outlined me-2" style={{
color: '#667eea',
fontSize: 'clamp(18px, 3vw, 20px)'
}}>groups</span>
<p className="fw-6 mb-0" style={{
color: '#333',
fontSize: 'clamp(14px, 2vw, 15px)'
}}>Openings</p>
</div>
<p style={{
color: '#666',
fontSize: 'clamp(13px, 2vw, 14px)'
}}>
{job.open_positions} {job.open_positions === 1 ? 'Position' : 'Positions'} Available
</p>
</div>
</div>
<div className="cta mt-20 mt-md-30">
<button
onClick={scrollToForm}
className="btn w-100 apply-btn"
style={{
backgroundColor: 'white',
color: '#333',
border: '2px solid #667eea',
padding: 'clamp(12px, 2vw, 15px) clamp(20px, 4vw, 30px)',
fontSize: 'clamp(14px, 2vw, 16px)',
fontWeight: '600',
borderRadius: 'clamp(6px, 1.5vw, 8px)',
transition: 'all 0.3s ease',
cursor: 'pointer'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#FFD700';
e.currentTarget.style.borderColor = '#FFD700';
e.currentTarget.style.color = '#333';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'white';
e.currentTarget.style.borderColor = '#667eea';
e.currentTarget.style.color = '#333';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<span className="d-none d-sm-inline">Apply for This Position</span>
<span className="d-sm-none">Apply Now</span>
</button>
<p className="text-center mt-12 mt-md-16" style={{
color: '#999',
fontSize: 'clamp(11px, 2vw, 13px)'
}}>
<span className="d-none d-sm-inline">Application takes ~5 minutes</span>
<span className="d-sm-none">~5 min</span>
</p>
<a
href="/career"
className="btn w-100 mt-12 mt-md-16"
style={{
backgroundColor: 'transparent',
color: '#667eea',
border: '2px solid #e0e0e0',
padding: 'clamp(10px, 2vw, 12px) clamp(20px, 4vw, 30px)',
fontSize: 'clamp(13px, 2vw, 14px)',
fontWeight: '500',
borderRadius: 'clamp(6px, 1.5vw, 8px)',
transition: 'all 0.3s ease',
cursor: 'pointer',
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 'clamp(6px, 1.5vw, 8px)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f5f5f5';
e.currentTarget.style.borderColor = '#667eea';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.borderColor = '#e0e0e0';
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span>
<span className="d-none d-sm-inline">Back to Career Page</span>
<span className="d-sm-none">Back</span>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Application Form Modal/Popup */}
{showApplicationForm && (
<>
{/* Backdrop Overlay */}
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
zIndex: 9998,
animation: 'fadeIn 0.3s ease-in-out'
}}
onClick={() => setShowApplicationForm(false)}
aria-hidden="true"
/>
{/* Modal Container */}
<div
role="dialog"
aria-modal="true"
aria-labelledby="application-form-title"
tabIndex={-1}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 'clamp(10px, 2vw, 20px)',
overflow: 'hidden',
animation: 'fadeIn 0.3s ease-in-out'
}}
onClick={(e) => {
// Close when clicking the container background
if (e.target === e.currentTarget) {
setShowApplicationForm(false);
}
}}
onKeyDown={(e) => {
// Close on ESC key
if (e.key === 'Escape') {
setShowApplicationForm(false);
}
}}
ref={(el) => {
if (el) {
setTimeout(() => el.focus(), 100);
}
}}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '16px',
width: '100%',
maxWidth: '900px',
maxHeight: '90vh',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
animation: 'slideUp 0.3s ease-out',
outline: 'none',
overflow: 'hidden',
touchAction: 'none'
}}
onClick={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<JobApplicationForm job={job} onClose={() => setShowApplicationForm(false)} />
</div>
</div>
{/* Animation Styles */}
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</>
)}
</>
);
};
export default JobSingle;

View File

@@ -0,0 +1,121 @@
"use client";
import Link from "next/link";
import { useJobs } from "@/lib/hooks/useCareer";
const OpenPosition = () => {
const { jobs, loading, error } = useJobs();
if (loading) {
return (
<section className="op-position pt-120 pb-120" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="intro mb-60">
<h2 className="mt-8 fw-7 title-anim text-secondary">
Open Positions
</h2>
</div>
</div>
<div className="col-12 mt-60">
<p className="text-center">Loading positions...</p>
</div>
</div>
</div>
</section>
);
}
if (error) {
return (
<section className="op-position pt-120 pb-120" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="intro mb-60">
<h2 className="mt-8 fw-7 title-anim text-secondary">
Open Positions
</h2>
</div>
</div>
<div className="col-12 mt-60">
<p className="text-center text-danger">Error loading positions. Please try again later.</p>
</div>
</div>
</div>
</section>
);
}
if (jobs.length === 0) {
return (
<section className="op-position pt-120 pb-120" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="intro mb-60">
<h2 className="mt-8 fw-7 title-anim text-secondary">
Open Positions
</h2>
</div>
</div>
<div className="col-12 mt-60">
<p className="text-center">No open positions at the moment. Please check back later.</p>
</div>
</div>
</div>
</section>
);
}
return (
<section className="op-position pt-120 pb-120" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="intro mb-60">
<h2 className="mt-8 fw-7 title-anim text-secondary">
Open Positions
</h2>
</div>
</div>
<div className="col-12 mt-60">
{jobs.map((job, index) => (
<div key={job.id} className="op-position-single appear-down">
<div className="row vertical-column-gap align-items-center">
<div className="col-12 col-sm-2">
<span className="fw-7 text-xl text-tertiary">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="col-12 col-sm-5">
<h4 className="fw-7">
<Link href={`/career/${job.slug}`}>{job.title}</Link>
</h4>
</div>
<div className="col-12 col-sm-3">
<div className="roles">
<span className="text-tertiary fw-5 text-xl">
({job.open_positions.toString().padStart(2, '0')} Open {job.open_positions === 1 ? 'Role' : 'Roles'})
</span>
</div>
</div>
<div className="col-12 col-sm-2">
<div className="cta text-start text-sm-end">
<Link href={`/career/${job.slug}`}>
<span className="material-symbols-outlined">east</span>
</Link>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
};
export default OpenPosition;

View File

@@ -0,0 +1,85 @@
import Image from "next/legacy/image";
import time from "@/public/images/time.png";
import trans from "@/public/images/trans.png";
import support from "@/public/images/support.png";
import skill from "@/public/images/skill.png";
const Thrive = () => {
return (
<section className="thrive pt-120 pb-120 bg-black fade-wrapper">
<div className="container">
<div className="row">
<div className="col-12">
<div className="intro">
<h2 className="mt-8 fw-7 title-anim text-white">
What lets us thrive together
</h2>
</div>
</div>
</div>
<div className="row vertical-column-gap-lg mt-60">
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={time} alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
Flexible workday
</h4>
<p className="cur-lg text-quinary">
We understand that productivity thrives in balance. Our flexible work arrangements
empower you to structure your day for optimal performance while maintaining work-life
harmony. Whether remote, hybrid, or on-site, we trust our teams to deliver excellence.
</p>
</div>
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={trans} alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
Transparency
</h4>
<p className="cur-lg text-quinary">
Open communication is the foundation of our culture. We believe in clear goal-setting,
honest feedback, and accessible leadership. Every team member has visibility into
company objectives, strategic decisions, and how their contributions drive enterprise success.
</p>
</div>
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={support} alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
<p className="cur-lg text-quinary">
Your success is our priority. From comprehensive onboarding to continuous mentorship,
we provide enterprise-grade resources, cutting-edge tools, and dedicated support systems.
Our collaborative environment ensures you&apos;re never alone in tackling complex challenges.
</p>
</div>
</div>
<div className="col-12 col-md-6 fade-top">
<div className="thumb">
<Image src={skill} alt="Image" width={80} height={80} />
</div>
<div className="content mt-40">
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
Growth Skill
</h4>
<p className="cur-lg text-quinary">
Invest in your future with our comprehensive professional development programs. Access
industry certifications, advanced training workshops, and leadership development initiatives.
We champion continuous learning and provide clear pathways for career advancement.
</p>
</div>
</div>
</div>
</div>
</section>
);
};
export default Thrive;

View File

@@ -0,0 +1,185 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/legacy/image";
import Link from "next/link";
import { useCaseStudies, useClients } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
const CaseItems = () => {
const [activeTabIndex, setActiveTabIndex] = useState(0);
const { caseStudies, loading: casesLoading } = useCaseStudies();
const { clients, loading: clientsLoading } = useClients();
const handleTabClick = (index: number) => {
setActiveTabIndex(index);
};
// Filter case studies by category
const caseStudiesData = caseStudies.filter((cs) => !cs.client);
const clientCaseStudies = caseStudies.filter((cs) => cs.client);
if (casesLoading || clientsLoading) {
return (
<section className="fix-top pb-120 c-study">
<div className="container">
<div className="row">
<div className="col-12">
<p className="text-center">Loading case studies...</p>
</div>
</div>
</div>
</section>
);
}
return (
<section className="fix-top pb-120 c-study">
<div className="container">
<div className="row">
<div className="col-12">
<div className="c-study-banner pb-120">
<div className="row">
<div className="col-12 col-lg-9">
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
Case Studies
</h2>
<p className="cur-lg">
Discover how we help enterprises solve complex challenges with
secure, scalable solutions. Our case studies highlight real
business outcomes accelerated delivery, reduced costs,
improved reliability, and data-driven growth powered by modern
cloud, AI, and platform engineering.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="c-study-inner pt-120 mb-60">
<div className="c-study-btns">
<button
className={`study-btn ${
activeTabIndex === 0 ? "study-btn-active" : ""
}`}
onClick={() => handleTabClick(0)}
>
Case Study
</button>
<span></span>
<button
className={`study-btn ${
activeTabIndex === 1 ? "study-btn-active" : ""
}`}
onClick={() => handleTabClick(1)}
>
Client
</button>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="c-content-wrapper mt-60">
<div
className={`c-tab-single ${
activeTabIndex === 0 ? "active-tab" : ""
}`}
>
<div className="row vertical-column-gap-lg">
{caseStudiesData.map((caseStudy) => (
<div key={caseStudy.id} className="col-12 col-lg-6">
<div className="c-study-single">
<div className="thumb mb-24">
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
<Image
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
className="w-100 mh-300"
alt={caseStudy.title}
width={600}
height={400}
/>
</Link>
</div>
<div className="content">
<Link href={`/case-study/${caseStudy.slug}`} className="mb-30 fw-6">
{caseStudy.category_name || 'Case Study'}
</Link>
<h4 className="fw-6 mt-8 text-secondary">
<Link href={`/case-study/${caseStudy.slug}`}>
{caseStudy.title}
</Link>
</h4>
</div>
</div>
</div>
))}
{caseStudiesData.length === 0 && (
<div className="col-12">
<p className="text-center">No case studies found.</p>
</div>
)}
</div>
</div>
<div
className={`c-tab-single ${
activeTabIndex === 1 ? "active-tab" : ""
}`}
>
<div className="row vertical-column-gap-lg">
{clientCaseStudies.map((caseStudy, index) => (
<div key={caseStudy.id} className="col-12">
<div className={`row vertical-column-gap-md align-items-center ${index % 2 === 1 ? 'flex-row-reverse' : ''}`}>
<div className="col-12 col-lg-6">
<div className="c-tab__client">
<h2 className="mt-8 fw-7 title-anim text-secondary mb-24">
{caseStudy.client?.name || caseStudy.title}
</h2>
<p className="cur-lg">
{caseStudy.excerpt || caseStudy.client?.description}
</p>
<div className="mt-40">
<Link
href={`/case-study/${caseStudy.slug}`}
className="btn-anim btn-anim-light"
>
Read More
<i className="fa-solid fa-arrow-trend-up"></i>
<span></span>
</Link>
</div>
</div>
</div>
<div className={`col-12 col-lg-6 col-xxl-5 ${index % 2 === 0 ? 'offset-xxl-1' : ''}`}>
<div className="c-tab__thumb">
<Image
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
className="w-100 mh-300"
alt={caseStudy.client?.name || caseStudy.title}
width={600}
height={400}
/>
</div>
</div>
</div>
</div>
))}
{clientCaseStudies.length === 0 && (
<div className="col-12">
<p className="text-center">No client case studies found.</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default CaseItems;

View File

@@ -0,0 +1,143 @@
"use client";
import { use } from 'react';
import Image from "next/legacy/image";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import poster from "@/public/images/case/poster.png";
import project from "@/public/images/case/project.png";
import nine from "@/public/images/case/nine.png";
interface CaseSingleProps {
slug: string;
}
const CaseSingle = ({ slug }: CaseSingleProps) => {
const { caseStudy, loading, error } = useCaseStudy(slug);
if (loading) {
return (
<section className="c-details fix-top pb-120">
<div className="container">
<div className="row">
<div className="col-12">
<p className="text-center">Loading case study...</p>
</div>
</div>
</div>
</section>
);
}
if (error || !caseStudy) {
return (
<section className="c-details fix-top pb-120">
<div className="container">
<div className="row">
<div className="col-12">
<p className="text-center text-danger">Case study not found.</p>
</div>
</div>
</div>
</section>
);
}
return (
<section className="c-details fix-top pb-120">
<div className="container">
<div className="row">
<div className="col-12">
<div className="c-details-intro">
<h2 className="mt-8 text-secondary title-anim fw-7">
{caseStudy.title}
</h2>
{caseStudy.subtitle && (
<h4 className="mt-4 text-secondary">{caseStudy.subtitle}</h4>
)}
<div className="poster mt-60 fade-top">
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={caseStudy.poster_image ? getImageUrl(caseStudy.poster_image) : poster}
className="w-100 parallax-image mh-300"
alt={caseStudy.title}
width={1200}
height={600}
/>
</div>
</div>
</div>
<div className="row vertical-column-gap align-items-center pt-120 pb-120">
<div className="col-12 col-lg-6">
<div className="c-details-content">
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
Project
</h2>
{caseStudy.project_overview ? (
<p className="cur-lg">{caseStudy.project_overview}</p>
) : (
<div
className="cur-lg"
dangerouslySetInnerHTML={{ __html: caseStudy.description || '' }}
/>
)}
</div>
</div>
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1 fade-wrapper">
<div className="c-details-thumb fade-top">
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={caseStudy.project_image ? getImageUrl(caseStudy.project_image) : project}
className="w-100 parallax-image mh-260"
alt={`${caseStudy.title} - Project`}
width={600}
height={500}
/>
</div>
</div>
</div>
</div>
</div>
{caseStudy.site_map_content && (
<div className="row">
<div className="col-12">
<div className="road-map__content">
<h2 className="mt-8 fw-7 text-secondary title-anim mb-24">
Site Map
</h2>
<p className="cur-lg">{caseStudy.site_map_content}</p>
</div>
</div>
</div>
)}
{caseStudy.gallery_images && caseStudy.gallery_images.length > 0 && (
<div className="row vertical-column-gap mt-60 fade-wrapper">
{caseStudy.gallery_images.map((image) => (
<div key={image.id} className="col-12 col-sm-6 col-xl-3">
<div className="c-details-thumb fade-top">
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={getImageUrl(image.image) || nine}
className="w-100 mh-300 parallax-image"
alt={image.caption || caseStudy.title}
width={300}
height={300}
/>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</section>
);
};
export default CaseSingle;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const CaseStudyInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default CaseStudyInitAnimations;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const CaseStudyScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default CaseStudyScrollProgressButton;

View File

@@ -0,0 +1,52 @@
"use client";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
interface ProcessProps {
slug: string;
}
const Process = ({ slug }: ProcessProps) => {
const { caseStudy, loading } = useCaseStudy(slug);
if (loading || !caseStudy || !caseStudy.process_steps || caseStudy.process_steps.length === 0) {
return null;
}
return (
<section className="pt-120 pb-120 tp-process bg-black sticky-wrapper">
<div className="container">
<div className="row vertical-column-gap">
<div className="col-12 col-lg-6">
<div className="process__content sticky-item">
<h2 className="mt-8 title-anim text-white fw-7 mb-24">
{caseStudy.title} Process
</h2>
<p className="cur-lg text-quinary">
{caseStudy.excerpt}
</p>
</div>
</div>
<div className="col-12 col-lg-6 col-xxl-5 offset-xxl-1">
<div className="process__thumb sticky-item">
{caseStudy.process_steps.map((step) => (
<div key={step.id} className="process__single">
<span className="op-text text-white mb-40 cur-lg">
{String(step.step_number).padStart(2, '0')}
</span>
<h5 className="mt-8 text-white mb-24 title-anim">
{step.title}
</h5>
<p className="cur-lg text-quinary">
{step.description}
</p>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
export default Process;

View File

@@ -0,0 +1,67 @@
"use client";
import Image from "next/legacy/image";
import Link from "next/link";
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
import { getImageUrl } from "@/lib/imageUtils";
import one from "@/public/images/case/one.png";
interface RelatedCaseProps {
slug: string;
}
const RelatedCase = ({ slug }: RelatedCaseProps) => {
const { caseStudy, loading } = useCaseStudy(slug);
if (loading || !caseStudy || !caseStudy.related_case_studies || caseStudy.related_case_studies.length === 0) {
return null;
}
return (
<section className="pt-120 pb-120 c-study fade-wrapper">
<div className="container">
<div className="row">
<div className="col-12 col-lg-9">
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
Similar Case Studies
</h2>
</div>
</div>
<div className="row vertical-column-gap-lg">
{caseStudy.related_case_studies.slice(0, 2).map((relatedCase) => (
<div key={relatedCase.id} className="col-12 col-lg-6">
<div className="c-study-single fade-top">
<div className="thumb mb-24">
<Link href={`/case-study/${relatedCase.slug}`} className="w-100">
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
className="w-100 mh-300 parallax-image"
alt={relatedCase.title}
width={600}
height={400}
/>
</div>
</div>
</Link>
</div>
<div className="content">
<Link href={`/case-study/${relatedCase.slug}`} className="mb-30 fw-6">
{relatedCase.category_name || 'Case Study'}
</Link>
<h4 className="fw-6 mt-8 text-secondary">
<Link href={`/case-study/${relatedCase.slug}`}>
{relatedCase.title}
</Link>
</h4>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default RelatedCase;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const ContactInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default ContactInitAnimations;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const ContactScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default ContactScrollProgressButton;

View File

@@ -0,0 +1,602 @@
"use client";
import { usePathname } from "next/navigation";
import { useState } from "react";
import Image from "next/legacy/image";
import thumb from "@/public/images/contact-thumb.png";
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
const ContactSection = () => {
const pathname = usePathname();
const isServiceSingle = pathname === "/service-single" || pathname === "/about-us";
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
company: '',
jobTitle: '',
industry: '',
companySize: '',
budget: '',
timeline: '',
projectType: '',
message: '',
newsletter: false,
privacy: false
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState<{
type: 'success' | 'error' | null;
message: string;
}>({ type: null, message: '' });
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({ ...prev, [name]: checked }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitStatus({ type: null, message: '' });
try {
// Transform form data to match API requirements
const apiData: ContactFormData = {
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
phone: formData.phone || undefined,
company: formData.company,
job_title: formData.jobTitle,
industry: formData.industry || undefined,
company_size: formData.companySize || undefined,
project_type: formData.projectType || undefined,
timeline: formData.timeline || undefined,
budget: formData.budget || undefined,
message: formData.message,
newsletter_subscription: formData.newsletter,
privacy_consent: formData.privacy
};
// Submit to Django API
const response = await contactApiService.submitContactForm(apiData);
setSubmitStatus({
type: 'success',
message: response.message || 'Thank you! We\'ll contact you within 24 hours.'
});
// Reset form on success
setFormData({
firstName: '',
lastName: '',
email: '',
phone: '',
company: '',
jobTitle: '',
industry: '',
companySize: '',
budget: '',
timeline: '',
projectType: '',
message: '',
newsletter: false,
privacy: false
});
} catch (error) {
setSubmitStatus({
type: 'error',
message: error instanceof Error ? error.message : 'Failed to submit form. Please try again.'
});
} finally {
setIsSubmitting(false);
}
};
return (
<section
className={
"tp-contact pb-120 fade-wrapper" +
(isServiceSingle ? " pt-120" : " fix-top")
}
>
<div className="container">
{/* Contact Information Cards */}
<div className="row mb-40">
<div className="col-12 col-md-4">
<div className="contact-info-card">
<div className="contact-info-icon">
<i className="fa-solid fa-phone"></i>
</div>
<h4>Phone Support</h4>
<p>Main Contact & Emergency</p>
<a href="tel:+359897338147">+359 897 338 147</a>
<a href="tel:+359896138030">Emergency: +359 896 13 80 30</a>
<span className="contact-hours">Available 24/7</span>
</div>
</div>
<div className="col-12 col-md-4">
<div className="contact-info-card">
<div className="contact-info-icon">
<i className="fa-solid fa-envelope"></i>
</div>
<h4>Email Support</h4>
<p>Software Solutions</p>
<a href="mailto:info@gnxsoft.com">info@gnxsoft.com</a>
<a href="mailto:contact@gnxsoft.com">contact@gnxsoft.com</a>
<span className="contact-hours">24/7 Response Time</span>
</div>
</div>
<div className="col-12 col-md-4">
<div className="contact-info-card">
<div className="contact-info-icon">
<i className="fa-solid fa-map-marker-alt"></i>
</div>
<h4>Office Locations</h4>
<p>Our Location</p>
<address>
GNX Soft Ltd.<br />
Tsar Simeon I, 56<br />
Burgas, Burgas 8000, Bulgaria
</address>
<span className="contact-hours">Schedule a Visit</span>
</div>
</div>
</div>
<div className="row vertical-column-gap-md justify-content-between mt-40">
<div className="col-12 col-lg-7">
<div className="tp-contact__content">
<div className="contact-form mt-40">
<form onSubmit={handleSubmit} className="enterprise-form">
<div className="form-section">
<h4 className="form-section-title">
<i className="fa-solid fa-user"></i>
Personal Information
</h4>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="firstName">First Name *</label>
<input
type="text"
name="firstName"
id="firstName"
value={formData.firstName}
onChange={handleInputChange}
placeholder="John"
required
/>
</div>
</div>
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="lastName">Last Name *</label>
<input
type="text"
name="lastName"
id="lastName"
value={formData.lastName}
onChange={handleInputChange}
placeholder="Smith"
required
/>
</div>
</div>
</div>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="email">Business Email *</label>
<input
type="email"
name="email"
id="email"
value={formData.email}
onChange={handleInputChange}
placeholder="john.smith@company.com"
required
/>
</div>
</div>
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="phone">Phone Number</label>
<input
type="tel"
name="phone"
id="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder="+1 (555) 123-4567"
/>
</div>
</div>
</div>
</div>
<div className="form-section">
<h4 className="form-section-title">
<i className="fa-solid fa-building"></i>
Company Information
</h4>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="company">Company Name *</label>
<input
type="text"
name="company"
id="company"
value={formData.company}
onChange={handleInputChange}
placeholder="Acme Corporation"
required
/>
</div>
</div>
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="jobTitle">Job Title *</label>
<input
type="text"
name="jobTitle"
id="jobTitle"
value={formData.jobTitle}
onChange={handleInputChange}
placeholder="CTO, IT Director, etc."
required
/>
</div>
</div>
</div>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="industry">Industry</label>
<select
name="industry"
id="industry"
value={formData.industry}
onChange={handleInputChange}
>
<option value="">Select Industry</option>
<option value="technology">Technology</option>
<option value="finance">Finance</option>
<option value="healthcare">Healthcare</option>
<option value="manufacturing">Manufacturing</option>
<option value="retail">Retail</option>
<option value="education">Education</option>
<option value="government">Government</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="companySize">Company Size</label>
<select
name="companySize"
id="companySize"
value={formData.companySize}
onChange={handleInputChange}
>
<option value="">Select Company Size</option>
<option value="1-10">1-10 employees</option>
<option value="11-50">11-50 employees</option>
<option value="51-200">51-200 employees</option>
<option value="201-1000">201-1000 employees</option>
<option value="1000+">1000+ employees</option>
</select>
</div>
</div>
</div>
</div>
<div className="form-section">
<h4 className="form-section-title">
<i className="fa-solid fa-project-diagram"></i>
Project Details
</h4>
<div className="row">
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="projectType">Project Type</label>
<select
name="projectType"
id="projectType"
value={formData.projectType}
onChange={handleInputChange}
>
<option value="">Select Project Type</option>
<option value="software-development">Software Development</option>
<option value="cloud-migration">Cloud Migration</option>
<option value="digital-transformation">Digital Transformation</option>
<option value="data-analytics">Data Analytics</option>
<option value="security-compliance">Security & Compliance</option>
<option value="integration">System Integration</option>
<option value="consulting">Consulting Services</option>
</select>
</div>
</div>
<div className="col-12 col-md-6">
<div className="input-single">
<label htmlFor="timeline">Project Timeline</label>
<select
name="timeline"
id="timeline"
value={formData.timeline}
onChange={handleInputChange}
>
<option value="">Select Timeline</option>
<option value="immediate">Immediate (0-3 months)</option>
<option value="short">Short-term (3-6 months)</option>
<option value="medium">Medium-term (6-12 months)</option>
<option value="long">Long-term (12+ months)</option>
<option value="planning">Still planning</option>
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="input-single">
<label htmlFor="budget">Project Budget Range</label>
<select
name="budget"
id="budget"
value={formData.budget}
onChange={handleInputChange}
>
<option value="">Select Budget Range</option>
<option value="under-50k">Under 50,000</option>
<option value="50k-100k">50,000 - 100,000</option>
<option value="100k-250k">100,000 - 250,000</option>
<option value="250k-500k">250,000 - 500,000</option>
<option value="500k-1m">500,000 - 1,000,000</option>
<option value="over-1m">Over 1,000,000</option>
<option value="discuss">Prefer to discuss</option>
</select>
</div>
</div>
</div>
<div className="input-single">
<label htmlFor="message">Project Description *</label>
<textarea
name="message"
id="message"
value={formData.message}
onChange={handleInputChange}
placeholder="Please describe your project requirements, current challenges, and expected outcomes..."
rows={5}
required
/>
</div>
</div>
<div className="form-section">
<h4 className="form-section-title">
<i className="fa-solid fa-shield-halved"></i>
Privacy & Communication
</h4>
<div className="checkbox-group">
<div className="checkbox-single">
<input
type="checkbox"
name="newsletter"
id="newsletter"
checked={formData.newsletter}
onChange={handleInputChange}
/>
<label htmlFor="newsletter">
Subscribe to our newsletter for industry insights and product updates
</label>
</div>
<div className="checkbox-single">
<input
type="checkbox"
name="privacy"
id="privacy"
checked={formData.privacy}
onChange={handleInputChange}
required
/>
<label htmlFor="privacy">
I agree to the <a href="/policy?type=privacy">Privacy Policy</a> and consent to being contacted by our team *
</label>
</div>
</div>
</div>
{/* Status Message */}
{submitStatus.type && (
<div className={`form-status mt-30 ${submitStatus.type === 'success' ? 'success' : 'error'}`}>
<div className="status-content">
<i className={`fa-solid ${submitStatus.type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}`}></i>
<span>{submitStatus.message}</span>
{submitStatus.type === 'error' && (
<div className="error-details mt-10">
<small>
If the problem persists, please contact us directly at{' '}
<a href="mailto:info@gnxsoft.com" className="text-primary">info@gnxsoft.com</a>
</small>
</div>
)}
</div>
</div>
)}
<div className="form-actions mt-40">
<button
type="submit"
className="btn btn-primary enterprise-btn"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<span>Sending...</span>
<i className="fa-solid fa-spinner fa-spin"></i>
</>
) : (
<>
<span>Request Software Consultation</span>
<i className="fa-solid fa-arrow-right"></i>
</>
)}
</button>
<p className="form-disclaimer">
<i className="fa-solid fa-lock"></i>
Your information is secure and will only be used to provide you with relevant software solutions.
</p>
</div>
</form>
</div>
</div>
</div>
<div className="col-12 col-lg-5">
<div className="tp-contact__sidebar">
<div className="contact-sidebar-card">
<div className="sidebar-header">
<h3>Why Choose Our Software Solutions?</h3>
<div className="enterprise-badge">
<i className="fa-solid fa-award"></i>
<span>Enterprise Grade</span>
</div>
</div>
<div className="sidebar-features">
<div className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-shield-halved"></i>
</div>
<div className="feature-content">
<h4>Incident Management</h4>
<p>Advanced incident management software with real-time monitoring and automated response capabilities.</p>
<div className="feature-tags">
<span className="tag">Real-time</span>
<span className="tag">Automated</span>
</div>
</div>
</div>
<div className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-users"></i>
</div>
<div className="feature-content">
<h4>Custom Development</h4>
<p>Tailored software solutions built to your exact specifications with dedicated development teams.</p>
<div className="feature-tags">
<span className="tag">Dedicated Teams</span>
<span className="tag">Custom</span>
</div>
</div>
</div>
<div className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-chart-line"></i>
</div>
<div className="feature-content">
<h4>System Integrations & APIs</h4>
<p>Seamless integration with your existing systems and third-party applications through robust APIs for unified workflows.</p>
<div className="feature-tags">
<span className="tag">API-First</span>
<span className="tag">Seamless</span>
</div>
</div>
</div>
<div className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-cogs"></i>
</div>
<div className="feature-content">
<h4>Data Replication</h4>
<p>Reliable data replication and synchronization solutions to ensure data consistency across systems.</p>
<div className="feature-tags">
<span className="tag">Reliable</span>
<span className="tag">Sync</span>
</div>
</div>
</div>
</div>
</div>
<div className="contact-sidebar-card">
<h3>Next Steps</h3>
<div className="next-steps">
<div className="step-item">
<div className="step-number">1</div>
<div className="step-content">
<h4>Initial Consultation</h4>
<p>We&apos;ll review your requirements and provide initial recommendations within 24 hours.</p>
</div>
</div>
<div className="step-item">
<div className="step-number">2</div>
<div className="step-content">
<h4>Custom Demo</h4>
<p>Schedule a personalized demonstration tailored to your specific use case.</p>
</div>
</div>
<div className="step-item">
<div className="step-number">3</div>
<div className="step-content">
<h4>Proposal & Pricing</h4>
<p>Receive a detailed proposal with custom pricing and implementation timeline.</p>
</div>
</div>
</div>
</div>
<div className="contact-sidebar-card">
<h3>Find Us</h3>
<div className="company-map">
<div className="map-container">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2933.123456789!2d27.4758968970689!3d42.496781103070504!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x40a6b8c9d1234567%3A0x1234567890abcdef!2sBurgas%2C%20Bulgaria!5e0!3m2!1sen!2sbg!4v1234567890123!5m2!1sen!2sbg"
width="100%"
height="200"
style={{ border: 0, borderRadius: '8px' }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Company Location"
/>
</div>
<div className="map-info">
<div className="location-details">
<div className="location-icon">
<i className="fa-solid fa-map-marker-alt"></i>
</div>
<div className="location-text">
<h4>GNX Soft Ltd.</h4>
<p>Tsar Simeon I, 56<br />Burgas, Burgas 8000, Bulgaria</p>
</div>
</div>
<div className="location-actions">
<a href="https://maps.google.com/?q=Tsar+Simeon+I+56+Burgas+Bulgaria" target="_blank" rel="noopener noreferrer" className="map-link">
<i className="fa-solid fa-directions"></i>
Get Directions
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default ContactSection;

View File

@@ -0,0 +1,433 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
const HomeBanner = () => {
const [currentTextIndex, setCurrentTextIndex] = useState(0);
const [isTransitioning, setIsTransitioning] = useState(false);
// Fix viewport height for mobile browsers (especially iOS Safari)
useEffect(() => {
const setVH = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVH();
window.addEventListener('resize', setVH);
// Use 'resize' event instead of deprecated 'orientationchange'
// The resize event fires on orientation change as well
const handleOrientationChange = () => {
// Small delay to ensure viewport has updated
setTimeout(setVH, 100);
};
window.addEventListener('resize', handleOrientationChange);
return () => {
window.removeEventListener('resize', setVH);
window.removeEventListener('resize', handleOrientationChange);
};
}, []);
// Static banner slides data based on actual services
const carouselTexts = [
{
id: 1,
badge: "Custom Development",
icon: "fa-solid fa-code",
heading: "Tailored Enterprise Software ",
highlight: "Development",
subheading: "Aligned with Your Business Goals",
description: "We design and build custom digital solutions that deliver reliable, scalable, and future-ready applications, driving measurable value and competitive advantage for your enterprise.",
button_text: "Explore Solutions",
button_url: "/services/custom-software-development",
is_active: true,
display_order: 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 2,
badge: "Business Intelligence",
icon: "fa-solid fa-brain",
heading: "AI-Powered ",
highlight: "Analytics",
subheading: "Transform Data into Insights",
description: "Turn enterprise data into actionable intelligence with advanced AI and machine learning, enabling smarter decisions, performance optimization, and data-driven innovation.",
button_text: "Discover AI Solutions",
button_url: "/services/ai-powered-business-intelligence",
is_active: true,
display_order: 2,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 3,
badge: "System Integration",
icon: "fa-solid fa-plug",
heading: "Enterprise Systems ",
highlight: "Integration",
subheading: "Seamless Connectivity",
description: "Connect everything — from payment systems and ERP platforms to cloud services, enabling your enterprise to operate seamlessly across physical and digital environments.",
button_text: "View Integrations",
button_url: "/services/external-systems-integrations",
is_active: true,
display_order: 3,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 4,
badge: "Incident Management",
icon: "fa-solid fa-bell",
heading: "Intelligent Incident ",
highlight: "Management",
subheading: "Minimize Downtime & Protect Trust",
description: "Cloud-based incident management that empowers teams to detect, respond, and resolve issues faster, reducing downtime and maintaining customer confidence.",
button_text: "Learn More",
button_url: "/services/incident-management-saas",
is_active: true,
display_order: 4,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
];
// Carousel rotation effect
useEffect(() => {
if (carouselTexts.length <= 1) return;
const interval = setInterval(() => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentTextIndex((prevIndex) =>
prevIndex === carouselTexts.length - 1 ? 0 : prevIndex + 1
);
setIsTransitioning(false);
}, 1000); // Slightly longer for smoother transition
}, 6000); // Increased interval for slower changes
return () => clearInterval(interval);
}, [carouselTexts.length]);
const currentText = carouselTexts[currentTextIndex];
if (!currentText) {
return null;
}
return (
<>
<section className="modern-banner">
<div className="banner-background">
<div className="gradient-orb orb-1"></div>
<div className="gradient-orb orb-2"></div>
<div className="gradient-orb orb-3"></div>
{/* Industrial Enterprise Background Elements */}
<div className="enterprise-bg-elements">
{/* Flying Code Elements */}
<div className="flying-code">
<div className="code-snippet code-1">
<span className="code-line">const enterprise = {'{'}</span>
<span className="code-line"> security: &apos;enterprise-grade&apos;,</span>
<span className="code-line"> scalability: &apos;unlimited&apos;</span>
<span className="code-line">{'}'};</span>
</div>
<div className="code-snippet code-2">
<span className="code-line">if (security === &apos;max&apos;) {'{'}</span>
<span className="code-line"> deploy.enterprise();</span>
<span className="code-line">{'}'}</span>
</div>
<div className="code-snippet code-3">
<span className="code-line">class EnterpriseSoftware {'{'}</span>
<span className="code-line"> constructor() {'{'}</span>
<span className="code-line"> this.secure = true;</span>
<span className="code-line"> {'}'}</span>
<span className="code-line">{'}'}</span>
</div>
<div className="code-snippet code-4">
<span className="code-line">API.authenticate({'{'}</span>
<span className="code-line"> level: &apos;enterprise&apos;,</span>
<span className="code-line"> encryption: &apos;AES-256&apos;</span>
<span className="code-line">{'}'});</span>
</div>
</div>
{/* Industrial Grid */}
<div className="industrial-grid">
<div className="grid-line horizontal h-1"></div>
<div className="grid-line horizontal h-2"></div>
<div className="grid-line horizontal h-3"></div>
<div className="grid-line horizontal h-4"></div>
<div className="grid-line vertical v-1"></div>
<div className="grid-line vertical v-2"></div>
<div className="grid-line vertical v-3"></div>
<div className="grid-line vertical v-4"></div>
</div>
{/* Security Elements */}
<div className="security-elements">
<div className="shield shield-1">
<i className="fa-solid fa-shield-halved"></i>
</div>
<div className="shield shield-2">
<i className="fa-solid fa-lock"></i>
</div>
<div className="shield shield-3">
<i className="fa-solid fa-key"></i>
</div>
<div className="shield shield-4">
<i className="fa-solid fa-fingerprint"></i>
</div>
</div>
{/* Circuit Patterns */}
<div className="circuit-patterns">
<div className="circuit circuit-1">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
<div className="circuit circuit-2">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
<div className="circuit circuit-3">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
</div>
{/* Data Streams */}
<div className="data-streams">
<div className="stream stream-1">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
<div className="stream stream-2">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
<div className="stream stream-3">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
</div>
{/* Request/Response Data */}
<div className="request-response-data">
<div className="api-request req-1">
<div className="request-label">POST /api/enterprise</div>
<div className="request-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-response resp-1">
<div className="response-label">200 OK</div>
<div className="response-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-request req-2">
<div className="request-label">GET /api/analytics</div>
<div className="request-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-response resp-2">
<div className="response-label">201 Created</div>
<div className="response-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
</div>
{/* Space Data Generation */}
<div className="space-data-generation">
<div className="data-cluster cluster-1">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-2">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-3">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-4">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
</div>
{/* Database Connections */}
<div className="database-connections">
<div className="db-connection conn-1">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-database"></i>
</div>
<div className="connection-pulse"></div>
</div>
<div className="db-connection conn-2">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-server"></i>
</div>
<div className="connection-pulse"></div>
</div>
<div className="db-connection conn-3">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-cloud"></i>
</div>
<div className="connection-pulse"></div>
</div>
</div>
{/* Real-time Metrics */}
<div className="real-time-metrics">
<div className="metric metric-1">
<div className="metric-label">API Calls/sec</div>
<div className="metric-value">2,847</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
<div className="metric metric-2">
<div className="metric-label">Data Processed</div>
<div className="metric-value">15.2TB</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
<div className="metric metric-3">
<div className="metric-label">Active Users</div>
<div className="metric-value">45,892</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
</div>
</div>
</div>
<div className="container">
<div className="banner-content">
<div className="content-center">
<div className="badge-container">
<span className="badge">
<i className={currentText.icon}></i>
{currentText.badge}
</span>
</div>
<h1 className={`main-heading carousel-text ${isTransitioning ? 'fade-out' : 'fade-in'}`}>
{currentText.heading}
<span className="gradient-text"> {currentText.highlight}</span>
<br />
{currentText.subheading}
</h1>
<p className={`description carousel-text ${isTransitioning ? 'fade-out' : 'fade-in'}`}>
{currentText.description}
</p>
{/* Carousel Indicators */}
<div className="carousel-indicators">
{carouselTexts.map((_, index) => (
<button
key={index}
className={`indicator ${index === currentTextIndex ? 'active' : ''}`}
onClick={() => {
if (index !== currentTextIndex) {
setIsTransitioning(true);
setTimeout(() => {
setCurrentTextIndex(index);
setIsTransitioning(false);
}, 1000);
}
}}
/>
))}
</div>
<div className="cta-section">
<Link href={currentText.button_url || "#"} className="cta-primary">
<span>{currentText.button_text || "Learn More"}</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
<Link href="contact-us" className="cta-secondary">
<i className="fa-solid fa-phone"></i>
<span>Contact Sales</span>
</Link>
</div>
<div className="trust-indicators">
<div className="trust-item">
<div className="trust-number">30+</div>
<div className="trust-label">Enterprise Clients</div>
</div>
<div className="trust-item">
<div className="trust-number">99.9%</div>
<div className="trust-label">Uptime SLA</div>
</div>
<div className="trust-item">
<div className="trust-number">24/7</div>
<div className="trust-label">Enterprise Support</div>
</div>
</div>
</div>
</div>
</div>
<div className="scroll-indicator" onClick={() => window.scrollTo({ top: window.innerHeight, behavior: 'smooth' })}>
<div className="scroll-text">Scroll to explore</div>
<div className="scroll-arrow">
<i className="fa-solid fa-chevron-down"></i>
</div>
</div>
</section>
</>
);
};
export default HomeBanner;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const HomeInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default HomeInitAnimations;

View File

@@ -0,0 +1,123 @@
"use client";
import Image from "next/legacy/image";
import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules";
import "swiper/swiper-bundle.css";
import { useLatestPosts } from "@/lib/hooks/useBlog";
import { getValidImageUrl, getValidImageAlt, FALLBACK_IMAGES } from "@/lib/imageUtils";
const HomeLatestPost = () => {
const { posts, loading, error } = useLatestPosts(12); // Get 12 latest posts
return (
<section className="tp-latest-post pt-120 pb-120 bg-quinary">
<div className="container">
<div className="row vertical-column-gap align-items-center">
<div className="col-12 col-lg-7">
<div className="tp-lp-title text-center text-lg-start">
<h2 className="mt-8 fw-7 text-secondary title-anim">
Latest Insights
</h2>
</div>
</div>
<div className="col-12 col-lg-5">
<div className="tp-lp-cta text-center text-lg-end">
<Link href="/insights" className="btn-line text-uppercase">
View All Insights
</Link>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="tp-lp-slider-wrapper mt-60">
{loading ? (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="text-tertiary mt-3">Loading insights...</p>
</div>
) : error ? (
<div className="text-center py-5">
<p className="text-danger">Error loading insights. Please try again later.</p>
</div>
) : posts.length === 0 ? (
<div className="text-center py-5">
<p className="text-tertiary">No insights available yet.</p>
</div>
) : (
<div className="tp-lp-slider-wrapper">
<Swiper
slidesPerView={"auto"}
spaceBetween={24}
slidesPerGroup={1}
freeMode={true}
speed={1200}
loop={posts.length > 3}
roundLengths={true}
modules={[Autoplay]}
autoplay={{
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true,
}}
className="tp-lp-slider"
>
{posts.map((post) => (
<SwiperSlide key={post.id}>
<div className="tp-lp-slider__single topy-tilt">
<div className="thumb mb-24">
<Link
href={`/insights/${post.slug}`}
className="w-100 overflow-hidden d-block"
>
<div className="parallax-image-wrap">
<div className="parallax-image-inner">
<Image
src={getValidImageUrl(post.thumbnail, FALLBACK_IMAGES.BLOG)}
width={400}
height={220}
className="w-100 mh-220 parallax-image"
alt={getValidImageAlt(post.title)}
/>
</div>
</div>
</Link>
</div>
<div className="content">
<div className="tp-lp-post__meta mb-24 mt-8">
<p className="author text-xs text-tertiary">
{post.author_name || 'Admin'}
</p>
<span></span>
<p className="date text-xs text-tertiary">
{new Date(post.published_at || post.created_at).toLocaleDateString('en-US', {
day: 'numeric',
month: 'short',
year: 'numeric'
})}
</p>
</div>
<h5 className="mt-8 fw-5 text-secondary">
<Link href={`/insights/${post.slug}`}>
{post.title}
</Link>
</h5>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
)}
</div>
</div>
</div>
</div>
</section>
);
};
export default HomeLatestPost;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const HomeScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default HomeScrollProgressButton;

View File

@@ -0,0 +1,121 @@
"use client";
import Image from "next/legacy/image";
import Link from "next/link";
import { useMemo } from "react";
import { useServices } from "@/lib/hooks/useServices";
import { serviceUtils } from "@/lib/api/serviceService";
import one from "@/public/images/overview/one.png";
import two from "@/public/images/overview/two.png";
import three from "@/public/images/overview/three.png";
import four from "@/public/images/overview/four.png";
import five from "@/public/images/overview/five.png";
// Default images array for fallback
const defaultImages = [one, two, three, four, five];
const Overview = () => {
// Memoize the parameters to prevent infinite re-renders
const serviceParams = useMemo(() => ({
ordering: 'display_order',
page: 1
}), []);
// Fetch services from API, limit to 5 for overview section
const { services, loading, error } = useServices(serviceParams);
// Get first 5 services for the overview
const displayServices = services.slice(0, 5);
return (
<section
className="tp-overview pt-120 pb-120 sticky-wrapper"
id="scroll-to"
>
<div className="container">
<div className="row vertical-column-gap-lg">
<div className="col-12 col-lg-5">
<div className="tp-overview__content sticky-item">
<h2 className="fw-7 text-secondary title-anim mb-30 mt-8">
Enterprise Solutions That Scale
</h2>
<p className="mt-8 cur-lg">
Our enterprise software development teams deliver mission-critical solutions
for Fortune 500 companies. We design, build, and deploy scalable systems
that transform your business operations, enhance security, and drive digital
innovation across your organization.
</p>
<div className="mt-40">
<Link href="services" className="btn-line text-uppercase">
EXPLORE SOLUTIONS
</Link>
</div>
</div>
</div>
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1">
<div className="tp-overview__items sticky-item">
{loading ? (
// Loading state
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3 text-muted">Loading services...</p>
</div>
) : error ? (
// Error state
<div className="text-center py-5">
<p className="text-danger">Failed to load services. Please try again later.</p>
</div>
) : displayServices.length === 0 ? (
// No services state
<div className="text-center py-5">
<p className="text-muted">No services available at the moment.</p>
</div>
) : (
// Services list
displayServices.map((service, index) => {
// Always use hardcoded images, not API images
const serviceImage = defaultImages[index] || defaultImages[0];
return (
<div key={service.id} className="tp-overview-single appear-down">
<div className="thumb">
<Image
src={serviceImage}
width={80}
height={80}
alt={service.title}
/>
</div>
<div className="wrapper">
<div className="content">
<h4 className="mt-8 mb-12 fw-6 text-secondary">
<Link href={`/services/${service.slug}`}>
{service.title}
</Link>
</h4>
<p className="text-tertiary">
{service.short_description || service.description}
</p>
</div>
<div className="cta">
<Link href={`/services/${service.slug}`}>
<span className="material-symbols-outlined">
call_made
</span>
</Link>
</div>
</div>
</div>
);
})
)}
</div>
</div>
</div>
</div>
</section>
);
};
export default Overview;

View File

@@ -0,0 +1,78 @@
import Image from "next/legacy/image";
import Link from "next/link";
import thumb from "@/public/images/leading.jpg";
const ServiceIntro = () => {
return (
<section className="tp-service pt-120 pb-120">
<div className="container">
<div className="row vertical-column-gap-md">
<div className="col-12 col-lg-4">
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
<Link href="services">
<Image
src={thumb}
alt="Enterprise Software Solutions"
width={400}
height={500}
objectFit="cover"
style={{ display: 'block', border: 'none', margin: 0, padding: 0 }}
/>
</Link>
</div>
</div>
<div className="col-12 col-lg-8">
<div className="tp-service__content">
<div className="tp-section-wrapper">
<h2 className="title-anim text-secondary fw-7 mb-30">
Accelerating Digital Transformation Through<br />
Mission-Critical Enterprise Software
</h2>
</div>
<div className="pl-50">
<p className="cur-lg mb-25">
GNX partners with Fortune 40 companies and global enterprises to architect,
develop, and deploy business-critical software solutions that drive measurable
results. Our engineering teams deliver secure, scalable, and compliant systems
that power digital innovation across industries.
</p>
<p className="cur-lg mb-30">
From cloud-native architectures and enterprise integration platforms to
AI-powered analytics and legacy modernization, we provide end-to-end
technology solutions that reduce operational costs, enhance efficiency,
and deliver competitive advantage.
</p>
<div className="tp-service__features mb-40">
<ul className="list-unstyled">
<li className="mb-15">
<i className="fa-solid fa-circle-check text-primary me-10"></i>
<span className="fw-6">Enterprise-Grade Security & Compliance</span>
</li>
<li className="mb-15">
<i className="fa-solid fa-circle-check text-primary me-10"></i>
<span className="fw-6">Scalable Cloud-Native Architectures</span>
</li>
<li className="mb-15">
<i className="fa-solid fa-circle-check text-primary me-10"></i>
<span className="fw-6">24/7 Support & Dedicated Engineering Teams</span>
</li>
</ul>
</div>
<div className="mt-60">
<Link href="services" className="btn-anim btn-anim-light">
Explore Our Solutions
<i className="fa-solid fa-arrow-right"></i>
<span></span>
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default ServiceIntro;

View File

@@ -0,0 +1,175 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import Image from "next/legacy/image";
import Link from "next/link";
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
import { API_CONFIG } from "@/lib/config/api";
const Story = () => {
const [activeIndex, setActiveIndex] = useState(0);
const [activeImageIndex, setActiveImageIndex] = useState(0);
// Fetch case studies from API with ordering and limit
const params = useMemo(() => ({
ordering: 'display_order',
page_size: 5
}), []);
const { caseStudies, loading, error } = useCaseStudies(params);
// Fallback to static data if API fails or is loading
const staticStoryData = [
{
id: 1,
category_name: "Financial Services",
title: "Banking System Modernization",
excerpt: "Complete digital transformation of legacy banking systems with enhanced security and real-time processing capabilities.",
thumbnail: "/images/case/one.png",
slug: "banking-system-modernization",
display_order: 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 2,
category_name: "Healthcare",
title: "Patient Management System",
excerpt: "Enterprise-grade patient management system with HIPAA compliance and seamless integration across multiple healthcare facilities.",
thumbnail: "/images/case/two.png",
slug: "patient-management-system",
display_order: 2,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 3,
category_name: "Manufacturing",
title: "Supply Chain Optimization",
excerpt: "Advanced supply chain management system with real-time tracking, predictive analytics, and automated inventory management.",
thumbnail: "/images/case/three.png",
slug: "supply-chain-optimization",
display_order: 3,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 4,
category_name: "E-commerce",
title: "Multi-Platform Integration",
excerpt: "Seamless integration of multiple e-commerce platforms with unified inventory management and real-time synchronization.",
thumbnail: "/images/case/four.png",
slug: "multi-platform-integration",
display_order: 4,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
},
{
id: 5,
category_name: "Education",
title: "Learning Management System",
excerpt: "Comprehensive LMS with advanced analytics, automated grading, and seamless integration with existing educational tools.",
thumbnail: "/images/case/five.png",
slug: "learning-management-system",
display_order: 5,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
];
// Use API data if available, otherwise use static data
const storyData = caseStudies.length > 0 ? caseStudies : staticStoryData;
// Log when API data is loaded
useEffect(() => {
if (caseStudies.length > 0) {
}
}, [caseStudies]);
const handleMouseEnter = (index: number) => {
setActiveIndex(index);
setActiveImageIndex(index);
};
return (
<section className="tp-story pt-120 pb-120 bg-black sticky-wrapper fade-wrapper">
<div className="container">
<div className="row vertical-column-gap-md">
<div className="col-12 col-lg-5">
<div className="tp-story__content sticky-item">
<h2 className="mt-8 title-anim text-white fw-7">
Enterprise Case Studies
</h2>
<div className="tp-story__items">
{storyData.map((item, index) => {
return (
<div
key={index}
className={`tp-story__single fade-top ${
index === activeIndex ? "active" : ""
}`}
onMouseEnter={() => handleMouseEnter(index)}
>
<p className="fw-6 mt-8">
<Link href={`/case-study/${item.slug}`}>
{item.category_name || "Case Study"}
</Link>
</p>
<h5 className="fw-4 mt-12 mb-12 text-white">
{item.title}
</h5>
<p className="text-xs">{item.excerpt}</p>
</div>
);
})}
</div>
</div>
</div>
<div className="col-12 col-lg-7 col-xxl-6 offset-xxl-1 d-none d-lg-block">
<div className="tp-story__thumbs sticky-item">
{storyData.map((item, index) => {
// Get the image URL - handle different scenarios
let imageUrl;
if (item.thumbnail) {
if (item.thumbnail.startsWith('http')) {
// Full URL (external)
imageUrl = item.thumbnail;
} else if (item.thumbnail.startsWith('/media')) {
// Relative path starting with /media
imageUrl = `${API_CONFIG.BASE_URL}${item.thumbnail}`;
} else {
// Just filename or relative path
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
}
} else {
// Fallback to static image
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
}
return (
<div
key={index}
className={`tp-story-thumb ${
index === activeImageIndex ? "thumb-active" : ""
}`}
>
<Image
src={imageUrl}
width={600}
height={300}
className="w-100 mh-300"
alt={item.title || "Case Study"}
/>
</div>
);
})}
</div>
</div>
</div>
</div>
</section>
);
};
export default Story;

View File

@@ -0,0 +1,104 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { Service } from "@/lib/api/serviceService";
interface ServiceDeliverablesProps {
service: Service;
}
const ServiceDeliverables = ({ service }: ServiceDeliverablesProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Modern entrance animations
gsap.set(".deliverable-item", {
y: 60,
opacity: 0,
scale: 0.9,
});
ScrollTrigger.batch(".deliverable-item", {
start: "-150px bottom",
onEnter: (elements) =>
gsap.to(elements, {
y: 0,
opacity: 1,
scale: 1,
stagger: {
amount: 0.6,
from: "start"
},
duration: 1,
ease: "power3.out",
}),
});
// Animate section header
gsap.fromTo(".section-header",
{
y: 40,
opacity: 0
},
{
y: 0,
opacity: 1,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: ".section-header",
start: "-100px bottom"
}
}
);
}, []);
if (!service.deliverables) {
return null;
}
const deliverablesList = service.deliverables
.split(/[,;•\n]/)
.map(item => item.trim())
.filter(item => item.length > 0);
return (
<section className="enterprise-deliverables py-4">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="section-header text-center mb-4">
<span className="enterprise-section-tag">What You Get</span>
<h2 className="enterprise-section-title mb-3">
What You Get with {service.title}
</h2>
<p className="enterprise-section-description">
{service.deliverables_description || `Our comprehensive ${service.title.toLowerCase()} service includes everything you need for success`}
</p>
</div>
<div className="row g-5">
{deliverablesList.map((deliverable, index) => (
<div key={index} className="col-12 col-md-6 col-lg-4">
<div className="deliverable-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-check-circle"></i>
</div>
<div className="card-content">
<h6 className="card-title">
{deliverable}
</h6>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
export default ServiceDeliverables;

View File

@@ -0,0 +1,125 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Image from "next/legacy/image";
import { Service } from "@/lib/api/serviceService";
import { serviceUtils } from "@/lib/api/serviceService";
interface ServiceDetailsProps {
service: Service;
}
const ServiceDetails = ({ service }: ServiceDetailsProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Animate content on scroll
gsap.set(".detail-content", {
x: -50,
opacity: 0,
});
gsap.set(".detail-image", {
x: 50,
opacity: 0,
});
ScrollTrigger.batch(".detail-content, .detail-image", {
start: "-100px bottom",
onEnter: (elements) =>
gsap.to(elements, {
x: 0,
opacity: 1,
duration: 0.8,
ease: "power2.out",
}),
});
}, []);
return (
<section id="service-details" className="enterprise-details py-5">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="row align-items-center">
<div className="col-12 col-lg-6">
<div className="detail-content">
<div className="section-header mb-5">
<span className="enterprise-section-tag">About Service</span>
<h2 className="enterprise-section-title mb-4">
About Our {service.title}
</h2>
{service.short_description && (
<p className="enterprise-section-description">
{service.short_description}
</p>
)}
</div>
<div className="enterprise-stats mb-4">
<div className="row g-3">
<div className="col-12">
<div className="enterprise-stat-card">
<div className="stat-icon">
<i className="fa-solid fa-star"></i>
</div>
<div className="stat-content">
<div className="stat-number">
{service.featured ? 'Featured' : 'Standard'}
</div>
<div className="stat-label">Service Type</div>
</div>
</div>
</div>
</div>
</div>
<div className="enterprise-meta mb-4">
<div className="meta-item">
<div className="meta-icon">
<i className="fa-solid fa-tag"></i>
</div>
<div className="meta-content">
<span className="meta-label">Category</span>
<span className="meta-value">{service.category?.name || 'General'}</span>
</div>
</div>
{service.duration && (
<div className="meta-item">
<div className="meta-icon">
<i className="fa-solid fa-clock"></i>
</div>
<div className="meta-content">
<span className="meta-label">Duration</span>
<span className="meta-value">{service.duration}</span>
</div>
</div>
)}
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="detail-image">
<div className="enterprise-image-wrapper">
<Image
src={serviceUtils.getServiceImageUrl(service) || '/images/service/default.png'}
alt={service.title}
width={600}
height={400}
className="enterprise-service-image"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default ServiceDetails;

View File

@@ -0,0 +1,102 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { Service, ServiceFeature } from "@/lib/api/serviceService";
interface ServiceFeaturesProps {
service: Service;
}
const ServiceFeatures = ({ service }: ServiceFeaturesProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Modern entrance animations
gsap.set(".feature-item", {
y: 60,
opacity: 0,
scale: 0.9,
});
ScrollTrigger.batch(".feature-item", {
start: "-150px bottom",
onEnter: (elements) =>
gsap.to(elements, {
y: 0,
opacity: 1,
scale: 1,
stagger: {
amount: 0.6,
from: "start"
},
duration: 1,
ease: "power3.out",
}),
});
// Animate section header
gsap.fromTo(".section-header",
{
y: 40,
opacity: 0
},
{
y: 0,
opacity: 1,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: ".section-header",
start: "-100px bottom"
}
}
);
}, []);
if (!service.features || service.features.length === 0) {
return null;
}
return (
<section className="enterprise-features py-4">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="section-header text-center mb-4">
<span className="enterprise-section-tag">Key Features</span>
<h2 className="enterprise-section-title mb-3">
{service.title} Features
</h2>
<p className="enterprise-section-description">
{service.features_description || `Discover the key features that make our ${service.title.toLowerCase()} service stand out from the competition`}
</p>
</div>
<div className="row g-5">
{service.features.map((feature: ServiceFeature, index: number) => (
<div key={feature.id} className="col-12 col-md-6 col-lg-4">
<div className="feature-item enterprise-section-card">
<div className="card-icon">
<i className={`fa-solid fa-${feature.icon || 'check'}`}></i>
</div>
<div className="card-content">
<h6 className="card-title">
{feature.title}
</h6>
<p className="card-description">
{feature.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
export default ServiceFeatures;

View File

@@ -0,0 +1,155 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/legacy/image";
import Link from "next/link";
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
import { useServices } from "@/lib/hooks/useServices";
import { serviceUtils } from "@/lib/api/serviceService";
const ServiceMain = () => {
// Fetch services from API
const { services, loading, error } = useServices();
// Show loading state
if (loading) {
return (
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-3">Loading services...</p>
</div>
</div>
</div>
</div>
</section>
);
}
// Show error state
if (error) {
return (
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center">
<div className="alert alert-danger" role="alert">
<h4>Error Loading Services</h4>
<p>{error}</p>
<button
className="btn btn-primary"
onClick={() => window.location.reload()}
>
Try Again
</button>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
// Show empty state
if (!services || services.length === 0) {
return (
<section className="tp-service pt-120 fade-wrapper" id="scroll-to">
<div className="container">
<div className="row">
<div className="col-12">
<div className="text-center">
<h3>No Services Available</h3>
<p>We&apos;re working on adding new services. Please check back later.</p>
</div>
</div>
</div>
</div>
</section>
);
}
return (
<section className="enterprise-services py-5" id="scroll-to">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="section-header text-center mb-5">
<span className="enterprise-section-tag">Our Services</span>
<h2 className="enterprise-section-title mb-4">
Professional Software Development Services
</h2>
<p className="enterprise-section-description">
We deliver comprehensive technology solutions tailored to your business needs
</p>
</div>
</div>
</div>
<div className="row g-4">
{services.map((service, index) => (
<div key={service.id} className="col-12 col-lg-6 col-xl-4">
<div className="enterprise-service-card">
<div className="service-image-wrapper">
<Link href={`/services/${service.slug}`}>
<Image
src={getValidImageUrl(serviceUtils.getServiceImageUrl(service), FALLBACK_IMAGES.SERVICE)}
alt={service.title}
width={400}
height={300}
className="service-image"
/>
</Link>
<div className="service-overlay">
<Link href={`/services/${service.slug}`} className="btn btn-primary">
<span>Learn More</span>
<i className="fa-solid fa-arrow-right ms-2"></i>
</Link>
</div>
</div>
<div className="service-content">
<div className="service-meta">
<span className="service-number">
{String(index + 1).padStart(2, '0')}
</span>
{service.category && (
<span className="service-category">
{service.category.name}
</span>
)}
</div>
<h3 className="service-title">
<Link href={`/services/${service.slug}`}>
{service.title}
</Link>
</h3>
<p className="service-description">
{service.short_description || service.description}
</p>
<div className="service-footer">
<Link href={`/services/${service.slug}`} className="service-link">
<span>View Details</span>
<i className="fa-solid fa-arrow-right"></i>
</Link>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default ServiceMain;

View File

@@ -0,0 +1,151 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Link from "next/link";
import { Service } from "@/lib/api/serviceService";
interface ServicePricingProps {
service: Service;
}
const ServicePricing = ({ service }: ServicePricingProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Animate pricing section
gsap.set(".pricing-content", {
y: 50,
opacity: 0,
});
ScrollTrigger.create({
trigger: ".pricing-content",
start: "-100px bottom",
onEnter: () =>
gsap.to(".pricing-content", {
y: 0,
opacity: 1,
duration: 0.8,
ease: "power2.out",
}),
});
}, []);
return (
<section className="enterprise-pricing py-5">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="row">
<div className="col-12 col-lg-8 offset-lg-2">
<div className="section-header text-center mb-5">
<span className="enterprise-section-tag">Pricing & Packages</span>
<h2 className="enterprise-section-title mb-4">
Pricing & Packages
</h2>
<p className="enterprise-section-description">
Get started with our {service.title.toLowerCase()} service.
Contact us for a customized quote based on your specific requirements.
</p>
</div>
<div className="pricing-content text-center">
<div className="enterprise-pricing-card">
<div className="pricing-header">
<div className="pricing-badge">
<span>Most Popular</span>
</div>
<h3 className="pricing-title">
{service.title}
</h3>
<div className="price-display">
<span className="price-period">
Contact Us for Pricing
</span>
</div>
</div>
<div className="pricing-features">
<ul className="enterprise-feature-list">
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-check"></i>
</div>
<span>Custom {service.title.toLowerCase()} solution</span>
</li>
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-check"></i>
</div>
<span>Professional consultation</span>
</li>
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-check"></i>
</div>
<span>Ongoing support & maintenance</span>
</li>
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-check"></i>
</div>
<span>Quality assurance & testing</span>
</li>
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-check"></i>
</div>
<span>Documentation & training</span>
</li>
{service.duration && (
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-clock"></i>
</div>
<span>Project duration: {service.duration}</span>
</li>
)}
{service.technologies && (
<li className="feature-item">
<div className="feature-icon">
<i className="fa-solid fa-code"></i>
</div>
<span>Latest technologies & frameworks</span>
</li>
)}
</ul>
</div>
<div className="pricing-cta">
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
<span>Get Free Quote</span>
<i className="fa-solid fa-arrow-right ms-2"></i>
</Link>
<Link href="/services" className="btn btn-outline-secondary btn-lg mb-3">
<span>View All Services</span>
</Link>
</div>
</div>
<div className="pricing-note">
<div className="note-content">
<i className="fa-solid fa-info-circle"></i>
<p>
Final pricing depends on project scope, complexity, and specific requirements.
Contact us for a detailed proposal.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default ServicePricing;

View File

@@ -0,0 +1,130 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { Service } from "@/lib/api/serviceService";
interface ServiceProcessProps {
service: Service;
}
const ServiceProcess = ({ service }: ServiceProcessProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Modern entrance animations
gsap.set(".process-step", {
y: 60,
opacity: 0,
scale: 0.9,
});
ScrollTrigger.batch(".process-step", {
start: "-150px bottom",
onEnter: (elements) =>
gsap.to(elements, {
y: 0,
opacity: 1,
scale: 1,
stagger: {
amount: 0.6,
from: "start"
},
duration: 1,
ease: "power3.out",
}),
});
// Animate section header
gsap.fromTo(".section-header",
{
y: 40,
opacity: 0
},
{
y: 0,
opacity: 1,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: ".section-header",
start: "-100px bottom"
}
}
);
}, []);
if (!service.process_steps) {
return null;
}
// Split process steps by common separators
const processSteps = service.process_steps
.split(/[,;•\n]/)
.map(step => step.trim())
.filter(step => step.length > 0);
return (
<section className="enterprise-process py-4">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="section-header text-center mb-4">
<span className="enterprise-section-tag">Our Process</span>
<h2 className="enterprise-section-title mb-3">
{service.title} Process
</h2>
<p className="enterprise-section-description">
{service.process_description || `Our proven methodology ensures successful delivery of your ${service.title.toLowerCase()} project.`}
</p>
</div>
<div className="row g-5">
{processSteps.map((step, index) => (
<div key={index} className="col-12 col-md-6 col-lg-4">
<div className="process-step enterprise-process-step-compact">
<div className="step-number">
<span className="step-number-text">
{String(index + 1).padStart(2, '0')}
</span>
</div>
<div className="step-content">
<h6 className="step-title">
{step}
</h6>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
// Helper function to generate step descriptions based on step name and service
const getStepDescription = (step: string, serviceTitle: string): string => {
const descriptions: { [key: string]: string } = {
'Requirements Analysis': 'We analyze your business requirements and technical specifications to ensure we understand your goals.',
'System Design': 'Our team creates a comprehensive system architecture and design that meets your specific needs.',
'Development': 'We implement the solution using industry best practices and modern technologies.',
'Testing': 'Rigorous testing ensures your solution is reliable, secure, and performs optimally.',
'Deployment': 'We handle the deployment process and ensure smooth transition to production.',
'Training': 'We provide comprehensive training to your team for successful adoption.',
'API Planning': 'We design the API architecture and define endpoints based on your integration needs.',
'API Development': 'We build robust, scalable APIs using modern frameworks and best practices.',
'Documentation': 'Comprehensive API documentation ensures easy integration and maintenance.',
'Integration': 'We integrate the API with your existing systems and third-party services.',
'Assessment': 'We evaluate your current infrastructure and identify migration opportunities.',
'Migration Planning': 'We create a detailed migration strategy with minimal downtime.',
'Implementation': 'We execute the migration plan with careful monitoring and rollback procedures.',
'Optimization': 'We optimize your cloud infrastructure for performance and cost efficiency.',
'Support': 'Ongoing support and maintenance ensure your solution continues to perform optimally.'
};
return descriptions[step] || `This step involves ${step.toLowerCase()} to ensure the success of your ${serviceTitle.toLowerCase()} project.`;
};
export default ServiceProcess;

View File

@@ -0,0 +1,113 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Image from "next/legacy/image";
import Link from "next/link";
const ServicesBanner = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
if (document.querySelector(".service-banner")) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".service-banner",
start: "center center",
end: "+=100%",
scrub: true,
pin: false,
},
});
tl.to(".thumb-one", {
opacity: 0.3,
y: "-100%",
zIndex: -1,
duration: 2,
});
tl.to(
".thumb-two",
{
opacity: 0.3,
scale: 2,
y: "100%",
zIndex: -1,
duration: 2,
},
0
);
}
}, []);
return (
<section className="enterprise-banner position-relative overflow-hidden">
<div className="banner-background">
<div className="gradient-overlay"></div>
<div className="geometric-pattern"></div>
</div>
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="enterprise-banner__content">
<div className="banner-badge mb-4">
<span className="enterprise-badge">
Enterprise Services
</span>
</div>
<h1 className="enterprise-title mb-4">
The end-to-end bespoke software development company you need
</h1>
<p className="enterprise-description mb-5">
Empower your business with sophisticated tech solutions by GNX.
From ideation to implementation, with a team of experts right at your fingertips
</p>
<div className="enterprise-cta">
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
<span>Get Free Quote</span>
<i className="fa-solid fa-arrow-right ms-2"></i>
</Link>
<Link href="#scroll-to" className="btn btn-outline-light btn-lg mb-3">
<span>Explore Services</span>
<i className="fa-solid fa-arrow-down ms-2"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
<ul className="social">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
target="_blank"
aria-label="connect with us on linkedin"
>
<i className="fa-brands fa-linkedin-in"></i>
</Link>
</li>
<li>
<Link
href="https://github.com/gnxtech"
target="_blank"
aria-label="view our code on github"
>
<i className="fa-brands fa-github"></i>
</Link>
</li>
</ul>
<Link href="#scroll-to" className="scroll-to">
Scroll
<span className="arrow"></span>
</Link>
</section>
);
};
export default ServicesBanner;

View File

@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
const SmoothScroll = dynamic(() => import("../../shared/layout/animations/SmoothScroll"), {
ssr: false,
});
const ParallaxImage = dynamic(() => import("../../shared/layout/animations/ParallaxImage"), {
ssr: false,
});
const FadeImageBottom = dynamic(() => import("../../shared/layout/animations/FadeImageBottom"), {
ssr: false,
});
const ButtonHoverAnimation = dynamic(
() => import("../../shared/layout/animations/ButtonHoverAnimation"),
{
ssr: false,
}
);
const VanillaTiltHover = dynamic(
() => import("../../shared/layout/animations/VanillaTiltHover"),
{
ssr: false,
}
);
const SplitTextAnimations = dynamic(
() => import("../../shared/layout/animations/SplitTextAnimations"),
{
ssr: false,
}
);
const ScrollToElement = dynamic(() => import("../../shared/layout/animations/ScrollToElement"), {
ssr: false,
});
const AppearDown = dynamic(() => import("../../shared/layout/animations/AppearDown"), {
ssr: false,
});
const FadeAnimations = dynamic(() => import("../../shared/layout/animations/FadeAnimations"), {
ssr: false,
});
const ServicesInitAnimations = () => {
return (
<>
<SmoothScroll />
<ParallaxImage />
<FadeImageBottom />
<ButtonHoverAnimation />
<VanillaTiltHover />
<SplitTextAnimations />
<ScrollToElement />
<AppearDown />
<FadeAnimations />
</>
);
};
export default ServicesInitAnimations;

View File

@@ -0,0 +1,67 @@
"use client";
import { useState, useEffect, useRef } from "react";
const ServicesScrollProgressButton = () => {
useEffect(() => {
window.scroll(0, 0);
}, []);
const [scrollProgress, setScrollProgress] = useState(0);
const [isActive, setIsActive] = useState(false);
const scrollRef = useRef<HTMLButtonElement>(null);
const handleScroll = () => {
const totalHeight = document.body.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
setIsActive(window.scrollY > 50);
};
const handleProgressClick = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
useEffect(() => {
window.scrollTo(0, 0);
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<button
ref={scrollRef}
className={`progress-wrap ${isActive ? " active-progress" : " "}`}
onClick={handleProgressClick}
title="Go To Top"
>
<span></span>
<svg
className="progress-circle svg-content"
width="100%"
height="100%"
viewBox="-1 -1 102 102"
>
<path
d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98"
stroke="#3887FE"
strokeWidth="4"
fill="none"
style={{
strokeDasharray: "308.66px",
strokeDashoffset: `${308.66 - scrollProgress * 3.0866}px`,
}}
/>
</svg>
</button>
);
};
export default ServicesScrollProgressButton;

View File

@@ -0,0 +1,220 @@
"use client";
import { useEffect } from "react";
import Image from "next/legacy/image";
import gsap from "gsap";
import ScrollTrigger from "gsap/dist/ScrollTrigger";
import thumb from "@/public/images/transform-thumb.png";
import teamThumb from "@/public/images/team-thumb.png";
import { Service } from "@/lib/api/serviceService";
import { serviceUtils } from "@/lib/api/serviceService";
interface TransformProps {
service: Service;
}
const Transform = ({ service }: TransformProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
gsap.set(".foot-fade", {
x: -100,
opacity: 0,
});
ScrollTrigger.batch(".foot-fade", {
start: "-100px bottom",
onEnter: (elements) =>
gsap.to(elements, {
x: 0,
opacity: 1,
stagger: 0.3,
}),
});
}, []);
return (
<section className="enterprise-transform py-5" id="scroll-to">
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="row align-items-center mb-5">
<div className="col-12 col-lg-6">
<div className="transform__content">
<div className="section-header mb-4">
<span className="enterprise-section-tag">Transform Your Business</span>
<h2 className="enterprise-section-title mb-4">
Transform your business with {service.title}
</h2>
<p className="enterprise-section-description">
{service.description}
</p>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="transform__thumb">
<div className="enterprise-image-wrapper">
<Image
src={serviceUtils.getServiceImageUrl(service) || thumb}
className="enterprise-service-image"
alt={service.title}
width={600}
height={400}
/>
</div>
</div>
</div>
</div>
<div className="section-header text-center mb-4">
<span className="enterprise-section-tag">Why Choose Us</span>
<h2 className="enterprise-section-title mb-3">
{service.why_choose_description || `Why choose our ${service.title.toLowerCase()}?`}
</h2>
</div>
<div className="row g-5">
{service.features && service.features.length > 0 ? (
service.features.slice(0, 6).map((feature, index) => (
<div key={feature.id} className="col-12 col-md-6 col-lg-4">
<div className="benefit-item enterprise-section-card">
<div className="card-icon">
<i className={`fa-solid fa-${feature.icon || 'check'}`}></i>
</div>
<div className="card-content">
<h6 className="card-title">{feature.title}</h6>
<p className="card-description">
{feature.description}
</p>
</div>
</div>
</div>
))
) : (
// Fallback content if no features are available
<>
<div className="col-12 col-md-6 col-lg-4">
<div className="benefit-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-rocket"></i>
</div>
<div className="card-content">
<h6 className="card-title">Fast Delivery</h6>
<p className="card-description">
{service.duration ? `Delivered in ${service.duration}` : 'Quick turnaround times for your project needs'}
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="benefit-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-shield-halved"></i>
</div>
<div className="card-content">
<h6 className="card-title">Quality Assured</h6>
<p className="card-description">
Rigorous testing and quality control ensure reliable, robust solutions
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="benefit-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-headset"></i>
</div>
<div className="card-content">
<h6 className="card-title">Ongoing Support</h6>
<p className="card-description">
Comprehensive support and maintenance to ensure optimal performance
</p>
</div>
</div>
</div>
</>
)}
</div>
<div className="section-header text-center mb-4 mt-6">
<span className="enterprise-section-tag">Our Expertise</span>
<h2 className="enterprise-section-title mb-3">
Our Expertise
</h2>
{service.expertise_description && (
<p className="enterprise-section-description">
{service.expertise_description}
</p>
)}
</div>
<div className="row g-5">
{service.expertise_items && service.expertise_items.length > 0 ? (
service.expertise_items.slice(0, 3).map((expertise, index) => (
<div key={expertise.id} className="col-12 col-md-6 col-lg-4">
<div className="expertise-item enterprise-section-card">
<div className="card-icon">
<i className={`fa-solid fa-${expertise.icon || 'star'}`}></i>
</div>
<div className="card-content">
<h6 className="card-title">{expertise.title}</h6>
<p className="card-description">
{expertise.description}
</p>
</div>
</div>
</div>
))
) : (
// Fallback content if no expertise items are available
<>
<div className="col-12 col-md-6 col-lg-4">
<div className="expertise-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-users"></i>
</div>
<div className="card-content">
<h6 className="card-title">Expert Team</h6>
<p className="card-description">
Our experienced team specializes in {service.title.toLowerCase()} with years of industry expertise.
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="expertise-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-code"></i>
</div>
<div className="card-content">
<h6 className="card-title">Modern Technologies</h6>
<p className="card-description">
We use cutting-edge technologies and frameworks to deliver scalable, secure solutions.
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="expertise-item enterprise-section-card">
<div className="card-icon">
<i className="fa-solid fa-chart-line"></i>
</div>
<div className="card-content">
<h6 className="card-title">Proven Results</h6>
<p className="card-description">
We have successfully delivered solutions to numerous clients, helping them achieve their goals.
</p>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
</section>
);
};
export default Transform;

View File

@@ -0,0 +1,470 @@
"use client";
import { useState, FormEvent } from 'react';
import { createTicket, CreateTicketData } from '@/lib/api/supportService';
import { useTicketCategories } from '@/lib/hooks/useSupport';
interface CreateTicketFormProps {
onOpenStatusCheck?: () => void;
}
const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
const { categories, loading: categoriesLoading } = useTicketCategories();
const [formData, setFormData] = useState<CreateTicketData>({
title: '',
description: '',
ticket_type: 'general',
user_name: '',
user_email: '',
user_phone: '',
company: '',
category: undefined
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [ticketNumber, setTicketNumber] = useState<string>('');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: name === 'category' ? (value ? parseInt(value) : undefined) : value
}));
// Clear field error when user starts typing
if (fieldErrors[name]) {
setFieldErrors(prev => ({ ...prev, [name]: '' }));
}
};
const validateForm = () => {
const errors: Record<string, string> = {};
if (!formData.user_name.trim()) {
errors.user_name = 'Full name is required';
}
if (!formData.user_email.trim()) {
errors.user_email = 'Email address is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.user_email)) {
errors.user_email = 'Please enter a valid email address';
}
if (!formData.title.trim()) {
errors.title = 'Subject is required';
}
if (!formData.description.trim()) {
errors.description = 'Description is required';
} else if (formData.description.trim().length < 10) {
errors.description = 'Please provide a more detailed description (minimum 10 characters)';
}
setFieldErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setSubmitError(null);
setSubmitSuccess(false);
try {
const response = await createTicket(formData);
setTicketNumber(response.ticket_number);
setSubmitSuccess(true);
// Reset form
setFormData({
title: '',
description: '',
ticket_type: 'general',
user_name: '',
user_email: '',
user_phone: '',
company: '',
category: undefined
});
setFieldErrors({});
} catch (error: any) {
console.error('Ticket creation error:', error);
// Provide user-friendly error messages based on error type
let errorMessage = 'Failed to submit ticket. Please try again.';
if (error.message) {
if (error.message.includes('email')) {
errorMessage = 'There was an issue with your email address. Please check and try again.';
} else if (error.message.includes('network') || error.message.includes('fetch')) {
errorMessage = 'Network error. Please check your connection and try again.';
} else if (error.message.includes('validation')) {
errorMessage = 'Please check all required fields and try again.';
} else if (error.message.includes('server') || error.message.includes('500')) {
errorMessage = 'Server error. Our team has been notified. Please try again later.';
} else {
// Use the actual error message if it's user-friendly
errorMessage = error.message;
}
}
setSubmitError(errorMessage);
} finally {
setIsSubmitting(false);
}
};
if (submitSuccess) {
return (
<div className="ticket-success">
<div className="success-icon">
<i className="fa-solid fa-circle-check"></i>
</div>
<h3>Ticket Created Successfully!</h3>
<div className="ticket-number-container">
<div className="ticket-number-label">Your Ticket Number</div>
<div className="ticket-number-value">{ticketNumber}</div>
<button
className="btn-copy-ticket"
onClick={() => {
navigator.clipboard.writeText(ticketNumber);
const btn = document.querySelector('.btn-copy-ticket');
if (btn) {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
}
}}
>
Copy
</button>
</div>
<p className="ticket-info">
We've received your support request and will respond as soon as possible.
Please save your ticket number for future reference.
</p>
<div className="success-actions">
<button
className="btn btn-primary"
onClick={() => setSubmitSuccess(false)}
>
<i className="fa-solid fa-plus me-2"></i>
Submit Another Ticket
</button>
{onOpenStatusCheck && (
<button
className="btn btn-outline"
onClick={onOpenStatusCheck}
>
<i className="fa-solid fa-search me-2"></i>
Check Ticket Status
</button>
)}
</div>
</div>
);
}
const issueTypeIcons: Record<string, string> = {
general: 'fa-info-circle',
technical: 'fa-tools',
billing: 'fa-credit-card',
feature_request: 'fa-lightbulb',
bug_report: 'fa-bug',
account: 'fa-user-circle'
};
return (
<div className="create-ticket-form enterprise-form">
<div className="row justify-content-center">
<div className="col-12">
<div className="form-header-enterprise">
<div className="form-header-icon">
<i className="fa-solid fa-ticket"></i>
</div>
<h2>Submit a Support Ticket</h2>
<p>Fill out the form below and our dedicated support team will get back to you within 24 hours.</p>
<div className="info-banner">
<i className="fa-solid fa-shield-check"></i>
<div>
<strong>Secure & Confidential</strong>
<span>All tickets are encrypted and handled with enterprise-grade security standards.</span>
</div>
</div>
</div>
{submitError && (
<div className="alert-enterprise alert-error">
<i className="fa-solid fa-triangle-exclamation"></i>
<div>
<strong>Submission Error</strong>
<p>{submitError}</p>
</div>
</div>
)}
<form onSubmit={handleSubmit} className="support-form-enterprise">
{/* Personal Information */}
<div className="form-section">
<div className="section-header">
<i className="fa-solid fa-user"></i>
<h3>Personal Information</h3>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_name" className="form-label-enterprise">
<span>Full Name</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-user"></i>
<input
type="text"
id="user_name"
name="user_name"
value={formData.user_name}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.user_name ? 'error' : ''}`}
placeholder="Enter your full name"
/>
</div>
{fieldErrors.user_name && (
<span className="field-error">{fieldErrors.user_name}</span>
)}
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_email" className="form-label-enterprise">
<span>Email Address</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-envelope"></i>
<input
type="email"
id="user_email"
name="user_email"
value={formData.user_email}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.user_email ? 'error' : ''}`}
placeholder="your.email@company.com"
/>
</div>
{fieldErrors.user_email && (
<span className="field-error">{fieldErrors.user_email}</span>
)}
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="user_phone" className="form-label-enterprise">
<span>Phone Number</span>
<span className="optional-badge">Optional</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-phone"></i>
<input
type="tel"
id="user_phone"
name="user_phone"
value={formData.user_phone}
onChange={handleInputChange}
className="form-control-enterprise"
placeholder="+1 (555) 123-4567"
/>
</div>
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="company" className="form-label-enterprise">
<span>Company Name</span>
<span className="optional-badge">Optional</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-building"></i>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleInputChange}
className="form-control-enterprise"
placeholder="Your Company Inc."
/>
</div>
</div>
</div>
</div>
</div>
{/* Ticket Details */}
<div className="form-section">
<div className="section-header">
<i className="fa-solid fa-clipboard-list"></i>
<h3>Ticket Details</h3>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="ticket_type" className="form-label-enterprise">
<span>Issue Type</span>
<span className="required-badge">Required</span>
</label>
<div className="select-with-icon">
<i className={`fa-solid ${issueTypeIcons[formData.ticket_type] || 'fa-tag'}`}></i>
<select
id="ticket_type"
name="ticket_type"
value={formData.ticket_type}
onChange={handleInputChange}
required
className="form-control-enterprise select-enterprise"
>
<option value="general">General Inquiry</option>
<option value="technical">Technical Issue</option>
<option value="billing">Billing Question</option>
<option value="feature_request">Feature Request</option>
<option value="bug_report">Bug Report</option>
<option value="account">Account Issue</option>
</select>
</div>
</div>
</div>
<div className="col-md-6">
<div className="form-group-enterprise">
<label htmlFor="category" className="form-label-enterprise">
<span>Category</span>
<span className="optional-badge">Optional</span>
</label>
<div className="select-with-icon">
<i className="fa-solid fa-folder"></i>
<select
id="category"
name="category"
value={formData.category || ''}
onChange={handleInputChange}
className="form-control-enterprise select-enterprise"
disabled={categoriesLoading}
>
<option value="">Select a category</option>
{Array.isArray(categories) && categories.map(cat => (
<option key={cat.id} value={cat.id}>
{cat.name}
</option>
))}
</select>
</div>
</div>
</div>
<div className="col-12">
<div className="form-group-enterprise">
<label htmlFor="title" className="form-label-enterprise">
<span>Subject</span>
<span className="required-badge">Required</span>
</label>
<div className="input-with-icon">
<i className="fa-solid fa-heading"></i>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleInputChange}
required
className={`form-control-enterprise ${fieldErrors.title ? 'error' : ''}`}
placeholder="Brief, descriptive subject line"
/>
</div>
{fieldErrors.title && (
<span className="field-error">{fieldErrors.title}</span>
)}
</div>
</div>
<div className="col-12">
<div className="form-group-enterprise">
<label htmlFor="description" className="form-label-enterprise">
<span>Description</span>
<span className="required-badge">Required</span>
<span className="char-count">{formData.description.length} characters</span>
</label>
<div className="textarea-wrapper">
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
required
className={`form-control-enterprise textarea-enterprise ${fieldErrors.description ? 'error' : ''}`}
rows={6}
placeholder="Please provide detailed information about your issue, including any error messages, steps to reproduce, or relevant context..."
/>
<div className="textarea-footer">
<i className="fa-solid fa-lightbulb"></i>
<span>Tip: More details help us resolve your issue faster</span>
</div>
</div>
{fieldErrors.description && (
<span className="field-error">{fieldErrors.description}</span>
)}
</div>
</div>
</div>
</div>
{/* Submit Section */}
<div className="form-submit-section">
<button
type="submit"
className="btn-submit-enterprise"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<span className="spinner-enterprise"></span>
<span>Submitting Ticket...</span>
</>
) : (
<>
<i className="fa-solid fa-paper-plane"></i>
<span>Submit Ticket</span>
<i className="fa-solid fa-arrow-right"></i>
</>
)}
</button>
<p className="submit-note">
<i className="fa-solid fa-clock"></i>
Average response time: <strong>2-4 hours</strong>
</p>
</div>
</form>
</div>
</div>
</div>
);
};
export default CreateTicketForm;

View File

@@ -0,0 +1,217 @@
"use client";
import { useState } from 'react';
import { useKnowledgeBaseCategories, useFeaturedArticles, useKnowledgeBaseArticles } from '@/lib/hooks/useSupport';
import KnowledgeBaseArticleModal from './KnowledgeBaseArticleModal';
const KnowledgeBase = () => {
const { categories, loading: categoriesLoading } = useKnowledgeBaseCategories();
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedArticleSlug, setSelectedArticleSlug] = useState<string | null>(null);
// Fetch all articles (for browsing and category filtering)
const { articles: allArticles, loading: allArticlesLoading } = useKnowledgeBaseArticles();
// Fetch featured articles (for default view)
const { articles: featuredArticles, loading: featuredLoading } = useFeaturedArticles();
// Determine which articles to display
let displayArticles = featuredArticles;
let isLoading = featuredLoading;
let headerText = 'Featured Articles';
if (searchTerm) {
// If searching, filter all articles by search term
displayArticles = allArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
article.content.toLowerCase().includes(searchTerm.toLowerCase())
);
isLoading = allArticlesLoading;
headerText = 'Search Results';
} else if (selectedCategory) {
// If a category is selected, filter articles by that category
displayArticles = allArticles.filter(article => article.category_slug === selectedCategory);
isLoading = allArticlesLoading;
const categoryName = categories.find(cat => cat.slug === selectedCategory)?.name || 'Category';
headerText = `${categoryName} Articles`;
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
// The search is already being performed by the hook
};
const filteredCategories = selectedCategory
? categories.filter(cat => cat.slug === selectedCategory)
: categories;
return (
<div className="knowledge-base">
<div className="row justify-content-center">
<div className="col-12 col-lg-10">
<div className="form-header text-center">
<h2>Knowledge Base</h2>
<p>Find answers to frequently asked questions and explore our documentation.</p>
</div>
{/* Search Bar */}
<form onSubmit={handleSearch} className="kb-search-form">
<div className="search-input-group">
<i className="fa-solid fa-search search-icon"></i>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search articles, topics, or keywords..."
className="form-control"
/>
{searchTerm && (
<button
type="button"
className="clear-search"
onClick={() => setSearchTerm('')}
aria-label="Clear search"
>
<i className="fa-solid fa-times"></i>
</button>
)}
</div>
</form>
{/* Categories */}
{!searchTerm && (
<div className="kb-categories">
<h3>Browse by Category</h3>
{categoriesLoading ? (
<div className="loading-state">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<div className="row g-4">
{Array.isArray(categories) && categories.map(category => (
<div key={category.id} className="col-md-6 col-lg-4">
<div
className="category-card"
onClick={() => setSelectedCategory(category.slug)}
style={{ borderLeftColor: category.color }}
>
<div
className="category-icon"
style={{ color: category.color }}
>
<i className={`fa-solid ${category.icon}`}></i>
</div>
<div className="category-content">
<h4>{category.name}</h4>
<p>{category.description}</p>
<div className="category-meta">
<span className="article-count">
{category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
</span>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Featured/Search Results Articles */}
<div className="kb-articles">
<div className="articles-header">
<div className="d-flex align-items-center justify-content-between">
<h3>{headerText}</h3>
{selectedCategory && !searchTerm && (
<button
className="btn btn-outline-primary btn-sm"
onClick={() => setSelectedCategory(null)}
>
<i className="fa-solid fa-arrow-left me-2"></i>
Back to All Articles
</button>
)}
</div>
{searchTerm && (
<p className="search-info">
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
</p>
)}
</div>
{isLoading ? (
<div className="loading-state">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : displayArticles.length === 0 ? (
<div className="empty-state">
<i className="fa-solid fa-search empty-icon"></i>
<h4>No articles found</h4>
<p>
{searchTerm
? `We couldn't find any articles matching "${searchTerm}". Try different keywords.`
: 'No articles available at the moment.'}
</p>
</div>
) : (
<div className="articles-list">
{Array.isArray(displayArticles) && displayArticles.map(article => (
<div
key={article.id}
className="article-item"
onClick={() => setSelectedArticleSlug(article.slug)}
>
<div className="article-header">
<h4>{article.title}</h4>
{article.is_featured && (
<span className="featured-badge">
<i className="fa-solid fa-star me-1"></i>
Featured
</span>
)}
</div>
<p className="article-summary">{article.summary}</p>
<div className="article-meta">
<span className="article-category">
<i className="fa-solid fa-folder me-1"></i>
{article.category_name}
</span>
<span className="article-stats">
<i className="fa-solid fa-eye me-1"></i>
{article.view_count} views
</span>
<span className="article-stats">
<i className="fa-solid fa-thumbs-up me-1"></i>
{article.helpful_count} helpful
</span>
</div>
<button className="article-read-more">
Read More <i className="fa-solid fa-arrow-right ms-2"></i>
</button>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Article Modal */}
{selectedArticleSlug && (
<KnowledgeBaseArticleModal
slug={selectedArticleSlug}
onClose={() => setSelectedArticleSlug(null)}
/>
)}
</div>
);
};
export default KnowledgeBase;

View File

@@ -0,0 +1,138 @@
"use client";
import { useEffect, useState } from 'react';
import { useKnowledgeBaseArticle } from '@/lib/hooks/useSupport';
import { markArticleHelpful } from '@/lib/api/supportService';
interface KnowledgeBaseArticleModalProps {
slug: string;
onClose: () => void;
}
const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalProps) => {
const { article, loading, error } = useKnowledgeBaseArticle(slug);
const [feedbackGiven, setFeedbackGiven] = useState(false);
useEffect(() => {
// Prevent body scroll when modal is open
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = 'unset';
};
}, []);
const handleFeedback = async (helpful: boolean) => {
if (!article || feedbackGiven) return;
try {
await markArticleHelpful(slug, helpful);
setFeedbackGiven(true);
} catch (error) {
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
return (
<div className="kb-modal-overlay" onClick={onClose}>
<div className="kb-modal" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onClose} aria-label="Close modal">
<i className="fa-solid fa-times"></i>
</button>
<div className="modal-content">
{loading && (
<div className="loading-state">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p>Loading article...</p>
</div>
)}
{error && (
<div className="error-state">
<i className="fa-solid fa-triangle-exclamation"></i>
<h3>Error Loading Article</h3>
<p>{error}</p>
</div>
)}
{article && (
<>
<div className="article-header">
{article.is_featured && (
<span className="featured-badge">
<i className="fa-solid fa-star me-1"></i>
Featured
</span>
)}
<h2>{article.title}</h2>
<div className="article-meta">
<span className="meta-item">
<i className="fa-solid fa-folder me-1"></i>
{article.category_name}
</span>
<span className="meta-item">
<i className="fa-solid fa-calendar me-1"></i>
{formatDate(article.published_at || article.created_at)}
</span>
<span className="meta-item">
<i className="fa-solid fa-eye me-1"></i>
{article.view_count} views
</span>
</div>
</div>
<div className="article-body">
<div
className="article-content"
dangerouslySetInnerHTML={{ __html: article.content || article.summary }}
/>
</div>
<div className="article-footer">
<div className="article-feedback">
<h4>Was this article helpful?</h4>
{feedbackGiven ? (
<p className="feedback-thanks">
<i className="fa-solid fa-check-circle me-2"></i>
Thank you for your feedback!
</p>
) : (
<div className="feedback-buttons">
<button
className="btn btn-outline-success"
onClick={() => handleFeedback(true)}
>
<i className="fa-solid fa-thumbs-up me-2"></i>
Yes ({article.helpful_count})
</button>
<button
className="btn btn-outline-danger"
onClick={() => handleFeedback(false)}
>
<i className="fa-solid fa-thumbs-down me-2"></i>
No ({article.not_helpful_count})
</button>
</div>
)}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
};
export default KnowledgeBaseArticleModal;

View File

@@ -0,0 +1,174 @@
"use client";
import { useEffect } from 'react';
import CreateTicketForm from './CreateTicketForm';
import KnowledgeBase from './KnowledgeBase';
import TicketStatusCheck from './TicketStatusCheck';
type ModalType = 'create' | 'knowledge' | 'status' | null;
interface SupportCenterContentProps {
activeModal: ModalType;
onClose: () => void;
onOpenModal?: (type: ModalType) => void;
}
const SupportCenterContent = ({ activeModal, onClose, onOpenModal }: SupportCenterContentProps) => {
// Close modal on escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && activeModal) {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [activeModal, onClose]);
// Prevent body scroll when modal is open
useEffect(() => {
if (activeModal) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [activeModal]);
if (!activeModal) return null;
return (
<>
{/* Modal Overlay */}
<div
className="support-modal-overlay"
onClick={onClose}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
zIndex: 9998,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
backdropFilter: 'blur(5px)',
}}
>
{/* Modal Content */}
<div
className="support-modal-content"
onClick={(e) => e.stopPropagation()}
style={{
backgroundColor: '#fff',
borderRadius: '12px',
maxWidth: '1000px',
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
position: 'relative',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
animation: 'modalSlideIn 0.3s ease-out',
}}
>
{/* Close Button */}
<button
onClick={onClose}
className="support-modal-close"
aria-label="Close modal"
style={{
position: 'sticky',
top: '20px',
right: '20px',
float: 'right',
background: '#f3f4f6',
border: 'none',
borderRadius: '50%',
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontSize: '20px',
color: '#374151',
transition: 'all 0.2s',
zIndex: 10,
marginBottom: '-40px',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#e5e7eb';
e.currentTarget.style.transform = 'scale(1.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#f3f4f6';
e.currentTarget.style.transform = 'scale(1)';
}}
>
<i className="fa-solid fa-times"></i>
</button>
{/* Modal Body */}
<div className="support-modal-body" style={{ padding: '40px' }}>
{activeModal === 'create' && (
<CreateTicketForm onOpenStatusCheck={() => {
if (onOpenModal) {
onOpenModal('status');
}
}} />
)}
{activeModal === 'knowledge' && <KnowledgeBase />}
{activeModal === 'status' && <TicketStatusCheck />}
</div>
</div>
</div>
{/* Modal Animation Keyframes */}
<style jsx>{`
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.support-modal-content::-webkit-scrollbar {
width: 8px;
}
.support-modal-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.support-modal-content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.support-modal-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
@media (max-width: 768px) {
.support-modal-body {
padding: 20px !important;
}
}
`}</style>
</>
);
};
export default SupportCenterContent;

View File

@@ -0,0 +1,151 @@
"use client";
type ModalType = 'create' | 'knowledge' | 'status' | null;
interface SupportCenterHeroProps {
onFeatureClick: (type: ModalType) => void;
}
const SupportCenterHero = ({ onFeatureClick }: SupportCenterHeroProps) => {
return (
<section className="support-hero">
{/* Animated Background */}
<div className="hero-background">
{/* Floating Support Icons */}
<div className="floating-tech tech-1">
<i className="fa-solid fa-headset"></i>
</div>
<div className="floating-tech tech-2">
<i className="fa-solid fa-ticket"></i>
</div>
<div className="floating-tech tech-3">
<i className="fa-solid fa-book"></i>
</div>
<div className="floating-tech tech-4">
<i className="fa-solid fa-comments"></i>
</div>
<div className="floating-tech tech-5">
<i className="fa-solid fa-life-ring"></i>
</div>
<div className="floating-tech tech-6">
<i className="fa-solid fa-user-shield"></i>
</div>
{/* Grid Pattern */}
<div className="grid-overlay"></div>
{/* Animated Gradient Orbs */}
<div className="gradient-orb orb-1"></div>
<div className="gradient-orb orb-2"></div>
<div className="gradient-orb orb-3"></div>
{/* Video Overlay */}
<div className="video-overlay"></div>
</div>
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-lg-10 col-xl-8">
<div className="support-hero__content text-center">
<h1 className="support-hero__title">
Support Center
</h1>
<p className="support-hero__subtitle">
Get expert assistance whenever you need it. Our dedicated support team is here to help you succeed.
</p>
<div className="support-hero__features">
<div className="row g-3 g-md-4 g-lg-5 justify-content-center">
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('create')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('create')}
>
<div className="feature-icon">
<i className="fa-solid fa-ticket"></i>
</div>
<h3>Submit Tickets</h3>
<p>Create and track support requests</p>
</div>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('knowledge')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('knowledge')}
>
<div className="feature-icon">
<i className="fa-solid fa-book"></i>
</div>
<h3>Knowledge Base</h3>
<p>Find answers to common questions</p>
</div>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<div
className="feature-item clickable"
onClick={() => onFeatureClick('status')}
role="button"
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onFeatureClick('status')}
>
<div className="feature-icon">
<i className="fa-solid fa-search"></i>
</div>
<h3>Track Status</h3>
<p>Monitor your ticket progress</p>
</div>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=privacy"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-shield-halved"></i>
</div>
<h3>Privacy Policy</h3>
<p>Learn about data protection</p>
</a>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=terms"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-file-contract"></i>
</div>
<h3>Terms of Use</h3>
<p>Review our service terms</p>
</a>
</div>
<div className="col-12 col-sm-6 col-md-6 col-lg-4">
<a
href="/policy?type=support"
className="feature-item clickable link-item"
>
<div className="feature-icon">
<i className="fa-solid fa-headset"></i>
</div>
<h3>Support Policy</h3>
<p>Understand our support coverage</p>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default SupportCenterHero;

View File

@@ -0,0 +1,323 @@
"use client";
import { useState, FormEvent } from 'react';
import { checkTicketStatus, SupportTicket } from '@/lib/api/supportService';
const TicketStatusCheck = () => {
const [ticketNumber, setTicketNumber] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
const [ticket, setTicket] = useState<SupportTicket | null>(null);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSearching(true);
setSearchError(null);
setTicket(null);
try {
const response = await checkTicketStatus(ticketNumber);
setTicket(response);
} catch (error: any) {
if (error.message === 'Ticket not found') {
setSearchError('Ticket not found. Please check your ticket number and try again.');
} else {
setSearchError('An error occurred while searching. Please try again.');
}
} finally {
setIsSearching(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getRelativeTime = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
return formatDate(dateString);
};
const getStatusIcon = (statusName: string) => {
const status = statusName.toLowerCase();
if (status.includes('open') || status.includes('new')) return 'fa-inbox';
if (status.includes('progress') || status.includes('working')) return 'fa-spinner';
if (status.includes('pending') || status.includes('waiting')) return 'fa-clock';
if (status.includes('resolved') || status.includes('closed')) return 'fa-check-circle';
return 'fa-ticket';
};
const getPriorityIcon = (priorityName: string) => {
const priority = priorityName.toLowerCase();
if (priority.includes('urgent') || priority.includes('critical')) return 'fa-exclamation-triangle';
if (priority.includes('high')) return 'fa-arrow-up';
if (priority.includes('medium')) return 'fa-minus';
if (priority.includes('low')) return 'fa-arrow-down';
return 'fa-flag';
};
return (
<div className="ticket-status-check enterprise-status-check">
<div className="row justify-content-center">
<div className="col-12">
<div className="status-header-enterprise">
<div className="status-header-icon">
<i className="fa-solid fa-magnifying-glass"></i>
</div>
<h2>Check Ticket Status</h2>
<p>Track your support request in real-time with instant status updates</p>
</div>
<form onSubmit={handleSubmit} className="status-search-form-enterprise">
<div className="search-container">
<div className="search-input-wrapper">
<i className="fa-solid fa-ticket search-icon"></i>
<input
type="text"
value={ticketNumber}
onChange={(e) => setTicketNumber(e.target.value.toUpperCase())}
placeholder="TKT-YYYYMMDD-XXXXX"
required
className="search-input-enterprise"
pattern="TKT-\d{8}-[A-Z0-9]{5}"
title="Please enter a valid ticket number (e.g., TKT-20231015-ABCDE)"
/>
{ticketNumber && (
<button
type="button"
className="clear-btn"
onClick={() => {
setTicketNumber('');
setTicket(null);
setSearchError(null);
}}
>
<i className="fa-solid fa-times"></i>
</button>
)}
</div>
<button
type="submit"
className="btn-search-enterprise"
disabled={isSearching}
>
{isSearching ? (
<>
<span className="spinner-enterprise"></span>
<span>Searching...</span>
</>
) : (
<>
<i className="fa-solid fa-search"></i>
<span>Search</span>
</>
)}
</button>
</div>
<p className="search-hint">
<i className="fa-solid fa-info-circle"></i>
Enter your ticket number exactly as it appears in your confirmation email
</p>
</form>
{searchError && (
<div className="alert-enterprise alert-error">
<i className="fa-solid fa-exclamation-circle"></i>
<div>
<strong>Ticket Not Found</strong>
<p>{searchError}</p>
</div>
</div>
)}
{ticket && (
<div className="ticket-details-enterprise">
{/* Header Section */}
<div className="ticket-header-enterprise">
<div className="ticket-number-section">
<span className="ticket-label">Ticket Number</span>
<div className="ticket-number-display">
<i className="fa-solid fa-ticket"></i>
<span>{ticket.ticket_number}</span>
<button
className="btn-copy-inline"
onClick={() => {
navigator.clipboard.writeText(ticket.ticket_number);
const btn = document.querySelector('.btn-copy-inline');
if (btn) {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
setTimeout(() => {
btn.innerHTML = originalHTML;
}, 2000);
}
}}
>
<i className="fa-solid fa-copy"></i>
</button>
</div>
</div>
<div
className="status-badge-enterprise"
style={{
backgroundColor: ticket.status_color || '#6366f1',
boxShadow: `0 0 20px ${ticket.status_color}33`
}}
>
<i className={`fa-solid ${getStatusIcon(ticket.status_name)}`}></i>
<span>{ticket.status_name}</span>
</div>
</div>
{/* Info Cards Section */}
<div className="ticket-info-cards">
<div className="info-card">
<i className="fa-solid fa-calendar-plus"></i>
<div>
<span className="card-label">Created</span>
<span className="card-value">{getRelativeTime(ticket.created_at)}</span>
</div>
</div>
<div className="info-card">
<i className="fa-solid fa-clock"></i>
<div>
<span className="card-label">Last Updated</span>
<span className="card-value">{getRelativeTime(ticket.updated_at)}</span>
</div>
</div>
{ticket.priority_name && (
<div className="info-card">
<i className={`fa-solid ${getPriorityIcon(ticket.priority_name)}`}></i>
<div>
<span className="card-label">Priority</span>
<span
className="card-value priority-value"
style={{ color: ticket.priority_color }}
>
{ticket.priority_name}
</span>
</div>
</div>
)}
{ticket.category_name && (
<div className="info-card">
<i className="fa-solid fa-folder-open"></i>
<div>
<span className="card-label">Category</span>
<span className="card-value">{ticket.category_name}</span>
</div>
</div>
)}
</div>
{/* Main Content */}
<div className="ticket-content-section">
<div className="content-header">
<h3>{ticket.title}</h3>
<div className="content-meta">
<span><i className="fa-solid fa-user"></i> {ticket.user_name}</span>
{ticket.company && (
<span><i className="fa-solid fa-building"></i> {ticket.company}</span>
)}
</div>
</div>
<div className="description-section">
<h4><i className="fa-solid fa-align-left"></i> Description</h4>
<p>{ticket.description}</p>
</div>
{/* Messages Section */}
{ticket.messages && ticket.messages.filter(msg => !msg.is_internal).length > 0 && (
<div className="messages-section-enterprise">
<h4>
<i className="fa-solid fa-comments"></i>
Conversation
<span className="count-badge">{ticket.messages.filter(msg => !msg.is_internal).length}</span>
</h4>
<div className="messages-list-enterprise">
{ticket.messages
.filter(msg => !msg.is_internal)
.map((message) => (
<div key={message.id} className="message-card-enterprise">
<div className="message-avatar">
<i className="fa-solid fa-user"></i>
</div>
<div className="message-content-wrapper">
<div className="message-header-enterprise">
<span className="message-author">{message.author_name || message.author_email}</span>
<span className="message-time">{getRelativeTime(message.created_at)}</span>
</div>
<div className="message-text">
{message.content}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Activity Timeline */}
{ticket.activities && ticket.activities.length > 0 && (
<div className="timeline-section-enterprise">
<h4>
<i className="fa-solid fa-list-check"></i>
Activity Timeline
</h4>
<div className="timeline-items">
{ticket.activities.slice(0, 5).map((activity, index) => (
<div key={activity.id} className="timeline-item-enterprise">
<div className="timeline-marker">
<i className="fa-solid fa-circle"></i>
</div>
<div className="timeline-content-wrapper">
<div className="timeline-text">{activity.description}</div>
<div className="timeline-time">{getRelativeTime(activity.created_at)}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
{/* Footer Actions */}
<div className="ticket-footer-enterprise">
<div className="footer-info">
<i className="fa-solid fa-shield-check"></i>
<span>This ticket is securely tracked and monitored by our support team</span>
</div>
<button
className="btn-refresh"
onClick={() => handleSubmit({ preventDefault: () => {} } as FormEvent<HTMLFormElement>)}
>
<i className="fa-solid fa-rotate"></i>
Refresh Status
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TicketStatusCheck;

View File

@@ -0,0 +1,370 @@
"use client";
import { useEffect } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Image from "next/legacy/image";
import Link from "next/link";
import { Service } from "@/lib/api/serviceService";
import { serviceUtils } from "@/lib/api/serviceService";
interface ServiceDetailsBannerProps {
service: Service;
}
const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
if (document.querySelector(".service-banner")) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".service-banner",
start: "center center",
end: "+=100%",
scrub: true,
pin: false,
},
});
tl.to(".thumb-one", {
opacity: 0.3,
y: "-100%",
zIndex: -1,
duration: 2,
});
tl.to(
".thumb-two",
{
opacity: 0.3,
scale: 2,
y: "100%",
zIndex: -1,
duration: 2,
},
0
);
}
}, []);
return (
<section className="enterprise-banner position-relative overflow-hidden">
<div className="banner-background">
<div className="gradient-orb orb-1"></div>
<div className="gradient-orb orb-2"></div>
<div className="gradient-orb orb-3"></div>
{/* Service-Specific Background Elements */}
<div className="enterprise-bg-elements">
{/* Flying Service Code Elements */}
<div className="flying-code">
<div className="code-snippet code-1">
<span className="code-line">const service = {'{'}</span>
<span className="code-line"> name: &apos;{service.title}&apos;,</span>
<span className="code-line"> category: &apos;{service.category?.name}&apos;</span>
<span className="code-line">{'}'};</span>
</div>
<div className="code-snippet code-2">
<span className="code-line">if (service.featured) {'{'}</span>
<span className="code-line"> deploy.premium();</span>
<span className="code-line">{'}'}</span>
</div>
<div className="code-snippet code-3">
<span className="code-line">class ServiceDelivery {'{'}</span>
<span className="code-line"> constructor() {'{'}</span>
<span className="code-line"> this.quality = &apos;enterprise&apos;;</span>
<span className="code-line"> {'}'}</span>
<span className="code-line">{'}'}</span>
</div>
<div className="code-snippet code-4">
<span className="code-line">API.deliver({'{'}</span>
<span className="code-line"> service: &apos;{service.title}&apos;,</span>
<span className="code-line"> duration: &apos;{service.duration}&apos;</span>
<span className="code-line">{'}'});</span>
</div>
</div>
{/* Industrial Grid */}
<div className="industrial-grid">
<div className="grid-line horizontal h-1"></div>
<div className="grid-line horizontal h-2"></div>
<div className="grid-line horizontal h-3"></div>
<div className="grid-line horizontal h-4"></div>
<div className="grid-line vertical v-1"></div>
<div className="grid-line vertical v-2"></div>
<div className="grid-line vertical v-3"></div>
<div className="grid-line vertical v-4"></div>
</div>
{/* Service Elements */}
<div className="security-elements">
<div className="shield shield-1">
<i className="fa-solid fa-cogs"></i>
</div>
<div className="shield shield-2">
<i className="fa-solid fa-rocket"></i>
</div>
<div className="shield shield-3">
<i className="fa-solid fa-chart-line"></i>
</div>
<div className="shield shield-4">
<i className="fa-solid fa-users"></i>
</div>
</div>
{/* Circuit Patterns */}
<div className="circuit-patterns">
<div className="circuit circuit-1">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
<div className="circuit circuit-2">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
<div className="circuit circuit-3">
<div className="circuit-node"></div>
<div className="circuit-line"></div>
<div className="circuit-node"></div>
</div>
</div>
{/* Data Streams */}
<div className="data-streams">
<div className="stream stream-1">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
<div className="stream stream-2">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
<div className="stream stream-3">
<div className="data-bit"></div>
<div className="data-bit"></div>
<div className="data-bit"></div>
</div>
</div>
{/* Service Request/Response Data */}
<div className="request-response-data">
<div className="api-request req-1">
<div className="request-label">POST /api/services</div>
<div className="request-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-response resp-1">
<div className="response-label">200 OK</div>
<div className="response-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-request req-2">
<div className="request-label">GET /api/delivery</div>
<div className="request-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
<div className="api-response resp-2">
<div className="response-label">201 Created</div>
<div className="response-data">
<div className="data-packet"></div>
<div className="data-packet"></div>
<div className="data-packet"></div>
</div>
</div>
</div>
{/* Service Data Generation */}
<div className="space-data-generation">
<div className="data-cluster cluster-1">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-2">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-3">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
<div className="data-cluster cluster-4">
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
<div className="data-point"></div>
</div>
</div>
{/* Service Connections */}
<div className="database-connections">
<div className="db-connection conn-1">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-cogs"></i>
</div>
<div className="connection-pulse"></div>
</div>
<div className="db-connection conn-2">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-rocket"></i>
</div>
<div className="connection-pulse"></div>
</div>
<div className="db-connection conn-3">
<div className="connection-line"></div>
<div className="db-node">
<i className="fa-solid fa-chart-line"></i>
</div>
<div className="connection-pulse"></div>
</div>
</div>
{/* Service Metrics */}
<div className="real-time-metrics">
<div className="metric metric-1">
<div className="metric-label">Service Uptime</div>
<div className="metric-value">99.9%</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
<div className="metric metric-2">
<div className="metric-label">Response Time</div>
<div className="metric-value">45ms</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
<div className="metric metric-3">
<div className="metric-label">Client Satisfaction</div>
<div className="metric-value">98%</div>
<div className="metric-bar">
<div className="bar-fill"></div>
</div>
</div>
</div>
</div>
</div>
<div className="container">
<div className="row justify-content-center">
<div className="col-12 col-xl-10">
<div className="enterprise-banner__content">
<div className="banner-badge mb-4">
<span className="enterprise-badge">
{service.category?.name || 'Professional Service'}
</span>
</div>
<h1 className="enterprise-title mb-4">
{service.title}
</h1>
<p className="enterprise-description mb-5">
{service.description}
</p>
<div className="enterprise-highlights mb-5">
<div className="row justify-content-center g-4">
{service.duration && (
<div className="col-auto">
<div className="highlight-card">
<div className="highlight-icon">
<i className="fa-solid fa-clock"></i>
</div>
<div className="highlight-content">
<span className="highlight-label">Duration</span>
<span className="highlight-value">{service.duration}</span>
</div>
</div>
</div>
)}
{service.featured && (
<div className="col-auto">
<div className="highlight-card featured">
<div className="highlight-icon">
<i className="fa-solid fa-star"></i>
</div>
<div className="highlight-content">
<span className="highlight-label">Premium</span>
<span className="highlight-value">Featured Service</span>
</div>
</div>
</div>
)}
</div>
</div>
<div className="enterprise-cta">
<Link href="/contact-us" className="btn btn-primary btn-lg me-3 mb-3">
<span>Get Free Quote</span>
<i className="fa-solid fa-arrow-right ms-2"></i>
</Link>
<Link href="#service-details" className="btn btn-outline-light btn-lg mb-3">
<span>Learn More</span>
<i className="fa-solid fa-arrow-down ms-2"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
<ul className="social">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
target="_blank"
aria-label="connect with us on linkedin"
>
<i className="fa-brands fa-linkedin-in"></i>
</Link>
</li>
<li>
<Link
href="https://github.com/gnxtech"
target="_blank"
aria-label="view our code on github"
>
<i className="fa-brands fa-github"></i>
</Link>
</li>
</ul>
<Link href="#scroll-to" className="scroll-to">
Scroll
<span className="arrow"></span>
</Link>
</section>
);
};
export default ServiceDetailsBanner;

View File

@@ -0,0 +1,387 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useCookieConsent, CookiePreferences } from './CookieConsentContext';
// Cookie type definitions
interface CookieType {
id: keyof CookiePreferences;
name: string;
description: string;
required: boolean;
icon: string;
}
const cookieTypes: CookieType[] = [
{
id: 'necessary',
name: 'Necessary Cookies',
description: 'Essential cookies required for the website to function properly. These cannot be disabled.',
required: true,
icon: 'fas fa-shield-alt',
},
{
id: 'functional',
name: 'Functional Cookies',
description: 'These cookies enable enhanced functionality and personalization, such as remembering your preferences.',
required: false,
icon: 'fas fa-cogs',
},
];
// Main Cookie Consent Banner Component
export const CookieConsentBanner: React.FC = () => {
const { state, config, acceptAll, acceptNecessary, showSettings } = useCookieConsent();
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (state.showBanner) {
// Small delay to ensure smooth animation
const timer = setTimeout(() => setIsVisible(true), 100);
return () => clearTimeout(timer);
} else {
setIsVisible(false);
}
}, [state.showBanner]);
if (!state.showBanner || !isVisible) return null;
return (
<AnimatePresence>
{/* Fullscreen overlay to center the banner */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
style={{
position: 'fixed',
inset: 0,
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(17, 24, 39, 0.45)',
backdropFilter: 'blur(4px)',
}}
>
{/* Centered enterprise-style card */}
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.98, y: 8 }}
transition={{ duration: 0.25, ease: 'easeOut' }}
className="cookie-consent-banner"
style={{
width: 'min(680px, 92vw)',
background: '#0b1220',
color: '#e5e7eb',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 16,
boxShadow: '0 25px 70px rgba(0,0,0,0.45)',
padding: 24,
}}
>
<div className="cookie-consent-banner__container" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="cookie-consent-banner__content" style={{ display: 'flex', gap: 16 }}>
<div
className="cookie-consent-banner__icon"
style={{
width: 48,
height: 48,
borderRadius: 12,
display: 'grid',
placeItems: 'center',
background: 'linear-gradient(135deg, rgba(199, 213, 236, 0.39), rgba(147,197,253,0.08))',
color: '#93c5fd',
flex: '0 0 auto',
}}
>
<i className="fas fa-cookie-bite"></i>
</div>
<div className="cookie-consent-banner__text" style={{ display: 'grid', gap: 6 }}>
<h3 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>Cookie Preferences</h3>
<p style={{ margin: 0, lineHeight: 1.6, color: '#ffffff' }}>
We use only essential functional cookies to ensure our website works properly. We do not collect
personal data or use tracking cookies. Your privacy is important to us.
</p>
{config.showPrivacyNotice && (
<div className="cookie-consent-banner__links" style={{ marginTop: 6 }}>
<a
href={config.privacyPolicyUrl}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#93c5fd', textDecoration: 'underline' }}
>
Privacy Policy
</a>
</div>
)}
</div>
</div>
<div
className="cookie-consent-banner__actions"
style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 8 }}
>
<button
type="button"
className="cookie-consent-banner__btn cookie-consent-banner__btn--secondary"
onClick={showSettings}
style={{
padding: '10px 14px',
borderRadius: 10,
border: '1px solid rgba(255,255,255,0.12)',
background: 'transparent',
color: '#e5e7eb',
fontWeight: 600,
}}
>
<i className="fas fa-cog" style={{ marginRight: 8 }}></i>
Settings
</button>
<button
type="button"
className="cookie-consent-banner__btn cookie-consent-banner__btn--primary"
onClick={acceptNecessary}
style={{
padding: '10px 14px',
borderRadius: 10,
border: '1px solid rgba(59,130,246,0.35)',
background: 'linear-gradient(135deg, rgba(59,130,246,0.25), rgba(37,99,235,0.35))',
color: '#ffffff',
fontWeight: 700,
}}
>
<i className="fas fa-check" style={{ marginRight: 8 }}></i>
Accept Functional Only
</button>
</div>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
// Cookie Settings Modal Component
export const CookieSettingsModal: React.FC = () => {
const { state, config, hideSettings, acceptSelected, updatePreferences, withdrawConsent, exportConsentData } = useCookieConsent();
const [tempPreferences, setTempPreferences] = useState<CookiePreferences>(state.preferences);
useEffect(() => {
setTempPreferences(state.preferences);
}, [state.preferences]);
const handlePreferenceChange = (type: keyof CookiePreferences, value: boolean) => {
if (type === 'necessary') return; // Cannot change necessary cookies
setTempPreferences(prev => ({
...prev,
[type]: value,
}));
};
const handleSavePreferences = () => {
acceptSelected(tempPreferences);
};
const handleAcceptAll = () => {
const allAccepted: CookiePreferences = {
necessary: true,
functional: true,
};
acceptSelected(allAccepted);
};
if (!state.showSettings) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="cookie-settings-overlay"
onClick={hideSettings}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ duration: 0.2 }}
className="cookie-settings-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="cookie-settings-modal__header">
<div className="cookie-settings-modal__header-content">
<h2>Cookie Preferences</h2>
<p className="cookie-settings-modal__version">v{config.version}</p>
</div>
<button
type="button"
className="cookie-settings-modal__close"
onClick={hideSettings}
aria-label="Close settings"
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="cookie-settings-modal__content">
<div className="cookie-settings-modal__description">
<p>
We respect your privacy and only use essential functional cookies.
You can choose which types of cookies to allow below.
</p>
</div>
<div className="cookie-settings-modal__types">
{cookieTypes.map((cookieType) => (
<div
key={cookieType.id}
className={`cookie-settings-modal__type ${
cookieType.required ? 'cookie-settings-modal__type--required' : ''
}`}
>
<div className="cookie-settings-modal__type-header">
<div className="cookie-settings-modal__type-info">
<i className={cookieType.icon}></i>
<div>
<h4>{cookieType.name}</h4>
<p>{cookieType.description}</p>
</div>
</div>
<div className="cookie-settings-modal__type-toggle">
<label className="cookie-toggle">
<input
type="checkbox"
checked={tempPreferences[cookieType.id]}
onChange={(e) => handlePreferenceChange(cookieType.id, e.target.checked)}
disabled={cookieType.required}
/>
<span className="cookie-toggle__slider"></span>
</label>
</div>
</div>
</div>
))}
</div>
<div className="cookie-settings-modal__privacy">
<h4>Privacy Information</h4>
<ul>
<li>We do not collect personal data without your explicit consent</li>
<li>Functional cookies are used only to maintain website functionality</li>
<li>We do not use tracking, analytics, or marketing cookies</li>
<li>You can change your preferences at any time</li>
<li>Data retention period: {config.retentionPeriod} days</li>
<li>Contact: <a href={`mailto:${config.dataControllerEmail}`}>{config.dataControllerEmail}</a></li>
</ul>
</div>
{config.enableDetailedSettings && (
<div className="cookie-settings-modal__enterprise">
<h4>Enterprise Features</h4>
<div className="cookie-settings-modal__enterprise-actions">
<button
type="button"
className="cookie-settings-modal__btn cookie-settings-modal__btn--outline"
onClick={() => {
const data = exportConsentData();
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cookie-consent-data-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
}}
>
<i className="fas fa-download"></i>
Export Data
</button>
<button
type="button"
className="cookie-settings-modal__btn cookie-settings-modal__btn--danger"
onClick={() => {
if (confirm('Are you sure you want to withdraw your consent? This will reset all preferences.')) {
withdrawConsent();
}
}}
>
<i className="fas fa-user-times"></i>
Withdraw Consent
</button>
</div>
</div>
)}
</div>
<div className="cookie-settings-modal__footer">
<button
type="button"
className="cookie-settings-modal__btn cookie-settings-modal__btn--secondary"
onClick={hideSettings}
>
Cancel
</button>
<button
type="button"
className="cookie-settings-modal__btn cookie-settings-modal__btn--primary"
onClick={handleSavePreferences}
>
Save Preferences
</button>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
// Main Cookie Consent Component that combines both banner and modal
export const CookieConsent: React.FC = () => {
return (
<>
<CookieConsentBanner />
<CookieSettingsModal />
</>
);
};
// Cookie Preferences Display Component (for footer or privacy page)
export const CookiePreferencesDisplay: React.FC = () => {
const { state, showSettings, resetConsent } = useCookieConsent();
return (
<div className="cookie-preferences-display">
<h3>Cookie Preferences</h3>
<div className="cookie-preferences-display__status">
<p>
<strong>Status:</strong> {state.hasConsented ? 'Consent Given' : 'No Consent'}
</p>
<p>
<strong>Functional Cookies:</strong> {state.preferences.functional ? 'Enabled' : 'Disabled'}
</p>
</div>
<div className="cookie-preferences-display__actions">
<button
type="button"
className="cookie-preferences-display__btn"
onClick={showSettings}
>
Update Preferences
</button>
<button
type="button"
className="cookie-preferences-display__btn cookie-preferences-display__btn--reset"
onClick={resetConsent}
>
Reset Consent
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,413 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
// Types for cookie consent
export interface CookiePreferences {
necessary: boolean;
functional: boolean;
}
export interface ConsentAuditLog {
timestamp: string;
action: 'consent_given' | 'consent_updated' | 'consent_withdrawn' | 'settings_opened';
preferences: CookiePreferences;
userAgent: string;
ipAddress?: string;
sessionId: string;
}
export interface CookieConsentConfig {
version: string;
companyName: string;
privacyPolicyUrl: string;
cookiePolicyUrl: string;
dataControllerEmail: string;
retentionPeriod: number; // days
enableAuditLog: boolean;
enableDetailedSettings: boolean;
showPrivacyNotice: boolean;
}
export interface CookieConsentState {
hasConsented: boolean;
preferences: CookiePreferences;
showBanner: boolean;
showSettings: boolean;
consentDate?: string;
lastUpdated?: string;
auditLog: ConsentAuditLog[];
}
export interface CookieConsentContextType {
state: CookieConsentState;
config: CookieConsentConfig;
acceptAll: () => void;
acceptNecessary: () => void;
acceptSelected: (preferences: Partial<CookiePreferences>) => void;
showSettings: () => void;
hideSettings: () => void;
updatePreferences: (preferences: Partial<CookiePreferences>) => void;
resetConsent: () => void;
withdrawConsent: () => void;
exportConsentData: () => string;
getConsentSummary: () => any;
}
// Default cookie preferences
const defaultPreferences: CookiePreferences = {
necessary: true, // Always true, cannot be disabled
functional: false,
};
// Enterprise configuration
const defaultConfig: CookieConsentConfig = {
version: '2.0',
companyName: 'Your Company Name',
privacyPolicyUrl: '/policy?type=privacy',
cookiePolicyUrl: '/policy?type=privacy',
dataControllerEmail: 'privacy@yourcompany.com',
retentionPeriod: 365, // 1 year
enableAuditLog: true,
enableDetailedSettings: true,
showPrivacyNotice: true,
};
// Default state
const defaultState: CookieConsentState = {
hasConsented: false,
preferences: defaultPreferences,
showBanner: false,
showSettings: false,
auditLog: [],
};
// Create context
const CookieConsentContext = createContext<CookieConsentContextType | undefined>(undefined);
// Storage keys
const CONSENT_STORAGE_KEY = 'cookie-consent-preferences';
const CONSENT_VERSION = '2.0';
// Utility functions
const generateSessionId = (): string => {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
};
const createAuditLogEntry = (
action: ConsentAuditLog['action'],
preferences: CookiePreferences
): ConsentAuditLog => {
return {
timestamp: new Date().toISOString(),
action,
preferences,
userAgent: typeof window !== 'undefined' ? window.navigator.userAgent : '',
sessionId: generateSessionId(),
};
};
// Provider component
export const CookieConsentProvider: React.FC<{
children: ReactNode;
config?: Partial<CookieConsentConfig>;
}> = ({ children, config: customConfig }) => {
const [state, setState] = useState<CookieConsentState>(defaultState);
const config = { ...defaultConfig, ...customConfig };
// Load saved preferences on mount
useEffect(() => {
const loadSavedPreferences = () => {
try {
const saved = localStorage.getItem(CONSENT_STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
// Check if saved version matches current version
if (parsed.version === CONSENT_VERSION) {
setState({
hasConsented: true,
preferences: parsed.preferences,
showBanner: false,
showSettings: false,
consentDate: parsed.consentDate,
lastUpdated: parsed.lastUpdated,
auditLog: parsed.auditLog || [],
});
return;
}
}
} catch (error) {
}
// Show banner if no valid consent found
setState(prev => ({ ...prev, showBanner: true }));
};
loadSavedPreferences();
}, []);
// Save preferences to localStorage with audit logging
const savePreferences = (preferences: CookiePreferences, action: ConsentAuditLog['action'] = 'consent_given') => {
try {
const auditEntry = config.enableAuditLog ? createAuditLogEntry(action, preferences) : null;
const data = {
version: CONSENT_VERSION,
preferences,
timestamp: new Date().toISOString(),
consentDate: state.consentDate || new Date().toISOString(),
lastUpdated: new Date().toISOString(),
auditLog: auditEntry ? [...state.auditLog, auditEntry] : state.auditLog,
};
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(data));
// Update state with audit log
if (auditEntry) {
setState(prev => ({
...prev,
auditLog: [...prev.auditLog, auditEntry],
}));
}
} catch (error) {
}
};
// Accept all cookies
const acceptAll = () => {
const allAccepted: CookiePreferences = {
necessary: true,
functional: true,
};
setState(prev => ({
...prev,
hasConsented: true,
preferences: allAccepted,
showBanner: false,
showSettings: false,
consentDate: prev.consentDate || new Date().toISOString(),
lastUpdated: new Date().toISOString(),
}));
savePreferences(allAccepted, 'consent_given');
};
// Accept only necessary cookies
const acceptNecessary = () => {
const necessaryOnly: CookiePreferences = {
necessary: true,
functional: false,
};
setState(prev => ({
...prev,
hasConsented: true,
preferences: necessaryOnly,
showBanner: false,
showSettings: false,
consentDate: prev.consentDate || new Date().toISOString(),
lastUpdated: new Date().toISOString(),
}));
savePreferences(necessaryOnly, 'consent_given');
};
// Accept selected cookies
const acceptSelected = (preferences: Partial<CookiePreferences>) => {
const newPreferences: CookiePreferences = {
necessary: true, // Always true
functional: preferences.functional ?? false,
};
setState(prev => ({
...prev,
hasConsented: true,
preferences: newPreferences,
showBanner: false,
showSettings: false,
consentDate: prev.consentDate || new Date().toISOString(),
lastUpdated: new Date().toISOString(),
}));
savePreferences(newPreferences, 'consent_updated');
};
// Show settings modal
const showSettings = () => {
setState(prev => ({ ...prev, showSettings: true }));
// Log settings opened
if (config.enableAuditLog) {
const auditEntry = createAuditLogEntry('settings_opened', state.preferences);
setState(prev => ({
...prev,
auditLog: [...prev.auditLog, auditEntry],
}));
}
};
// Hide settings modal
const hideSettings = () => {
setState(prev => ({ ...prev, showSettings: false }));
};
// Update preferences (for settings modal)
const updatePreferences = (preferences: Partial<CookiePreferences>) => {
setState(prev => ({
...prev,
preferences: {
...prev.preferences,
...preferences,
necessary: true, // Always keep necessary as true
},
}));
};
// Reset consent (for testing or user request)
const resetConsent = () => {
try {
localStorage.removeItem(CONSENT_STORAGE_KEY);
} catch (error) {
}
setState({
hasConsented: false,
preferences: defaultPreferences,
showBanner: true,
showSettings: false,
auditLog: [],
});
};
// Withdraw consent (GDPR compliance)
const withdrawConsent = () => {
try {
localStorage.removeItem(CONSENT_STORAGE_KEY);
} catch (error) {
}
const auditEntry = config.enableAuditLog ? createAuditLogEntry('consent_withdrawn', defaultPreferences) : null;
setState({
hasConsented: false,
preferences: defaultPreferences,
showBanner: true,
showSettings: false,
auditLog: auditEntry ? [...state.auditLog, auditEntry] : state.auditLog,
});
};
// Export consent data (GDPR compliance)
const exportConsentData = (): string => {
const exportData = {
consentData: {
hasConsented: state.hasConsented,
preferences: state.preferences,
consentDate: state.consentDate,
lastUpdated: state.lastUpdated,
auditLog: state.auditLog,
},
config: {
version: config.version,
companyName: config.companyName,
retentionPeriod: config.retentionPeriod,
},
exportDate: new Date().toISOString(),
};
return JSON.stringify(exportData, null, 2);
};
// Get consent summary
const getConsentSummary = () => {
return {
hasConsented: state.hasConsented,
preferences: state.preferences,
consentDate: state.consentDate,
lastUpdated: state.lastUpdated,
auditLogCount: state.auditLog.length,
config: {
version: config.version,
companyName: config.companyName,
retentionPeriod: config.retentionPeriod,
},
};
};
const contextValue: CookieConsentContextType = {
state,
config,
acceptAll,
acceptNecessary,
acceptSelected,
showSettings,
hideSettings,
updatePreferences,
resetConsent,
withdrawConsent,
exportConsentData,
getConsentSummary,
};
return (
<CookieConsentContext.Provider value={contextValue}>
{children}
</CookieConsentContext.Provider>
);
};
// Hook to use cookie consent context
export const useCookieConsent = (): CookieConsentContextType => {
const context = useContext(CookieConsentContext);
if (context === undefined) {
throw new Error('useCookieConsent must be used within a CookieConsentProvider');
}
return context;
};
// Hook to check if specific cookie type is allowed
export const useCookiePermission = (type: keyof CookiePreferences): boolean => {
const { state } = useCookieConsent();
return state.preferences[type];
};
// Hook for functional features (only runs if functional cookies are allowed)
export const useFunctional = () => {
const { state } = useCookieConsent();
const isEnabled = state.preferences.functional && state.hasConsented;
const saveUserPreference = (key: string, value: any) => {
if (isEnabled && typeof window !== 'undefined') {
try {
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
} catch (error) {
}
}
};
const loadUserPreference = (key: string, defaultValue?: any) => {
if (isEnabled && typeof window !== 'undefined') {
try {
const saved = localStorage.getItem(`user-preference-${key}`);
return saved ? JSON.parse(saved) : defaultValue;
} catch (error) {
return defaultValue;
}
}
return defaultValue;
};
const rememberUserAction = (action: string, data?: any) => {
if (isEnabled) {
// Implement your user action tracking logic here
}
};
return {
saveUserPreference,
loadUserPreference,
rememberUserAction,
isEnabled,
};
};

View File

@@ -0,0 +1,97 @@
'use client';
import React, { useEffect } from 'react';
import { useCookieConsent, useCookiePermission } from './CookieConsentContext';
// Utility hook for conditional rendering based on cookie permissions
export const useConditionalFeature = (featureType: 'functional') => {
const isAllowed = useCookiePermission(featureType);
const { state } = useCookieConsent();
return {
isAllowed,
hasConsented: state.hasConsented,
canShow: isAllowed && state.hasConsented,
};
};
// Note: Analytics and marketing hooks removed as we don't collect this data
// Hook for functional features (only runs if functional cookies are allowed)
export const useFunctional = () => {
const { canShow } = useConditionalFeature('functional');
const saveUserPreference = (key: string, value: any) => {
if (canShow && typeof window !== 'undefined') {
try {
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
} catch (error) {
}
}
};
const loadUserPreference = (key: string, defaultValue?: any) => {
if (canShow && typeof window !== 'undefined') {
try {
const saved = localStorage.getItem(`user-preference-${key}`);
return saved ? JSON.parse(saved) : defaultValue;
} catch (error) {
return defaultValue;
}
}
return defaultValue;
};
const rememberUserAction = (action: string, data?: any) => {
if (canShow) {
// Implement your user action tracking logic here
}
};
return {
saveUserPreference,
loadUserPreference,
rememberUserAction,
isEnabled: canShow,
};
};
// Component wrapper for conditional rendering
interface ConditionalFeatureProps {
feature: 'functional';
children: React.ReactNode;
fallback?: React.ReactNode;
}
export const ConditionalFeature: React.FC<ConditionalFeatureProps> = ({
feature,
children,
fallback = null,
}) => {
const { canShow } = useConditionalFeature(feature);
return canShow ? <>{children}</> : <>{fallback}</>;
};
// Example usage components (analytics and marketing removed)
export const UserPreferenceManager: React.FC<{
preferenceKey: string;
defaultValue?: any;
children: (value: any, setValue: (value: any) => void) => React.ReactNode;
}> = ({ preferenceKey, defaultValue, children }) => {
const { saveUserPreference, loadUserPreference } = useFunctional();
const [value, setValue] = React.useState(defaultValue);
useEffect(() => {
const saved = loadUserPreference(preferenceKey, defaultValue);
setValue(saved);
}, [preferenceKey, defaultValue, loadUserPreference]);
const handleSetValue = (newValue: any) => {
setValue(newValue);
saveUserPreference(preferenceKey, newValue);
};
return <>{children(value, handleSetValue)}</>;
};

View File

@@ -0,0 +1,62 @@
"use client";
import { ReactNode, useEffect } from "react";
import Preloader from "./Preloader";
import ScrollToTop from "./ScrollToTop";
import { usePathname } from "next/navigation";
interface LayoutWrapperProps {
children: ReactNode;
}
const LayoutWrapper = ({ children }: LayoutWrapperProps) => {
const pathname = usePathname();
useEffect(() => {
// Force scroll to top on every pathname change - runs FIRST
window.history.scrollRestoration = 'manual';
// Immediate scroll
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
// Disable any smooth scroll temporarily
const html = document.documentElement;
const body = document.body;
const originalHtmlScroll = html.style.scrollBehavior;
const originalBodyScroll = body.style.scrollBehavior;
html.style.scrollBehavior = 'auto';
body.style.scrollBehavior = 'auto';
// Multiple forced scrolls
const scrollInterval = setInterval(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}, 10);
// Clean up after 300ms
const cleanup = setTimeout(() => {
clearInterval(scrollInterval);
html.style.scrollBehavior = originalHtmlScroll;
body.style.scrollBehavior = originalBodyScroll;
}, 300);
return () => {
clearInterval(scrollInterval);
clearTimeout(cleanup);
};
}, [pathname]);
return (
<>
<ScrollToTop />
<Preloader>
{children}
</Preloader>
</>
);
};
export default LayoutWrapper;

View File

@@ -0,0 +1,405 @@
/* Enterprise Preloader Overlay */
.gnx-preloader-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999 !important;
animation: fadeIn 0.4s ease-in;
overflow: hidden;
}
/* Geometric Background Pattern */
.gnx-preloader-bg-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
animation: patternMove 20s linear infinite;
opacity: 0.5;
}
.gnx-preloader-bg-pattern::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
animation: pulse 4s ease-in-out infinite;
}
.gnx-preloader-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
text-align: center;
padding: 3rem;
z-index: 10;
}
/* Professional Logo Styling */
.gnx-preloader-logo {
position: relative;
margin-bottom: 1rem;
}
.gnx-logo-wrapper {
position: relative;
display: inline-block;
padding: 2rem;
background: linear-gradient(135deg, rgba(30, 41, 59, 0.8) 0%, rgba(15, 23, 42, 0.8) 100%);
border-radius: 20px;
overflow: visible;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.gnx-logo-border {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899);
border-radius: 22px;
opacity: 0.5;
z-index: -1;
animation: borderGlow 3s ease-in-out infinite;
}
.gnx-logo-image {
position: relative;
z-index: 2;
filter: brightness(1.2) contrast(1.1);
transition: all 0.3s ease;
display: block !important;
}
/* Enterprise Branding */
.gnx-enterprise-brand {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.gnx-brand-title {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.02em;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
}
.gnx-brand-subtitle {
font-size: 0.875rem;
font-weight: 400;
color: #94a3b8;
letter-spacing: 0.1em;
text-transform: uppercase;
margin: 0;
}
/* Professional Progress Container */
.gnx-progress-container {
width: 320px;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.gnx-progress-bar {
width: 100%;
height: 4px;
background: rgba(148, 163, 184, 0.1);
border-radius: 4px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
}
.gnx-progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
border-radius: 4px;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
}
.gnx-progress-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: progressShine 2s infinite;
}
.gnx-progress-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.gnx-progress-text {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.gnx-progress-percentage {
font-size: 1rem;
color: #f1f5f9;
font-weight: 600;
font-family: 'SF Mono', 'Courier New', monospace;
min-width: 45px;
text-align: right;
}
/* Professional Loading Indicator */
.gnx-loading-indicator {
margin-top: 1rem;
}
.gnx-spinner {
position: relative;
width: 50px;
height: 50px;
}
.gnx-spinner-ring {
position: absolute;
width: 100%;
height: 100%;
border: 2px solid transparent;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spinRing 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
}
.gnx-spinner-ring:nth-child(1) {
border-top-color: #3b82f6;
animation-delay: 0s;
}
.gnx-spinner-ring:nth-child(2) {
border-top-color: #8b5cf6;
animation-delay: 0.2s;
width: 75%;
height: 75%;
top: 12.5%;
left: 12.5%;
}
.gnx-spinner-ring:nth-child(3) {
border-top-color: #ec4899;
animation-delay: 0.4s;
width: 50%;
height: 50%;
top: 25%;
left: 25%;
}
/* Corporate Footer */
.gnx-preloader-footer {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid rgba(148, 163, 184, 0.1);
}
.gnx-footer-text {
font-size: 0.75rem;
color: #64748b;
font-weight: 400;
letter-spacing: 0.05em;
margin: 0;
text-transform: uppercase;
}
/* Content visibility */
.gnx-content-hidden {
opacity: 0;
pointer-events: none;
position: absolute;
left: -9999px;
}
.gnx-content-visible {
opacity: 1;
pointer-events: auto;
animation: contentFadeIn 0.5s ease-in;
}
/* Enterprise Animations */
@keyframes fadeIn {
from {
opacity: 0;
backdrop-filter: blur(0px);
}
to {
opacity: 1;
backdrop-filter: blur(10px);
}
}
@keyframes contentFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes patternMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.3;
transform: translate(-50%, -50%) scale(1);
}
50% {
opacity: 0.5;
transform: translate(-50%, -50%) scale(1.1);
}
}
@keyframes borderGlow {
0%, 100% {
opacity: 0.3;
filter: blur(10px);
}
50% {
opacity: 0.6;
filter: blur(15px);
}
}
@keyframes progressShine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
@keyframes spinRing {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Responsive Design */
@media (max-width: 768px) {
.gnx-preloader-container {
gap: 1.5rem;
padding: 2rem;
}
.gnx-logo-wrapper {
padding: 1.5rem;
}
.gnx-logo-image {
width: 75px !important;
height: 56px !important;
}
.gnx-brand-title {
font-size: 1.5rem;
}
.gnx-brand-subtitle {
font-size: 0.75rem;
}
.gnx-progress-container {
width: 260px;
}
.gnx-spinner {
width: 40px;
height: 40px;
}
}
@media (max-width: 480px) {
.gnx-preloader-container {
gap: 1rem;
padding: 1.5rem;
}
.gnx-logo-wrapper {
padding: 1rem;
}
.gnx-logo-image {
width: 60px !important;
height: 45px !important;
}
.gnx-brand-title {
font-size: 1.25rem;
}
.gnx-brand-subtitle {
font-size: 0.625rem;
}
.gnx-progress-container {
width: 200px;
}
.gnx-spinner {
width: 35px;
height: 35px;
}
.gnx-footer-text {
font-size: 0.625rem;
}
}

View File

@@ -0,0 +1,151 @@
"use client";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import Image from "next/image";
import "./Preloader.css";
interface PreloaderProps {
children: React.ReactNode;
}
const Preloader = ({ children }: PreloaderProps) => {
const [isLoading, setIsLoading] = useState(true);
const [isMounted, setIsMounted] = useState(false);
const [progress, setProgress] = useState(0);
const pathname = usePathname();
// Initial mount - show preloader on first load
useEffect(() => {
setIsMounted(true);
setIsLoading(true);
setProgress(0);
// Simulate loading progress
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + Math.random() * 15 + 5;
});
}, 50);
// Complete loading after minimum duration
const completeTimer = setTimeout(() => {
setProgress(100);
setTimeout(() => {
setIsLoading(false);
}, 200);
}, 600);
return () => {
clearInterval(progressInterval);
clearTimeout(completeTimer);
};
}, []);
// Handle route changes
useEffect(() => {
if (!isMounted) return;
// Show preloader on route change
setIsLoading(true);
setProgress(0);
// Simulate loading progress
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 90) {
clearInterval(progressInterval);
return 90;
}
return prev + Math.random() * 20 + 10;
});
}, 40);
// Complete loading
const completeTimer = setTimeout(() => {
setProgress(100);
setTimeout(() => {
setIsLoading(false);
}, 150);
}, 400);
return () => {
clearInterval(progressInterval);
clearTimeout(completeTimer);
};
}, [pathname, isMounted]);
return (
<>
{isLoading && (
<div className="gnx-preloader-overlay">
{/* Geometric background pattern */}
<div className="gnx-preloader-bg-pattern"></div>
<div className="gnx-preloader-container">
{/* Logo with professional wrapper */}
<div className="gnx-preloader-logo">
<div className="gnx-logo-wrapper">
<div className="gnx-logo-border"></div>
<Image
src="/images/logo.png"
alt="GNX Logo"
width={100}
height={75}
className="gnx-logo-image"
priority
unoptimized
/>
</div>
</div>
{/* Enterprise branding */}
<div className="gnx-enterprise-brand">
<h1 className="gnx-brand-title">GNX Enterprise</h1>
<p className="gnx-brand-subtitle">Digital Transformation Solutions</p>
</div>
{/* Professional progress indicator */}
<div className="gnx-progress-container">
<div className="gnx-progress-bar">
<div
className="gnx-progress-fill"
style={{ width: `${progress}%` }}
>
<div className="gnx-progress-shine"></div>
</div>
</div>
<div className="gnx-progress-info">
<span className="gnx-progress-text">Loading</span>
<span className="gnx-progress-percentage">{Math.round(progress)}%</span>
</div>
</div>
{/* Professional loading indicator */}
<div className="gnx-loading-indicator">
<div className="gnx-spinner">
<div className="gnx-spinner-ring"></div>
<div className="gnx-spinner-ring"></div>
<div className="gnx-spinner-ring"></div>
</div>
</div>
{/* Corporate footer */}
<div className="gnx-preloader-footer">
<p className="gnx-footer-text">Powered by Advanced Technology</p>
</div>
</div>
</div>
)}
<div className={isLoading ? "gnx-content-hidden" : "gnx-content-visible"}>
{children}
</div>
</>
);
};
export default Preloader;

View File

@@ -0,0 +1,40 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
const ScrollToTop = () => {
const pathname = usePathname();
useEffect(() => {
// Aggressive scroll to top - run immediately and synchronously
const scrollToTop = () => {
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
};
// 1. Immediate execution
scrollToTop();
// 2. After microtask
Promise.resolve().then(scrollToTop);
// 3. After next frame
requestAnimationFrame(scrollToTop);
// 4. Multiple delayed attempts to override any smooth scroll libraries
const timeouts = [0, 10, 50, 100, 150, 200].map(delay =>
setTimeout(scrollToTop, delay)
);
return () => {
timeouts.forEach(clearTimeout);
};
}, [pathname]);
return null;
};
export default ScrollToTop;

View File

@@ -0,0 +1,37 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
const AppearDown = () => {
useEffect(() => {
if (window.innerWidth >= 992) {
gsap.registerPlugin(ScrollTrigger);
const appearDownSections = document.querySelectorAll(".appear-down");
appearDownSections.forEach((section) => {
gsap.fromTo(
section,
{
scale: 0.8,
opacity: 0,
},
{
scale: 1,
opacity: 1,
duration: 1.5,
scrollTrigger: {
trigger: section,
scrub: 1,
start: "top bottom",
end: "bottom center",
markers: false,
},
}
);
});
}
}, []);
return null;
};
export default AppearDown;

View File

@@ -0,0 +1,45 @@
"use client";
import { useEffect } from "react";
const ButtonHoverAnimation = () => {
useEffect(() => {
const btnAnim = document.querySelectorAll(".btn-anim");
if (btnAnim.length > 0) {
btnAnim.forEach((element) => {
element.addEventListener("mouseenter", handleMouseEnter);
element.addEventListener("mouseleave", handleMouseLeave);
});
return () => {
btnAnim.forEach((element) => {
element.removeEventListener("mouseenter", handleMouseEnter);
element.removeEventListener("mouseleave", handleMouseLeave);
});
};
}
}, []);
const handleMouseEnter = (e: any) => {
const element = e.currentTarget as any;
const span = element.querySelector("span");
if (span) {
const rect = element.getBoundingClientRect();
span.style.left = `${e.clientX - rect.left}px`;
span.style.top = `${e.clientY - rect.top}px`;
}
};
const handleMouseLeave = (e: any) => {
const element = e.currentTarget as HTMLElement;
const span = element.querySelector("span");
if (span) {
const rect = element.getBoundingClientRect();
span.style.left = `${e.clientX - rect.left}px`;
span.style.top = `${e.clientY - rect.top}px`;
}
};
return null;
};
export default ButtonHoverAnimation;

View File

@@ -0,0 +1,122 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
const FadeAnimations = () => {
useEffect(() => {
if (window.innerWidth >= 992) {
gsap.registerPlugin(ScrollTrigger);
const fadeWrapperRefs = document.querySelectorAll(".fade-wrapper");
fadeWrapperRefs.forEach((fadeWrapperRef) => {
const fadeItems = fadeWrapperRef.querySelectorAll(".fade-top");
const fadeItemsBottom = fadeWrapperRef.querySelectorAll(".fade-bottom");
const fadeItemsLeft = fadeWrapperRef.querySelectorAll(".fade-left");
const fadeItemsRight = fadeWrapperRef.querySelectorAll(".fade-right");
// from top
fadeItems.forEach((element, index) => {
const delay = index * 0.15;
gsap.set(element, {
opacity: 0,
y: 100,
});
ScrollTrigger.create({
trigger: element,
start: "top 100%",
end: "bottom 20%",
scrub: 0.5,
onEnter: () => {
gsap.to(element, {
opacity: 1,
y: 0,
duration: 1,
delay: delay,
});
},
once: true,
});
});
// from bottom
fadeItemsBottom.forEach((element, index) => {
const delay = index * 0.15;
gsap.set(element, {
opacity: 0,
y: -100,
});
ScrollTrigger.create({
trigger: element,
start: "top 100%",
end: "bottom 20%",
scrub: 0.5,
onEnter: () => {
gsap.to(element, {
opacity: 1,
y: 0,
duration: 1,
delay: delay,
});
},
once: true,
});
});
// from left
fadeItemsLeft.forEach((element, index) => {
const delay = index * 0.15;
gsap.set(element, {
opacity: 0,
x: 100,
});
ScrollTrigger.create({
trigger: element,
start: "top 100%",
end: "bottom 20%",
scrub: 0.5,
onEnter: () => {
gsap.to(element, {
opacity: 1,
x: 0,
duration: 1,
delay: delay,
});
},
once: true,
});
});
// from right
fadeItemsRight.forEach((element, index) => {
const delay = index * 0.15;
gsap.set(element, {
opacity: 0,
x: -100,
});
ScrollTrigger.create({
trigger: element,
start: "top 100%",
end: "bottom 20%",
scrub: 0.5,
onEnter: () => {
gsap.to(element, {
opacity: 1,
x: 0,
duration: 1,
delay: delay,
});
},
once: true,
});
});
});
}
}, []);
return null;
};
export default FadeAnimations;

View File

@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
const FadeImageBottom = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const deviceWidth = window.innerWidth;
if (
document.querySelectorAll(".fade-img").length > 0 &&
deviceWidth >= 992
) {
gsap.utils.toArray(".fade-img").forEach((el: any) => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: el,
start: "center center",
end: "+=40%",
scrub: 1,
pin: false,
invalidateOnRefresh: true,
},
});
tl.to(el, {
y: "120px",
zIndex: "-1",
duration: 1,
});
});
}
}, []);
return null;
};
export default FadeImageBottom;

View File

@@ -0,0 +1,60 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
const ParallaxImage = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const imageParallax = document.querySelectorAll(".parallax-image");
if (imageParallax.length > 0) {
imageParallax.forEach((element) => {
const animImageParallax = element as HTMLElement;
const aipWrap = animImageParallax.closest(
".parallax-image-wrap"
) as HTMLElement;
const aipInner = aipWrap?.querySelector(".parallax-image-inner");
if (aipWrap && aipInner) {
let tl_ImageParallax = gsap.timeline({
scrollTrigger: {
trigger: aipWrap,
start: "top bottom",
end: "bottom top",
scrub: true,
},
});
tl_ImageParallax.to(animImageParallax, {
yPercent: 30,
ease: "none",
});
gsap.fromTo(
aipInner,
{
scale: 1.2,
opacity: 0,
},
{
scale: 1,
opacity: 1,
duration: 1.5,
scrollTrigger: {
trigger: aipWrap,
start: "top 99%",
markers: false,
},
}
);
ScrollTrigger.refresh();
}
});
}
}, []);
return null;
};
export default ParallaxImage;

View File

@@ -0,0 +1,39 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import { ScrollToPlugin } from "gsap/dist/ScrollToPlugin";
const ScrollToElement = () => {
useEffect(() => {
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
const target = e.currentTarget.getAttribute("href");
if (target) {
gsap.to(window, {
scrollTo: {
y: target,
offsetY: 200,
},
duration: 1.5,
ease: "power3.inOut",
});
}
};
const links = document.querySelectorAll('a[href^="#"]');
links.forEach((anchor: any) => {
anchor.addEventListener("click", handleLinkClick);
});
return () => {
links.forEach((anchor: any) => {
anchor.removeEventListener("click", handleLinkClick);
});
};
}, []);
return null;
};
export default ScrollToElement;

View File

@@ -0,0 +1,107 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import Lenis from "lenis";
import { usePathname } from "next/navigation";
const SmoothScroll = () => {
const lenisRef = useRef<Lenis | null>(null);
const pathname = usePathname();
const [isNavigating, setIsNavigating] = useState(false);
// Handle pathname changes - PRIORITY 1
useEffect(() => {
setIsNavigating(true);
// Stop Lenis completely
if (lenisRef.current) {
lenisRef.current.stop();
lenisRef.current.scrollTo(0, { immediate: true, force: true, lock: true });
}
// Force scroll to top with all methods
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
// Keep forcing scroll for a brief period
const forceScroll = () => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
};
// Force scroll every 16ms (one frame) for 200ms
const intervalId = setInterval(forceScroll, 16);
// After navigation is settled, restart Lenis
const restartTimeout = setTimeout(() => {
clearInterval(intervalId);
if (lenisRef.current) {
lenisRef.current.scrollTo(0, { immediate: true, force: true });
lenisRef.current.start();
}
setIsNavigating(false);
// Final scroll enforcement
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
}, 200);
return () => {
clearInterval(intervalId);
clearTimeout(restartTimeout);
};
}, [pathname]);
// Initialize Lenis - PRIORITY 2
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: 'vertical',
gestureOrientation: 'vertical',
smoothWheel: true,
wheelMultiplier: 1,
smoothTouch: false,
touchMultiplier: 2,
infinite: false,
});
lenisRef.current = lenis;
// Force initial scroll to top
lenis.scrollTo(0, { immediate: true, force: true });
window.scrollTo(0, 0);
// Connect to GSAP ticker
const tickerCallback = (time: number) => {
if (!isNavigating) {
lenis.raf(time * 350);
}
};
gsap.ticker.add(tickerCallback);
gsap.ticker.lagSmoothing(0);
// Sync with ScrollTrigger
lenis.on('scroll', ScrollTrigger.update);
return () => {
lenis.destroy();
gsap.ticker.remove(tickerCallback);
lenisRef.current = null;
};
}, []);
return null;
};
export default SmoothScroll;

View File

@@ -0,0 +1,65 @@
"use client";
import { useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
import SplitType from "split-type";
const SplitTextAnimations = () => {
useEffect(() => {
if (window.innerWidth >= 992) {
gsap.registerPlugin(ScrollTrigger);
new SplitType(".title-anim", {
types: ["chars", "words"],
});
const titleAnims = document.querySelectorAll(".title-anim");
titleAnims.forEach((titleAnim) => {
const charElements = titleAnim.querySelectorAll(".char");
charElements.forEach((char, index) => {
const tl2 = gsap.timeline({
scrollTrigger: {
trigger: char,
start: "top 90%",
end: "bottom 60%",
scrub: false,
markers: false,
toggleActions: "play none none none",
},
});
const charDelay = index * 0.03;
tl2.from(char, {
duration: 0.8,
x: 70,
delay: charDelay,
autoAlpha: 0,
});
});
});
const titleElements = document.querySelectorAll(".title-anim");
titleElements.forEach((el) => {
const triggerEl = el as gsap.DOMTarget;
gsap.to(triggerEl, {
scrollTrigger: {
trigger: triggerEl,
start: "top 100%",
markers: false,
onEnter: () => {
if (el instanceof Element) {
el.classList.add("title-anim-active");
}
},
},
});
});
}
}, []);
return null;
};
export default SplitTextAnimations;

View File

@@ -0,0 +1,33 @@
"use client";
import VanillaTilt from "vanilla-tilt";
const VanillaTiltHover = () => {
const tiltSelectors = [".btn-anim", ".topy-tilt"];
const tiltElements = document.querySelectorAll(tiltSelectors.join(", "));
tiltElements.forEach((element) => {
const tiltElement = element as HTMLElement;
let tiltConfig: any = {
speed: 3000,
};
if (tiltElement.classList.contains("btn-anim")) {
tiltConfig = {
...tiltConfig,
max: 15,
perspective: 400,
};
} else if (tiltElement.classList.contains("topy-tilt")) {
tiltConfig = {
...tiltConfig,
max: 5,
};
}
VanillaTilt.init(tiltElement, tiltConfig);
});
return null;
};
export default VanillaTiltHover;

View File

@@ -0,0 +1,267 @@
"use client";
import Link from "next/link";
import Image from "next/legacy/image";
import location from "@/public/images/footer/location.png";
import phone from "@/public/images/footer/phone.png";
import gmail from "@/public/images/footer/gmail.png";
import { useNavigationServices } from "@/lib/hooks/useServices";
import { useJobs } from "@/lib/hooks/useCareer";
const Footer = () => {
const currentYear = new Date().getFullYear();
const { services: dynamicServices, loading: servicesLoading } = useNavigationServices();
const { jobs, loading: jobsLoading } = useJobs();
// Static header data
const headerData = {
title: "GNX Soft Ltd.",
logoUrl: "/images/logo.png",
logoLightUrl: "/images/logo-light.png",
navigationType: "both",
headerClass: "tp-header",
scrolledClass: "navbar-active",
buttonText: "Let's Talk",
buttonUrl: "/contact-us",
buttonClass: "btn btn-primary d-none d-sm-flex",
isActive: true,
displayOrder: 1,
metaData: JSON.stringify({
mobileBreakpoint: 992,
scrollThreshold: 50,
hideOnMobile: false,
mobileFirst: true,
hamburgerMenu: true
})
};
// Get logo URL from static data
const logoSrc = headerData.logoUrl;
return (
<footer className="footer position-relative overflow-x-clip">
<div className="container">
{/* Enterprise Footer Logo Section */}
<div className="row">
<div className="col-12">
<div className="footer-logo-section text-center pt-40 pb-30">
<div className="enterprise-logo-container">
<div className="enterprise-security-badges">
{/* Left Badge */}
<div className="security-badges-left">
<div className="security-badge">
<i className="fa-solid fa-building"></i>
<span>Enterprise Solutions</span>
</div>
</div>
{/* Center Logo */}
<div className="logo-center">
<Link href="/" aria-label="go to home" className="footer-logo">
<Image
src={logoSrc}
alt="Logo"
width={120}
height={90}
className="footer-logo-image"
/>
</Link>
</div>
{/* Right Badge */}
<div className="security-badges-right">
<div className="security-badge">
<i className="fa-solid fa-shield-halved"></i>
<span>Incident Management</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="row vertical-column-gap-lg">
<div className="col-12">
<div className="pt-40">
</div>
</div>
</div>
<div className="row vertical-column-gap-lg pt-40">
<div className="col-12 col-lg-2 col-md-6">
<div className="footer-section">
<h6 className="text-white fm fw-6 mb-24">Company</h6>
<ul className="footer-links">
<li><Link href="about-us">About Us</Link></li>
<li><Link href="career">Careers</Link></li>
<li><Link href="case-study">Success Stories</Link></li>
<li><Link href="contact-us">Contact Us</Link></li>
</ul>
</div>
</div>
<div className="col-12 col-lg-2 col-md-6">
<div className="footer-section">
<h6 className="text-white fm fw-6 mb-24">Services</h6>
<ul className="footer-links">
{servicesLoading ? (
<>
<li><Link href="/services">Our Services</Link></li>
</>
) : (
dynamicServices.slice(0, 6).map((service) => (
<li key={service.slug}>
<Link href={`/services/${service.slug}`}>
{service.title}
</Link>
</li>
))
)}
</ul>
</div>
</div>
<div className="col-12 col-lg-2 col-md-6">
<div className="footer-section">
<h6 className="text-white fm fw-6 mb-24">Latest Jobs</h6>
<ul className="footer-links">
{jobsLoading ? (
<>
<li><Link href="/career">View All Jobs</Link></li>
</>
) : (
jobs.slice(0, 4).map((job) => (
<li key={job.slug}>
<Link href={`/career/${job.slug}`}>
{job.title}
</Link>
</li>
))
)}
</ul>
</div>
</div>
<div className="col-12 col-lg-2 col-md-6">
<div className="footer-section">
<h6 className="text-white fm fw-6 mb-24">Support</h6>
<ul className="footer-links">
<li><Link href="/support-center">Support Center</Link></li>
<li><Link href="/policy?type=privacy">Privacy Policy</Link></li>
<li><Link href="/policy?type=terms">Terms of Use</Link></li>
<li><Link href="/policy?type=support">Support Policy</Link></li>
</ul>
</div>
</div>
<div className="col-12 col-lg-4 col-md-12">
<div className="footer-cta-section">
<div className="cta-content">
<h6 className="text-white fm fw-6 mb-24">Ready to Transform?</h6>
<p className="text-white mb-30">Start your software journey with our incident management and custom development solutions.</p>
<Link href="contact-us" className="btn-anim">
Start Your Software Journey
<i className="fa-solid fa-arrow-trend-up"></i>
<span></span>
</Link>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="footer__inner pt-60">
<div className="row vertical-column-gap-lg">
<div className="col-12 col-md-6 col-lg-4">
<div className="footer__inner-single">
<div className="thumb">
<Image src={location} alt="Image" width={24} height={24} />
</div>
<div className="content">
<h5 className="mt-8 fm fw-6 text-white mb-24">
Location
</h5>
<p className="text-quinary">
<Link
href="https://maps.google.com/?q=42.496781103070504,27.4758968970689"
target="_blank"
>
GNX Soft Ltd.<br />
Tsar Simeon I, 56<br />
Burgas, Burgas 8000<br />
Bulgaria
</Link>
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="footer__inner-single">
<div className="thumb">
<Image src={phone} alt="Image" width={24} height={24} />
</div>
<div className="content">
<h5 className="mt-8 fm fw-6 text-white mb-24">Phone</h5>
<p className="text-quinary mb-12">
<Link href="tel:+359897338147">+359 897 338 147</Link>
</p>
</div>
</div>
</div>
<div className="col-12 col-md-6 col-lg-4">
<div className="footer__inner-single">
<div className="thumb">
<Image src={gmail} alt="Image" width={24} height={24} />
</div>
<div className="content">
<h5 className="mt-8 fm fw-6 text-white mb-24">Email</h5>
<p className="text-quinary mb-12 text-lowercase">
<Link href="mailto:info@gnxsoft.com">
info@gnxsoft.com
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-12">
<div className="footer-copyright">
<div className="row align-items-center vertical-column-gap">
<div className="col-12 col-lg-6">
<div className="footer__copyright-text text-center text-lg-start">
<p className="text-quinary mt-8">
&copy; <span id="copyrightYear">{currentYear}</span>{" "}
<Link href="/" className="fw-6">
GNX
</Link>
. All rights reserved. GNX Software Solutions.
</p>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="social justify-content-center justify-content-lg-end">
<Link
href="https://www.linkedin.com/company/gnxtech"
target="_blank"
title="LinkedIn"
>
<i className="fa-brands fa-linkedin"></i>
</Link>
<Link
href="https://github.com/gnxtech"
target="_blank"
title="GitHub"
>
<i className="fa-brands fa-github"></i>
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,266 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import OffcanvasMenu from "./OffcanvasMenu";
import { OffcanvasData } from "@/public/data/offcanvas-data";
import { useNavigationServices } from "@/lib/hooks/useServices";
const Header = () => {
const [isOffcanvasOpen, setIsOffcanvasOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [isActive, setIsActive] = useState(true);
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
const [isMobile, setIsMobile] = useState(false);
// Fetch services from API
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
// Create dynamic navigation data with services from API
const navigationData = useMemo(() => {
const baseNavigation = [...OffcanvasData];
// Find the Services menu item and update its submenu with API data
const servicesIndex = baseNavigation.findIndex(item => item.title === "Services");
if (servicesIndex !== -1 && apiServices.length > 0) {
baseNavigation[servicesIndex] = {
...baseNavigation[servicesIndex],
submenu: apiServices.map(service => ({
id: service.id + 1000, // Offset to avoid conflicts with existing IDs
title: service.title,
path: `/services/${service.slug}`,
parent_id: baseNavigation[servicesIndex].id,
display_order: service.display_order,
created_at: service.created_at,
updated_at: service.updated_at
}))
} as any;
}
return baseNavigation;
}, [apiServices]);
// Static header data
const headerData = {
title: "EnterpriseSoft Solutions",
logoUrl: "/images/logo.png",
logoLightUrl: "/images/logo-light.png",
navigationType: "both",
headerClass: "tp-header",
scrolledClass: "navbar-active",
buttonText: "Support Center",
buttonUrl: "/support-center",
buttonClass: "btn btn-primary d-none d-sm-flex",
isActive: true,
displayOrder: 1,
metaData: JSON.stringify({
mobileBreakpoint: 992,
scrollThreshold: 50,
hideOnMobile: false,
mobileFirst: true,
hamburgerMenu: true
})
};
const handleClick = () => {
setTimeout(() => {
setIsOffcanvasOpen(false);
}, 900);
setIsActive(false);
};
const handleDropdownToggle = (index: number) => {
setOpenDropdown(openDropdown === index ? null : index);
};
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY;
if (scrollPosition > 50) {
setScrolled(true);
} else {
setScrolled(false);
}
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [scrolled]);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 992);
setTimeout(() => {
setIsOffcanvasOpen(false);
}, 900);
setIsActive(false);
};
handleResize(); // Check on mount
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
// Use static data
let logoSrc: string = headerData.logoUrl;
let headerClass = headerData.headerClass;
let buttonText = headerData.buttonText;
let buttonUrl = headerData.buttonUrl;
let buttonClass = headerData.buttonClass;
const pathname = usePathname();
// Override logo based on pathname if needed (maintain existing behavior)
if (
pathname === "/career" ||
pathname === "/" ||
pathname === "/index" ||
pathname === "/services" ||
pathname === "/service-single"
) {
logoSrc = headerData.logoLightUrl;
}
const handleOffCanvas = () => {
setIsOffcanvasOpen(true);
setIsActive(true);
};
return (
<>
<header className={headerClass}>
<div className={"primary-navbar" + (scrolled ? " navbar-active" : " ")}>
<div className="container">
<div className="row">
<div className="col-12">
<nav className="navbar p-0">
<div className="navbar__logo">
<Link href="/" aria-label="go to home" className="logo-img">
<Image
src={logoSrc}
alt="Logo"
width={160}
height={120}
priority
className="logo-image"
/>
</Link>
</div>
{/* Desktop Navigation Menu */}
<div className="navbar__menu d-none d-lg-flex">
<ul>
{navigationData.map((item) =>
item.title === "Support Center" ? null : item.submenu ? (
<li
className="navbar__item navbar__item--has-children"
key={item.id}
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
>
<button
aria-label="dropdown menu"
className={
"navbar__dropdown-label" +
(openDropdown === item.id
? " navbar__item-active"
: " ")
}
onClick={() => isMobile && handleDropdownToggle(item.id)}
>
{item.title}
{item.title === "Services" && servicesLoading && (
<span className="loading-indicator"></span>
)}
</button>
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}>
{item.title === "Services" && servicesLoading ? (
<li>
<span className="text-muted">Loading services...</span>
</li>
) : item.title === "Services" && servicesError ? (
<li>
<span className="text-danger">Failed to load services</span>
</li>
) : (
item.submenu.map((subItem, subIndex) => (
<li key={subIndex}>
<Link
href={subItem.path || "#"}
className={
pathname === subItem.path
? " active-current-sub"
: " "
}
>
{subItem.title}
</Link>
</li>
))
)}
</ul>
</li>
) : (
<li
className="navbar__item"
key={item.id}
>
<Link
href={item.path || "#"}
className={
pathname === item.path ? " active-current-link" : " "
}
>
{item.title}
</Link>
</li>
)
)}
</ul>
</div>
<div className="navbar__options">
<Link href={buttonUrl} className={buttonClass}>
{buttonText}
</Link>
<button
className="open-offcanvas-nav d-lg-none"
aria-label="toggle mobile menu"
title="open offcanvas menu"
onClick={handleOffCanvas}
>
<span className="icon-bar top-bar"></span>
<span className="icon-bar middle-bar"></span>
<span className="icon-bar bottom-bar"></span>
</button>
</div>
</nav>
</div>
</div>
</div>
</div>
</header>
<OffcanvasMenu
isOffcanvasOpen={isOffcanvasOpen}
handleClick={handleClick}
isActive={isActive}
navigationData={navigationData}
servicesLoading={servicesLoading}
servicesError={servicesError}
/>
</>
);
};
export default Header;

View File

@@ -0,0 +1,204 @@
"use client";
import { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import AnimateHeight from "react-animate-height";
import Image from "next/legacy/image";
import Link from "next/link";
import { OffcanvasData } from "@/public/data/offcanvas-data";
import logoLight from "@/public/images/logo-light.png";
interface OffcanvasMenuProps {
isOffcanvasOpen: boolean;
isActive: boolean;
handleClick: () => void;
navigationData?: any[];
servicesLoading?: boolean;
servicesError?: string | null;
}
const OffcanvasMenu = ({
isOffcanvasOpen,
isActive,
handleClick,
navigationData = OffcanvasData,
servicesLoading = false,
servicesError = null
}: OffcanvasMenuProps) => {
const [openDropdown, setOpenDropdown] = useState(null);
const [mounted, setMounted] = useState(false);
const handleDropdownToggle = (index: any) => {
setOpenDropdown((prev) => (prev === index ? null : index));
};
const pathname = usePathname();
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
const parentItems = document.querySelectorAll(
".navbar__item--has-children"
);
parentItems.forEach((parentItem) => {
const childItems = parentItem.querySelectorAll(".active-current-sub");
if (childItems.length > 0) {
parentItem.classList.add("active-current-parent");
}
});
}, []);
return (
<div className="offcanvas-nav">
<div
className={
"offcanvas-menu" + (isOffcanvasOpen ? " show-offcanvas-menu" : " ")
}
suppressHydrationWarning
>
<nav
className={
"offcanvas-menu__wrapper" + (isActive ? " " : " nav-fade-active")
}
data-lenis-prevent
>
<div className="offcanvas-menu__header nav-fade">
<div className="logo">
<Link href="/" className="logo-img">
<Image src={logoLight} priority alt="Image" title="Logo" width={160} height={60} />
</Link>
</div>
<button
aria-label="close offcanvas menu"
className="close-offcanvas-menu"
onClick={handleClick}
>
<i className="fa-solid fa-xmark"></i>
</button>
</div>
<div className="offcanvas-menu__list">
<div className="navbar__menu">
<ul>
{navigationData.map((item, index) =>
item.submenu ? (
<li
className="navbar__item navbar__item--has-children nav-fade"
key={index}
>
<button
aria-label="dropdown menu"
className={
"navbar__dropdown-label" +
(openDropdown === index
? " navbar__item-active"
: " ")
}
onClick={() => handleDropdownToggle(index)}
>
{item.title}
{item.title === "Services" && servicesLoading && (
<span className="loading-indicator"></span>
)}
</button>
<AnimateHeight
duration={400}
height={openDropdown === index ? "auto" : 0}
>
<ul className="navbar__sub-menu">
{item.title === "Services" && servicesLoading ? (
<li>
<span className="text-muted">Loading services...</span>
</li>
) : item.title === "Services" && servicesError ? (
<li>
<span className="text-danger">Failed to load services</span>
</li>
) : (
item.submenu.map((subItem: any, subIndex: number) => (
<li key={subIndex}>
<Link
href={subItem.path || "#"}
className={
mounted && pathname === subItem.path
? " active-current-sub"
: " "
}
>
{subItem.title}
</Link>
</li>
))
)}
</ul>
</AnimateHeight>
</li>
) : (
<li className="navbar__item nav-fade" key={index}>
<Link
href={item.path || "#"}
className={
mounted && pathname === item.path ? " active-current-link" : " "
}
>
{item.title}
</Link>
</li>
)
)}
</ul>
</div>
</div>
</nav>
<div className="offcanvas-menu__enterprise-info nav-fade">
<div className="enterprise-contact">
<h4>Get in Touch</h4>
<p>Ready to transform your business?</p>
<div className="contact-methods">
<a href="tel:+359896138030" className="contact-item">
<i className="fa-solid fa-phone"></i>
<span>+359896138030</span>
</a>
<a href="mailto:info@gnxsoft.com" className="contact-item">
<i className="fa-solid fa-envelope"></i>
<span>info@gnxsoft.com</span>
</a>
</div>
</div>
</div>
<ul className="enterprise-social nav-fade">
<li>
<Link
href="https://www.linkedin.com/company/gnxtech"
target="_blank"
aria-label="Connect with us on LinkedIn"
>
<i className="fa-brands fa-linkedin-in"></i>
</Link>
</li>
<li>
<Link
href="https://github.com/gnxtech"
target="_blank"
aria-label="View our code on GitHub"
>
<i className="fa-brands fa-github"></i>
</Link>
</li>
</ul>
<div className="anime">
<span className="nav-fade"></span>
<span className="nav-fade"></span>
<span className="nav-fade"></span>
<span className="nav-fade"></span>
<span className="nav-fade"></span>
<span className="nav-fade"></span>
</div>
</div>
</div>
);
};
export default OffcanvasMenu;

View File

@@ -0,0 +1,378 @@
'use client';
import Script from 'next/script';
import { SITE_CONFIG } from '@/lib/seo/metadata';
// Organization Schema
export function OrganizationSchema() {
const schema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: SITE_CONFIG.name,
legalName: `${SITE_CONFIG.name} LLC`,
url: SITE_CONFIG.url,
logo: `${SITE_CONFIG.url}/images/logo.png`,
foundingDate: SITE_CONFIG.foundedYear.toString(),
description: SITE_CONFIG.description,
email: SITE_CONFIG.email,
telephone: SITE_CONFIG.phone,
address: {
'@type': 'PostalAddress',
streetAddress: SITE_CONFIG.address.street,
addressLocality: SITE_CONFIG.address.city,
addressRegion: SITE_CONFIG.address.state,
postalCode: SITE_CONFIG.address.zip,
addressCountry: SITE_CONFIG.address.country,
},
sameAs: [
SITE_CONFIG.social.linkedin,
SITE_CONFIG.social.github,
],
contactPoint: [
{
'@type': 'ContactPoint',
telephone: SITE_CONFIG.phone,
contactType: 'Customer Service',
email: SITE_CONFIG.email,
availableLanguage: ['English'],
areaServed: 'Worldwide',
},
{
'@type': 'ContactPoint',
telephone: SITE_CONFIG.phone,
contactType: 'Sales',
email: `sales@${SITE_CONFIG.email.split('@')[1]}`,
availableLanguage: ['English'],
},
{
'@type': 'ContactPoint',
telephone: SITE_CONFIG.phone,
contactType: 'Technical Support',
email: `support@${SITE_CONFIG.email.split('@')[1]}`,
availableLanguage: ['English'],
areaServed: 'Worldwide',
},
],
};
return (
<Script
id="organization-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Website Schema
export function WebsiteSchema() {
const schema = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_CONFIG.name,
url: SITE_CONFIG.url,
description: SITE_CONFIG.description,
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.name,
logo: {
'@type': 'ImageObject',
url: `${SITE_CONFIG.url}/images/logo.png`,
},
},
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_CONFIG.url}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
};
return (
<Script
id="website-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Breadcrumb Schema
interface BreadcrumbItem {
name: string;
url: string;
}
interface BreadcrumbSchemaProps {
items: BreadcrumbItem[];
}
export function BreadcrumbSchema({ items }: BreadcrumbSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: `${SITE_CONFIG.url}${item.url}`,
})),
};
return (
<Script
id="breadcrumb-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Service Schema
interface ServiceSchemaProps {
service: {
title: string;
description: string;
slug: string;
category?: { name: string };
duration?: string;
technologies?: string;
deliverables?: string;
image?: string | File;
image_url?: string;
};
}
export function ServiceSchema({ service }: ServiceSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Service',
name: service.title,
description: service.description,
provider: {
'@type': 'Organization',
name: SITE_CONFIG.name,
url: SITE_CONFIG.url,
},
serviceType: service.category?.name || 'Enterprise Software',
areaServed: {
'@type': 'Country',
name: 'Worldwide',
},
url: `${SITE_CONFIG.url}/services/${service.slug}`,
image: service.image_url ||
(typeof service.image === 'string' ? `${SITE_CONFIG.url}${service.image}` : `${SITE_CONFIG.url}/images/service/default.png`),
serviceOutput: service.deliverables,
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
ratingCount: '127',
bestRating: '5',
worstRating: '1',
},
};
return (
<Script
id={`service-schema-${service.slug}`}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Article Schema (for blog posts)
interface ArticleSchemaProps {
article: {
title: string;
description?: string;
excerpt?: string;
slug: string;
image?: string;
published_at?: string;
updated_at?: string;
author?: { name: string; image?: string };
category?: { name: string };
};
}
export function ArticleSchema({ article }: ArticleSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.description || article.excerpt,
image: article.image
? `${SITE_CONFIG.url}${article.image}`
: `${SITE_CONFIG.url}/images/blog/default.png`,
datePublished: article.published_at,
dateModified: article.updated_at || article.published_at,
author: {
'@type': 'Person',
name: article.author?.name || SITE_CONFIG.name,
image: article.author?.image,
},
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.name,
logo: {
'@type': 'ImageObject',
url: `${SITE_CONFIG.url}/images/logo.png`,
},
},
articleSection: article.category?.name,
url: `${SITE_CONFIG.url}/insights/${article.slug}`,
};
return (
<Script
id={`article-schema-${article.slug}`}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// FAQ Schema
interface FAQItem {
question: string;
answer: string;
}
interface FAQSchemaProps {
faqs: FAQItem[];
}
export function FAQSchema({ faqs }: FAQSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
};
return (
<Script
id="faq-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Job Posting Schema
interface JobPostingSchemaProps {
job: {
title: string;
description: string;
slug: string;
location?: string;
employment_type?: string;
salary_min?: number;
salary_max?: number;
posted_at?: string;
valid_through?: string;
};
}
export function JobPostingSchema({ job }: JobPostingSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'JobPosting',
title: job.title,
description: job.description,
datePosted: job.posted_at || new Date().toISOString(),
validThrough: job.valid_through,
employmentType: job.employment_type || 'FULL_TIME',
hiringOrganization: {
'@type': 'Organization',
name: SITE_CONFIG.name,
sameAs: SITE_CONFIG.url,
logo: `${SITE_CONFIG.url}/images/logo.png`,
},
jobLocation: {
'@type': 'Place',
address: {
'@type': 'PostalAddress',
addressLocality: job.location || SITE_CONFIG.address.city,
addressRegion: SITE_CONFIG.address.state,
addressCountry: SITE_CONFIG.address.country,
},
},
baseSalary: job.salary_min &&
job.salary_max && {
'@type': 'MonetaryAmount',
currency: 'USD',
value: {
'@type': 'QuantitativeValue',
minValue: job.salary_min,
maxValue: job.salary_max,
unitText: 'YEAR',
},
},
url: `${SITE_CONFIG.url}/career/${job.slug}`,
};
return (
<Script
id={`job-posting-schema-${job.slug}`}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Local Business Schema
export function LocalBusinessSchema() {
const schema = {
'@context': 'https://schema.org',
'@type': 'ProfessionalService',
name: SITE_CONFIG.name,
image: `${SITE_CONFIG.url}/images/logo.png`,
'@id': SITE_CONFIG.url,
url: SITE_CONFIG.url,
telephone: SITE_CONFIG.phone,
priceRange: '$$$$',
address: {
'@type': 'PostalAddress',
streetAddress: SITE_CONFIG.address.street,
addressLocality: SITE_CONFIG.address.city,
addressRegion: SITE_CONFIG.address.state,
postalCode: SITE_CONFIG.address.zip,
addressCountry: SITE_CONFIG.address.country,
},
geo: {
'@type': 'GeoCoordinates',
latitude: 37.7749,
longitude: -122.4194,
},
openingHoursSpecification: {
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
opens: '09:00',
closes: '18:00',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.9',
reviewCount: '127',
},
};
return (
<Script
id="local-business-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}

11
frontEnd/dev.log Normal file
View File

@@ -0,0 +1,11 @@
> itify@0.1.0 dev
> next dev
▲ Next.js 15.5.3
- Local: http://localhost:3000
- Network: http://192.168.1.101:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 2.4s

View File

@@ -0,0 +1,325 @@
import { API_BASE_URL } from '../config/api';
// Types for About Us data
export interface AboutStat {
number: string;
label: string;
order: number;
}
export interface AboutSocialLink {
platform: string;
url: string;
icon: string;
aria_label: string;
order: number;
}
export interface AboutBanner {
id: number;
title: string;
subtitle: string;
description: string;
badge_text: string;
badge_icon: string;
cta_text: string;
cta_link: string;
cta_icon: string;
image_url: string | null;
is_active: boolean;
stats: AboutStat[];
social_links: AboutSocialLink[];
created_at: string;
updated_at: string;
}
export interface AboutFeature {
title: string;
description: string;
icon: string;
order: number;
}
export interface AboutService {
id: number;
title: string;
subtitle: string;
description: string;
badge_text: string;
badge_icon: string;
image_url: string | null;
cta_text: string;
cta_link: string;
is_active: boolean;
features: AboutFeature[];
created_at: string;
updated_at: string;
}
export interface AboutProcessStep {
step_number: string;
title: string;
description: string;
order: number;
}
export interface AboutProcess {
id: number;
title: string;
subtitle: string;
description: string;
badge_text: string;
badge_icon: string;
image_url: string | null;
cta_text: string;
cta_link: string;
is_active: boolean;
steps: AboutProcessStep[];
created_at: string;
updated_at: string;
}
export interface AboutMilestone {
year: string;
title: string;
description: string;
order: number;
}
export interface AboutJourney {
id: number;
title: string;
subtitle: string;
description: string;
badge_text: string;
badge_icon: string;
image_url: string | null;
cta_text: string;
cta_link: string;
is_active: boolean;
milestones: AboutMilestone[];
created_at: string;
updated_at: string;
}
export interface AboutPageData {
banner: AboutBanner;
service: AboutService;
process: AboutProcess;
journey: AboutJourney;
}
class AboutServiceAPI {
private baseUrl = `${API_BASE_URL}/api/about`;
/**
* Get all about page data in one request
*/
async getAboutPageData(): Promise<AboutPageData> {
try {
const response = await fetch(`${this.baseUrl}/page/`, {
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;
}
}
/**
* Get all about banners
*/
async getBanners(): Promise<AboutBanner[]> {
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 about banner by ID
*/
async getBanner(id: number): Promise<AboutBanner> {
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;
}
}
/**
* Get all about services
*/
async getServices(): Promise<AboutService[]> {
try {
const response = await fetch(`${this.baseUrl}/service/`, {
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 about service by ID
*/
async getService(id: number): Promise<AboutService> {
try {
const response = await fetch(`${this.baseUrl}/service/${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;
}
}
/**
* Get all about processes
*/
async getProcesses(): Promise<AboutProcess[]> {
try {
const response = await fetch(`${this.baseUrl}/process/`, {
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 about process by ID
*/
async getProcess(id: number): Promise<AboutProcess> {
try {
const response = await fetch(`${this.baseUrl}/process/${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;
}
}
/**
* Get all about journeys
*/
async getJourneys(): Promise<AboutJourney[]> {
try {
const response = await fetch(`${this.baseUrl}/journey/`, {
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 about journey by ID
*/
async getJourney(id: number): Promise<AboutJourney> {
try {
const response = await fetch(`${this.baseUrl}/journey/${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 a singleton instance
export const aboutService = new AboutServiceAPI();
export default aboutService;

View File

@@ -0,0 +1,422 @@
import { API_CONFIG } from '../config/api';
// Types for Blog API
export interface BlogAuthor {
id: number;
name: string;
email?: string;
bio?: string;
avatar?: string;
}
export interface BlogCategory {
id: number;
title: string;
slug: string;
description?: string;
display_order: number;
posts_count?: number;
}
export interface BlogTag {
id: number;
name: string;
slug: string;
}
export interface BlogPost {
id: number;
title: string;
slug: string;
content?: string;
excerpt: string;
thumbnail?: string;
featured_image?: string;
author?: BlogAuthor;
author_name?: string;
category?: BlogCategory;
category_title?: string;
category_slug?: string;
tags?: BlogTag[];
meta_description?: string;
meta_keywords?: string;
published: boolean;
featured: boolean;
views_count: number;
reading_time: number;
published_at: string;
created_at: string;
updated_at: string;
related_posts?: BlogPost[];
}
export interface BlogPostListResponse {
count: number;
next: string | null;
previous: string | null;
results: BlogPost[];
}
export interface BlogComment {
id: number;
post: number;
name: string;
email: string;
content: string;
parent?: number;
is_approved: boolean;
created_at: string;
updated_at: string;
replies?: BlogComment[];
}
export interface BlogCommentCreateData {
post: number;
name: string;
email: string;
content: string;
parent?: number;
}
// Helper function to build query string
const buildQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.append(key, value.toString());
}
});
return searchParams.toString();
};
// Blog API functions
export const blogService = {
// Get all blog posts with optional filtering
getPosts: async (params?: {
category?: string;
tag?: string;
author?: number;
search?: string;
featured?: boolean;
ordering?: string;
page?: number;
page_size?: number;
}): Promise<BlogPostListResponse> => {
try {
const queryString = params ? buildQueryString(params) : '';
const url = `${API_CONFIG.BASE_URL}/api/blog/posts/${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
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;
}
},
// Get a single blog post by slug
getPostBySlug: async (slug: string): Promise<BlogPost> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/${slug}/`, {
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;
}
},
// Get featured blog posts
getFeaturedPosts: async (): Promise<BlogPost[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/featured/`, {
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;
}
},
// Get latest blog posts
getLatestPosts: async (limit: number = 5): Promise<BlogPost[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/latest/?limit=${limit}`, {
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;
}
},
// Get popular blog posts
getPopularPosts: async (limit: number = 5): Promise<BlogPost[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/popular/?limit=${limit}`, {
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;
}
},
// Get related posts for a specific post
getRelatedPosts: async (postSlug: string): Promise<BlogPost[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/posts/${postSlug}/related/`, {
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;
}
},
// Get all blog categories
getCategories: async (): Promise<BlogCategory[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return Array.isArray(data) ? data : data.results || [];
} catch (error) {
throw error;
}
},
// Get categories with posts
getCategoriesWithPosts: async (): Promise<BlogCategory[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/with_posts/`, {
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;
}
},
// Get a single category by slug
getCategoryBySlug: async (slug: string): Promise<BlogCategory> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/categories/${slug}/`, {
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;
}
},
// Get all blog tags
getTags: async (): Promise<BlogTag[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/tags/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return Array.isArray(data) ? data : data.results || [];
} catch (error) {
throw error;
}
},
// Get posts by tag
getPostsByTag: async (tagSlug: string): Promise<BlogPostListResponse> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/tags/${tagSlug}/posts/`, {
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;
}
},
// Get all blog authors
getAuthors: async (): Promise<BlogAuthor[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/authors/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return Array.isArray(data) ? data : data.results || [];
} catch (error) {
throw error;
}
},
// Get posts by author
getPostsByAuthor: async (authorId: number): Promise<BlogPostListResponse> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/authors/${authorId}/posts/`, {
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;
}
},
// Get comments for a post
getComments: async (postId: number): Promise<BlogComment[]> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/comments/?post=${postId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return Array.isArray(data) ? data : data.results || [];
} catch (error) {
throw error;
}
},
// Create a new comment
createComment: async (commentData: BlogCommentCreateData): Promise<{ message: string; data: BlogComment }> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/api/blog/comments/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(commentData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
throw error;
}
},
};

View File

@@ -0,0 +1,229 @@
import { API_BASE_URL } from '../config/api';
export interface JobPosition {
id: number;
title: string;
slug: string;
department: string;
employment_type: string;
location_type: string;
location: string;
open_positions: number;
experience_required?: string;
salary_min?: number;
salary_max?: number;
salary_currency: string;
salary_period: string;
salary_additional?: string;
short_description?: string;
about_role?: string;
requirements?: string[];
responsibilities?: string[];
qualifications?: string[];
bonus_points?: string[];
benefits?: string[];
start_date: string;
posted_date: string;
updated_date: string;
deadline?: string;
status: string;
featured?: boolean;
}
export interface JobApplication {
job: number;
first_name: string;
last_name: string;
email: string;
phone?: string;
current_position?: string;
current_company?: string;
years_of_experience?: string;
cover_letter?: string;
resume: File;
portfolio_url?: string;
linkedin_url?: string;
github_url?: string;
website_url?: string;
available_from?: string;
notice_period?: string;
expected_salary?: number;
salary_currency?: string;
consent: boolean;
}
class CareerService {
private baseUrl = `${API_BASE_URL}/api/career`;
/**
* Get all active job positions
*/
async getAllJobs(): Promise<JobPosition[]> {
try {
const response = await fetch(`${this.baseUrl}/jobs`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch jobs: ${response.statusText}`);
}
const data = await response.json();
// Handle paginated response - extract results array
return data.results || data;
} catch (error) {
throw error;
}
}
/**
* Get a single job position by slug
*/
async getJobBySlug(slug: string): Promise<JobPosition> {
try {
const response = await fetch(`${this.baseUrl}/jobs/${slug}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch job: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
throw error;
}
}
/**
* Get featured job positions
*/
async getFeaturedJobs(): Promise<JobPosition[]> {
try {
const response = await fetch(`${this.baseUrl}/jobs/featured`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch featured jobs: ${response.statusText}`);
}
const data = await response.json();
// Handle paginated response - extract results array
return data.results || data;
} catch (error) {
throw error;
}
}
/**
* Submit a job application
*/
async submitApplication(applicationData: JobApplication): Promise<any> {
try {
const formData = new FormData();
// Required fields
formData.append('job', applicationData.job.toString());
formData.append('first_name', applicationData.first_name);
formData.append('last_name', applicationData.last_name);
formData.append('email', applicationData.email);
formData.append('consent', applicationData.consent.toString());
formData.append('resume', applicationData.resume);
// Optional fields (only append if they exist)
if (applicationData.phone) formData.append('phone', applicationData.phone);
if (applicationData.current_position) formData.append('current_position', applicationData.current_position);
if (applicationData.current_company) formData.append('current_company', applicationData.current_company);
if (applicationData.years_of_experience) formData.append('years_of_experience', applicationData.years_of_experience);
if (applicationData.cover_letter) formData.append('cover_letter', applicationData.cover_letter);
if (applicationData.portfolio_url) formData.append('portfolio_url', applicationData.portfolio_url);
if (applicationData.linkedin_url) formData.append('linkedin_url', applicationData.linkedin_url);
if (applicationData.github_url) formData.append('github_url', applicationData.github_url);
if (applicationData.website_url) formData.append('website_url', applicationData.website_url);
if (applicationData.available_from) formData.append('available_from', applicationData.available_from);
if (applicationData.notice_period) formData.append('notice_period', applicationData.notice_period);
if (applicationData.expected_salary !== undefined) formData.append('expected_salary', applicationData.expected_salary.toString());
if (applicationData.salary_currency) formData.append('salary_currency', applicationData.salary_currency);
const response = await fetch(`${this.baseUrl}/applications`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || errorData.message || errorData.detail || `HTTP ${response.status}: ${response.statusText}`;
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
throw error;
}
}
/**
* Filter jobs by department
*/
async getJobsByDepartment(department: string): Promise<JobPosition[]> {
try {
const response = await fetch(`${this.baseUrl}/jobs?department=${department}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch jobs: ${response.statusText}`);
}
const data = await response.json();
// Handle paginated response - extract results array
return data.results || data;
} catch (error) {
throw error;
}
}
/**
* Filter jobs by employment type
*/
async getJobsByEmploymentType(employmentType: string): Promise<JobPosition[]> {
try {
const response = await fetch(`${this.baseUrl}/jobs?employment_type=${employmentType}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch jobs: ${response.statusText}`);
}
const data = await response.json();
// Handle paginated response - extract results array
return data.results || data;
} catch (error) {
throw error;
}
}
}
export const careerService = new CareerService();

View File

@@ -0,0 +1,364 @@
import { API_CONFIG } from '../config/api';
// Types for Case Study API
export interface CaseStudyCategory {
id: number;
name: string;
slug: string;
description?: string;
display_order: number;
case_studies_count?: number;
}
export interface Client {
id: number;
name: string;
slug: string;
logo?: string;
description?: string;
website?: string;
}
export interface CaseStudyImage {
id: number;
image: string;
caption?: string;
display_order: number;
}
export interface CaseStudyProcess {
id: number;
title: string;
description: string;
step_number: number;
}
export interface CaseStudy {
id: number;
title: string;
slug: string;
subtitle?: string;
description?: string;
excerpt: string;
thumbnail?: string;
featured_image?: string;
poster_image?: string;
project_image?: string;
project_overview?: string;
site_map_content?: string;
category?: CaseStudyCategory;
category_name?: string;
category_slug?: string;
client?: Client;
client_name?: string;
gallery_images?: CaseStudyImage[];
process_steps?: CaseStudyProcess[];
meta_description?: string;
meta_keywords?: string;
published: boolean;
featured: boolean;
views_count: number;
display_order: number;
published_at: string;
created_at: string;
updated_at: string;
related_case_studies?: CaseStudy[];
}
export interface CaseStudyListResponse {
count: number;
next: string | null;
previous: string | null;
results: CaseStudy[];
}
// Helper function to build query string
const buildQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.append(key, value.toString());
}
});
return searchParams.toString();
};
// Case Study API functions
export const caseStudyService = {
// Get all case studies with optional filtering
getCaseStudies: async (params?: {
category?: string;
client?: string;
search?: string;
featured?: boolean;
ordering?: string;
page?: number;
page_size?: number;
}): Promise<CaseStudyListResponse> => {
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/`;
const response = await fetch(url, {
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;
}
},
// Get a single case study by slug
getCaseStudyBySlug: async (slug: string): Promise<CaseStudy> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/`,
{
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;
}
},
// Get featured case studies
getFeaturedCaseStudies: async (): Promise<CaseStudy[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/featured/`,
{
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;
}
},
// Get latest case studies
getLatestCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/latest/?limit=${limit}`,
{
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;
}
},
// Get popular case studies
getPopularCaseStudies: async (limit: number = 6): Promise<CaseStudy[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/popular/?limit=${limit}`,
{
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;
}
},
// Get related case studies for a specific case study
getRelatedCaseStudies: async (slug: string): Promise<CaseStudy[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/case-studies/${slug}/related/`,
{
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;
}
},
// Get all categories
getCategories: async (): Promise<CaseStudyCategory[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/categories/`,
{
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;
}
},
// Get categories with case studies
getCategoriesWithCaseStudies: async (): Promise<CaseStudyCategory[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/categories/with_case_studies/`,
{
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;
}
},
// Get all clients
getClients: async (): Promise<Client[]> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/clients/`,
{
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;
}
},
// Get a client by slug
getClientBySlug: async (slug: string): Promise<Client> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/clients/${slug}/`,
{
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;
}
},
// Get case studies for a specific client
getClientCaseStudies: async (slug: string): Promise<CaseStudyListResponse> => {
try {
const response = await fetch(
`${API_CONFIG.BASE_URL}/api/case-studies/clients/${slug}/case_studies/`,
{
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 default caseStudyService;

View File

@@ -0,0 +1,131 @@
/**
* Contact API Service
* Handles communication with the Django REST API for contact form submissions
*/
import { API_CONFIG } from '@/lib/config/api';
export interface ContactFormData {
first_name: string;
last_name: string;
email: string;
phone?: string;
company: string;
job_title: string;
industry?: string;
company_size?: string;
project_type?: string;
timeline?: string;
budget?: string;
message: string;
newsletter_subscription: boolean;
privacy_consent: boolean;
}
export interface ContactSubmissionResponse {
message: string;
submission_id: number;
status: string;
}
export interface ApiError {
message: string;
errors?: Record<string, string[]>;
status: number;
}
class ContactApiService {
private baseUrl: string;
constructor() {
this.baseUrl = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTACT}`;
}
/**
* Submit a contact form to the Django API
*/
async submitContactForm(data: ContactFormData): Promise<ContactSubmissionResponse> {
try {
const response = await fetch(`${this.baseUrl}/submissions/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to submit contact form: ${error.message}`);
}
throw new Error('Failed to submit contact form: Unknown error');
}
}
/**
* Get contact submission statistics (admin only)
*/
async getContactStats(): Promise<any> {
try {
const response = await fetch(`${this.baseUrl}/submissions/stats/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch contact stats: ${error.message}`);
}
throw new Error('Failed to fetch contact stats: Unknown error');
}
}
/**
* Get recent contact submissions (admin only)
*/
async getRecentSubmissions(): Promise<any[]> {
try {
const response = await fetch(`${this.baseUrl}/submissions/recent/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch recent submissions: ${error.message}`);
}
throw new Error('Failed to fetch recent submissions: Unknown error');
}
}
}
// Create and export a singleton instance
export const contactApiService = new ContactApiService();
// Export the class for testing purposes
export default ContactApiService;

View File

@@ -0,0 +1,115 @@
import { API_BASE_URL } from '../config/api';
export interface PolicySection {
id: number;
heading: string;
content: string;
order: number;
}
export interface Policy {
id: number;
type: 'privacy' | 'terms' | 'support';
title: string;
slug: string;
description: string;
last_updated: string;
version: string;
effective_date: string;
sections: PolicySection[];
}
export interface PolicyListItem {
id: number;
type: 'privacy' | 'terms' | 'support';
title: string;
slug: string;
description: string;
last_updated: string;
version: string;
}
class PolicyServiceAPI {
private baseUrl = `${API_BASE_URL}/api/policies`;
/**
* Get all policies
*/
async getPolicies(): Promise<PolicyListItem[]> {
try {
const response = await fetch(`${this.baseUrl}/`, {
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 policy by type
*/
async getPolicyByType(type: 'privacy' | 'terms' | 'support'): Promise<Policy> {
try {
const response = await fetch(`${this.baseUrl}/${type}/`, {
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;
}
}
/**
* Get a specific policy by ID
*/
async getPolicyById(id: number): Promise<Policy> {
try {
const response = await fetch(`${this.baseUrl}/${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 a singleton instance
export const policyService = new PolicyServiceAPI();
// Export individual functions for convenience
export const getPolicies = () => policyService.getPolicies();
export const getPolicyByType = (type: 'privacy' | 'terms' | 'support') => policyService.getPolicyByType(type);
export const getPolicyById = (id: number) => policyService.getPolicyById(id);
export default policyService;

View File

@@ -0,0 +1,495 @@
import { API_CONFIG } from '../config/api';
// Types for Service API
export interface ServiceFeature {
id: number;
title: string;
description: string;
icon: string;
display_order: number;
}
export interface ServiceExpertise {
id: number;
title: string;
description: string;
icon: string;
display_order: number;
}
export interface ServiceCategory {
id: number;
name: string;
slug: string;
description: string;
display_order: number;
}
export interface Service {
id: number;
title: string;
description: string;
short_description?: string;
slug: string;
icon: string;
image?: File | string;
image_url?: string;
price: string;
formatted_price: string;
category?: ServiceCategory;
duration?: string;
deliverables?: string;
technologies?: string;
process_steps?: string;
features_description?: string;
deliverables_description?: string;
process_description?: string;
why_choose_description?: string;
expertise_description?: string;
featured: boolean;
display_order: number;
is_active: boolean;
created_at: string;
updated_at: string;
features?: ServiceFeature[];
expertise_items?: ServiceExpertise[];
}
export interface ServiceListResponse {
count: number;
next: string | null;
previous: string | null;
results: Service[];
}
export interface ServiceStats {
total_services: number;
featured_services: number;
categories: number;
average_price: number;
}
export interface ServiceSearchResponse {
query: string;
count: number;
results: Service[];
}
// Helper function to build query string
const buildQueryString = (params: Record<string, any>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
searchParams.append(key, value.toString());
}
});
return searchParams.toString();
};
// Service API functions
export const serviceService = {
// Get all services with optional filtering
getServices: async (params?: {
featured?: boolean;
category?: string;
min_price?: number;
max_price?: number;
search?: string;
ordering?: string;
page?: number;
}): Promise<ServiceListResponse> => {
try {
const queryString = params ? buildQueryString(params) : '';
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch services: ${error.message}`);
}
throw new Error('Failed to fetch services: Unknown error');
}
},
// Get a single service by slug
getServiceBySlug: async (slug: string): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/${slug}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch service: ${error.message}`);
}
throw new Error('Failed to fetch service: Unknown error');
}
},
// Get featured services
getFeaturedServices: async (): Promise<ServiceListResponse> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_FEATURED}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch featured services: ${error.message}`);
}
throw new Error('Failed to fetch featured services: Unknown error');
}
},
// Search services
searchServices: async (query: string): Promise<ServiceSearchResponse> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_SEARCH}/?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to search services: ${error.message}`);
}
throw new Error('Failed to search services: Unknown error');
}
},
// Get service statistics
getServiceStats: async (): Promise<ServiceStats> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_STATS}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch service stats: ${error.message}`);
}
throw new Error('Failed to fetch service stats: Unknown error');
}
},
// Get all service categories
getCategories: async (): Promise<ServiceCategory[]> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_CATEGORIES}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch categories: ${error.message}`);
}
throw new Error('Failed to fetch categories: Unknown error');
}
},
// Get a single category by slug
getCategoryBySlug: async (slug: string): Promise<ServiceCategory> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES_CATEGORIES}/${slug}/`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to fetch category: ${error.message}`);
}
throw new Error('Failed to fetch category: Unknown error');
}
},
// Admin functions (require authentication)
createService: async (serviceData: Partial<Service>): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/create/`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(serviceData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create service: ${error.message}`);
}
throw new Error('Failed to create service: Unknown error');
}
},
updateService: async (slug: string, serviceData: Partial<Service>): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/update/`;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(serviceData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to update service: ${error.message}`);
}
throw new Error('Failed to update service: Unknown error');
}
},
deleteService: async (slug: string): Promise<void> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/delete/`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to delete service: ${error.message}`);
}
throw new Error('Failed to delete service: Unknown error');
}
},
// Upload image for a service
uploadServiceImage: async (slug: string, imageFile: File): Promise<Service> => {
try {
const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SERVICES}/admin/${slug}/upload-image/`;
const formData = new FormData();
formData.append('image', imageFile);
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message ||
errorData.detail ||
`HTTP error! status: ${response.status}`
);
}
const result = await response.json();
return result.service;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to upload service image: ${error.message}`);
}
throw new Error('Failed to upload service image: Unknown error');
}
},
};
// Utility functions
export const serviceUtils = {
// Format price for display
formatPrice: (price: string | number): string => {
const numPrice = typeof price === 'string' ? parseFloat(price) : price;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(numPrice);
},
// Get service image URL
getServiceImageUrl: (service: Service): string => {
// If service has an uploaded image
if (service.image && typeof service.image === 'string' && service.image.startsWith('/media/')) {
return `${API_CONFIG.BASE_URL}${service.image}`;
}
// If service has an image_url
if (service.image_url) {
if (service.image_url.startsWith('http')) {
return service.image_url;
}
return `${API_CONFIG.BASE_URL}${service.image_url}`;
}
// Fallback to default image
return '/images/service/default.png';
},
// Generate service slug from title
generateSlug: (title: string): string => {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
},
// Check if service is featured
isFeatured: (service: Service): boolean => {
return service.featured;
},
// Sort services by display order
sortByDisplayOrder: (services: Service[]): Service[] => {
return [...services].sort((a, b) => a.display_order - b.display_order);
},
// Filter services by category
filterByCategory: (services: Service[], categorySlug: string): Service[] => {
return services.filter(service => service.category?.slug === categorySlug);
},
// Get services within price range
filterByPriceRange: (services: Service[], minPrice: number, maxPrice: number): Service[] => {
return services.filter(service => {
const price = parseFloat(service.price);
return price >= minPrice && price <= maxPrice;
});
},
};

Some files were not shown because too many files have changed in this diff Show More