updates
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
44
frontEnd/app/policy/layout.tsx
Normal file
44
frontEnd/app/policy/layout.tsx
Normal 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}</>;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
13
frontEnd/app/support-center/layout.tsx
Normal file
13
frontEnd/app/support-center/layout.tsx
Normal 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}</>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user