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