update
This commit is contained in:
370
frontEnd/components/shared/banners/ServiceDetailsBanner.tsx
Normal file
370
frontEnd/components/shared/banners/ServiceDetailsBanner.tsx
Normal 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: '{service.title}',</span>
|
||||
<span className="code-line"> category: '{service.category?.name}'</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 = 'enterprise';</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: '{service.title}',</span>
|
||||
<span className="code-line"> duration: '{service.duration}'</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;
|
||||
387
frontEnd/components/shared/layout/CookieConsent.tsx
Normal file
387
frontEnd/components/shared/layout/CookieConsent.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
413
frontEnd/components/shared/layout/CookieConsentContext.tsx
Normal file
413
frontEnd/components/shared/layout/CookieConsentContext.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
97
frontEnd/components/shared/layout/CookieConsentUtils.tsx
Normal file
97
frontEnd/components/shared/layout/CookieConsentUtils.tsx
Normal 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)}</>;
|
||||
};
|
||||
62
frontEnd/components/shared/layout/LayoutWrapper.tsx
Normal file
62
frontEnd/components/shared/layout/LayoutWrapper.tsx
Normal 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;
|
||||
405
frontEnd/components/shared/layout/Preloader.css
Normal file
405
frontEnd/components/shared/layout/Preloader.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
151
frontEnd/components/shared/layout/Preloader.tsx
Normal file
151
frontEnd/components/shared/layout/Preloader.tsx
Normal 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;
|
||||
40
frontEnd/components/shared/layout/ScrollToTop.tsx
Normal file
40
frontEnd/components/shared/layout/ScrollToTop.tsx
Normal 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;
|
||||
|
||||
37
frontEnd/components/shared/layout/animations/AppearDown.tsx
Normal file
37
frontEnd/components/shared/layout/animations/AppearDown.tsx
Normal 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;
|
||||
@@ -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;
|
||||
122
frontEnd/components/shared/layout/animations/FadeAnimations.tsx
Normal file
122
frontEnd/components/shared/layout/animations/FadeAnimations.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
107
frontEnd/components/shared/layout/animations/SmoothScroll.tsx
Normal file
107
frontEnd/components/shared/layout/animations/SmoothScroll.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
267
frontEnd/components/shared/layout/footer/Footer.tsx
Normal file
267
frontEnd/components/shared/layout/footer/Footer.tsx
Normal 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">
|
||||
© <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;
|
||||
266
frontEnd/components/shared/layout/header/Header.tsx
Normal file
266
frontEnd/components/shared/layout/header/Header.tsx
Normal 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;
|
||||
204
frontEnd/components/shared/layout/header/OffcanvasMenu.tsx
Normal file
204
frontEnd/components/shared/layout/header/OffcanvasMenu.tsx
Normal 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;
|
||||
378
frontEnd/components/shared/seo/StructuredData.tsx
Normal file
378
frontEnd/components/shared/seo/StructuredData.tsx
Normal 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) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user