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

View File

@@ -1,110 +1,125 @@
"use client";
import { useParams } from "next/navigation";
import { useEffect } from "react";
import Link from "next/link";
import { Metadata } from 'next';
import { notFound } 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";
import { JobPosition } from "@/lib/api/careerService";
import { generateCareerMetadata } from "@/lib/seo/metadata";
import { API_CONFIG, getApiHeaders } from "@/lib/config/api";
const JobPage = () => {
const params = useParams();
const slug = params?.slug as string;
const { job, loading, error } = useJob(slug);
interface JobPageProps {
params: Promise<{
slug: string;
}>;
}
// Update metadata dynamically for client component
useEffect(() => {
if (job) {
const metadata = generateCareerMetadata(job);
const title = typeof metadata.title === 'string' ? metadata.title : `Career - ${job.title} | GNX Soft`;
document.title = title;
// Update meta description
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
// Generate static params for all job positions at build time (optional - for better performance)
// This pre-generates known pages, but new pages can still be generated on-demand
export async function generateStaticParams() {
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/career/jobs`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute
}
const description = typeof metadata.description === 'string' ? metadata.description : `Apply for ${job.title} at GNX Soft. ${job.location || 'Remote'} position.`;
metaDescription.setAttribute('content', description);
);
// Update canonical URL
let canonical = document.querySelector('link[rel="canonical"]');
if (!canonical) {
canonical = document.createElement('link');
canonical.setAttribute('rel', 'canonical');
document.head.appendChild(canonical);
}
canonical.setAttribute('href', `${window.location.origin}/career/${job.slug}`);
if (!response.ok) {
console.error('Error fetching jobs for static params:', response.status);
return [];
}
}, [job]);
if (loading) {
const data = await response.json();
const jobs = data.results || data;
return jobs.map((job: JobPosition) => ({
slug: job.slug,
}));
} catch (error) {
console.error('Error generating static params for jobs:', error);
return [];
}
}
// Generate metadata for each job page
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/career/jobs/${slug}`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const job = await response.json();
return generateCareerMetadata({
title: job.title,
description: job.short_description || job.about_role,
slug: job.slug,
location: job.location,
department: job.department,
employment_type: job.employment_type,
});
} catch (error) {
return {
title: 'Job Not Found | GNX Soft',
description: 'The requested job position could not be found.',
};
}
}
const JobPage = async ({ params }: JobPageProps) => {
const { slug } = await params;
try {
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/career/jobs/${slug}`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const job: JobPosition = await response.json();
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>
<JobSingle job={job} />
</main>
<Footer />
<CareerScrollProgressButton />
<CareerInitAnimations />
</div>
);
} catch (error) {
notFound();
}
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>
<Link href="/career" className="btn mt-40">
View All Positions
</Link>
</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

@@ -12,6 +12,7 @@ const montserrat = Montserrat({
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--mont",
preload: false, // Disable preload to prevent warnings
fallback: [
"-apple-system",
"Segoe UI",
@@ -28,6 +29,7 @@ const inter = Inter({
display: "swap",
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
variable: "--inter",
preload: false, // Disable preload to prevent warnings
fallback: [
"-apple-system",
"Segoe UI",
@@ -64,6 +66,8 @@ export default function RootLayout({
return (
<html lang="en" style={{ scrollBehavior: 'auto', overflow: 'auto' }}>
<head>
{/* Suppress scroll-linked positioning warning - expected with GSAP ScrollTrigger */}
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<script
dangerouslySetInnerHTML={{
__html: `
@@ -71,6 +75,20 @@ export default function RootLayout({
history.scrollRestoration = 'manual';
}
window.scrollTo(0, 0);
// Suppress Font Awesome glyph bbox warnings (harmless font rendering warnings)
(function() {
const originalWarn = console.warn;
console.warn = function(...args) {
const message = args.join(' ');
if (message.includes('downloadable font: Glyph bbox') ||
message.includes('Font Awesome') ||
message.includes('glyph ids')) {
return; // Suppress Font Awesome font warnings
}
originalWarn.apply(console, args);
};
})();
`,
}}
/>

View File

@@ -0,0 +1,44 @@
import { Metadata } from 'next';
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
// Force dynamic rendering for policy pages
export const dynamic = 'force-dynamic';
export const dynamicParams = true;
export const revalidate = 0;
// Generate metadata for policy pages
// This prevents Next.js from trying to access undefined searchParams during SSR
export async function generateMetadata(): Promise<Metadata> {
try {
return createMetadata({
title: 'Policies - Privacy Policy, Terms of Use & Support Policy',
description: 'View GNX Soft\'s Privacy Policy, Terms of Use, and Support Policy. Learn about our data protection practices, terms and conditions, and support guidelines.',
keywords: [
'Privacy Policy',
'Terms of Use',
'Support Policy',
'Legal Documents',
'Company Policies',
'Data Protection',
'Terms and Conditions',
],
url: '/policy',
});
} catch (error) {
// Fallback metadata if generation fails
console.error('Error generating metadata for policy page:', error);
return {
title: 'Policies | GNX Soft',
description: 'View GNX Soft\'s Privacy Policy, Terms of Use, and Support Policy.',
};
}
}
export default function PolicyLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -1,21 +1,75 @@
"use client";
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useSearchParams, usePathname } 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';
import { generateMetadata as createMetadata } from "@/lib/seo/metadata";
import { sanitizeHTML } from "@/lib/security/sanitize";
const PolicyContent = () => {
// Component that reads type from URL using Next.js hooks (safe in client components)
const PolicyContentClient = () => {
const searchParams = useSearchParams();
const typeParam = searchParams.get('type') || 'privacy';
const type = typeParam as 'privacy' | 'terms' | 'support';
const pathname = usePathname();
const [type, setType] = useState<'privacy' | 'terms' | 'support'>('privacy');
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Only run on client side
if (typeof window === 'undefined') return;
setMounted(true);
// Get type from URL search params
try {
const urlType = searchParams?.get('type');
if (urlType && ['privacy', 'terms', 'support'].includes(urlType)) {
setType(urlType as 'privacy' | 'terms' | 'support');
} else {
setType('privacy'); // Default fallback
}
} catch (error) {
console.error('Error reading URL type:', error);
setType('privacy'); // Fallback to default
}
}, [searchParams, pathname]);
// If not mounted yet, show loading state
if (!mounted) {
return (
<div style={{ padding: '4rem', textAlign: 'center', minHeight: '50vh' }}>
<div style={{
width: '50px',
height: '50px',
margin: '0 auto 1rem',
border: '4px solid #f3f3f3',
borderTop: '4px solid #daa520',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}}></div>
<p style={{ color: '#64748b' }}>Loading policy...</p>
<style jsx>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
}
return <PolicyContentInner type={type} />;
};
// Inner component that doesn't use useSearchParams
const PolicyContentInner = ({ type }: { type: 'privacy' | 'terms' | 'support' }) => {
const { data: policy, isLoading, error } = usePolicy(type);
// Update metadata based on policy type
useEffect(() => {
// Only run on client side
if (typeof window === 'undefined' || typeof document === 'undefined') return;
const policyTitles = {
privacy: 'Privacy Policy - Data Protection & Privacy',
terms: 'Terms of Use - Terms & Conditions',
@@ -28,30 +82,50 @@ const PolicyContent = () => {
support: 'Learn about GNX Soft\'s Support Policy, including support terms, response times, and service level agreements.',
};
const metadata = createMetadata({
title: policyTitles[type],
description: policyDescriptions[type],
keywords: [
type === 'privacy' ? 'Privacy Policy' : type === 'terms' ? 'Terms of Use' : 'Support Policy',
'Legal Documents',
'Company Policies',
'Data Protection',
'Terms and Conditions',
],
url: `/policy?type=${type}`,
});
try {
// Dynamically import metadata function to avoid SSR issues
import("@/lib/seo/metadata").then(({ generateMetadata: createMetadata }) => {
const metadata = createMetadata({
title: policyTitles[type],
description: policyDescriptions[type],
keywords: [
type === 'privacy' ? 'Privacy Policy' : type === 'terms' ? 'Terms of Use' : 'Support Policy',
'Legal Documents',
'Company Policies',
'Data Protection',
'Terms and Conditions',
],
url: `/policy?type=${type}`,
});
const titleString = typeof metadata.title === 'string' ? metadata.title : `${policyTitles[type]} | GNX Soft`;
document.title = titleString;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
const titleString = typeof metadata.title === 'string' ? metadata.title : `${policyTitles[type]} | GNX Soft`;
document.title = titleString;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
const descriptionString = typeof metadata.description === 'string' ? metadata.description : policyDescriptions[type];
metaDescription.setAttribute('content', descriptionString);
}).catch((error) => {
// Fallback to simple title/description update if metadata import fails
console.warn('Error loading metadata function:', error);
document.title = `${policyTitles[type]} | GNX Soft`;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
metaDescription.setAttribute('content', policyDescriptions[type]);
});
} catch (error) {
// Silently handle metadata errors
console.error('Error setting metadata:', error);
}
const descriptionString = typeof metadata.description === 'string' ? metadata.description : policyDescriptions[type];
metaDescription.setAttribute('content', descriptionString);
}, [type]);
if (isLoading) {
@@ -178,23 +252,49 @@ const PolicyContent = () => {
<div className="col-12 col-lg-10">
{/* Policy Header */}
<div className="policy-header">
<h1 className="policy-title">{policy.title}</h1>
<h1 className="policy-title">{policy.title || 'Policy'}</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>
{policy.last_updated && (
<p className="policy-updated">
Last Updated: {(() => {
try {
return new Date(policy.last_updated).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
} catch {
return new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
})()}
</p>
)}
{policy.version && (
<p className="policy-version">Version {policy.version}</p>
)}
{policy.effective_date && (
<p className="policy-effective">
Effective Date: {(() => {
try {
return new Date(policy.effective_date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
} catch {
return new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
})()}
</p>
)}
</div>
{policy.description && (
<p className="policy-description">{policy.description}</p>
@@ -203,34 +303,42 @@ const PolicyContent = () => {
{/* 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')
}} />
{policy.sections && Array.isArray(policy.sections) && policy.sections.length > 0 ? (
policy.sections.map((section) => (
<div key={section.id || Math.random()} className="policy-section-item">
<h2 className="policy-heading">{section.heading || ''}</h2>
<div className="policy-text" dangerouslySetInnerHTML={{
__html: sanitizeHTML(
(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 className="policy-section-item">
<p>No content available.</p>
</div>
))}
)}
</div>
{/* Contact Section */}
@@ -423,14 +531,17 @@ const PolicyContent = () => {
);
};
// Wrapper component (no longer needs Suspense since we're not using useSearchParams)
const PolicyContentWrapper = () => {
return <PolicyContentClient />;
};
const PolicyPage = () => {
return (
<div className="tp-app">
<Header />
<main>
<Suspense fallback={<div>Loading...</div>}>
<PolicyContent />
</Suspense>
<PolicyContentWrapper />
</main>
<Footer />
</div>

View File

@@ -9,9 +9,10 @@ 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 { Service } from "@/lib/api/serviceService";
import { generateServiceMetadata } from "@/lib/seo/metadata";
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
import { API_CONFIG, getApiHeaders } from "@/lib/config/api";
interface ServicePageProps {
params: Promise<{
@@ -19,23 +20,59 @@ interface ServicePageProps {
}>;
}
// Generate static params for all services (optional - for better performance)
// Generate static params for all services at build time (optional - for better performance)
// This pre-generates known pages, but new pages can still be generated on-demand
export async function generateStaticParams() {
try {
const services = await serviceService.getServices();
return services.results.map((service: Service) => ({
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);
if (!response.ok) {
console.error('Error fetching services for static params:', response.status);
return [];
}
const data = await response.json();
const services = data.results || data;
return services.map((service: Service) => ({
slug: service.slug,
}));
} catch (error) {
console.error('Error generating static params for services:', error);
return [];
}
}
// Generate enhanced metadata for each service page
export async function generateMetadata({ params }: ServicePageProps) {
const { slug } = await params;
try {
const { slug } = await params;
const service = await serviceService.getServiceBySlug(slug);
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/${slug}/`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const service = await response.json();
return generateServiceMetadata(service);
} catch (error) {
@@ -47,23 +84,34 @@ export async function generateMetadata({ params }: ServicePageProps) {
}
const ServicePage = async ({ params }: ServicePageProps) => {
let service: Service;
const { slug } = await params;
try {
const { slug } = await params;
service = await serviceService.getServiceBySlug(slug);
} catch (error) {
notFound();
}
// Use internal API URL for server-side requests
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
const response = await fetch(
`${apiUrl}/api/services/${slug}/`,
{
method: 'GET',
headers: getApiHeaders(),
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
}
);
// Breadcrumb data for structured data
const breadcrumbItems = [
{ name: 'Home', url: '/' },
{ name: 'Services', url: '/services' },
{ name: service.title, url: `/services/${service.slug}` },
];
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return (
const service: Service = await response.json();
// 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} />
@@ -82,7 +130,10 @@ const ServicePage = async ({ params }: ServicePageProps) => {
<ServicesScrollProgressButton />
<ServicesInitAnimations />
</div>
);
);
} catch (error) {
notFound();
}
};
export default ServicePage;

View File

@@ -0,0 +1,13 @@
// Force dynamic rendering for support-center pages
export const dynamic = 'force-dynamic';
export const dynamicParams = true;
export const revalidate = 0;
export default function SupportCenterLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -12,31 +12,40 @@ type ModalType = 'create' | 'knowledge' | 'status' | null;
const SupportCenterPage = () => {
// Set metadata for client component
useEffect(() => {
const metadata = createMetadata({
title: "Support Center - Enterprise Support & Help Desk",
description: "Get 24/7 enterprise support from GNX Soft. Access our knowledge base, create support tickets, check ticket status, and get help with our software solutions and services.",
keywords: [
"Support Center",
"Customer Support",
"Help Desk",
"Technical Support",
"Knowledge Base",
"Support Tickets",
"Enterprise Support",
"IT Support",
],
url: "/support-center",
});
document.title = metadata.title || "Support Center | GNX Soft";
// Only run on client side
if (typeof window === 'undefined' || typeof document === 'undefined') return;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
try {
const metadata = createMetadata({
title: "Support Center - Enterprise Support & Help Desk",
description: "Get 24/7 enterprise support from GNX Soft. Access our knowledge base, create support tickets, check ticket status, and get help with our software solutions and services.",
keywords: [
"Support Center",
"Customer Support",
"Help Desk",
"Technical Support",
"Knowledge Base",
"Support Tickets",
"Enterprise Support",
"IT Support",
],
url: "/support-center",
});
const titleString = typeof metadata.title === 'string' ? metadata.title : "Support Center | GNX Soft";
document.title = titleString;
let metaDescription = document.querySelector('meta[name="description"]');
if (!metaDescription) {
metaDescription = document.createElement('meta');
metaDescription.setAttribute('name', 'description');
document.head.appendChild(metaDescription);
}
metaDescription.setAttribute('content', metadata.description || 'Get enterprise support from GNX Soft');
} catch (error) {
// Silently handle metadata errors
console.error('Error setting metadata:', error);
}
metaDescription.setAttribute('content', metadata.description || 'Get enterprise support from GNX Soft');
}, []);
const [activeModal, setActiveModal] = useState<ModalType>(null);