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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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