update
This commit is contained in:
@@ -57,11 +57,6 @@ export default function ServicesList() {
|
||||
)}
|
||||
</div>
|
||||
<p className="card-text">{service.description}</p>
|
||||
{service.price && (
|
||||
<p className="text-muted">
|
||||
<strong>Starting at: ${service.price}</strong>
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-auto">
|
||||
<a
|
||||
href={`/services/${service.slug}`}
|
||||
|
||||
@@ -227,18 +227,12 @@ const AboutBanner = () => {
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="social-links">
|
||||
<Link href="https://www.linkedin.com/company/enterprisesoft-solutions" target="_blank" className="social-link">
|
||||
<Link href="https://www.linkedin.com/company/gnxtech" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
<Link href="https://github.com/enterprisesoft" target="_blank" className="social-link">
|
||||
<Link href="https://github.com/gnxtech" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
<Link href="https://www.twitter.com/enterprisesoft" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
<Link href="https://stackoverflow.com/teams/enterprisesoft" target="_blank" className="social-link">
|
||||
<i className="fa-brands fa-stack-overflow"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,10 @@ const AboutServiceComponent = () => {
|
||||
{serviceData?.badge_text || "About Our Company"}
|
||||
</div>
|
||||
<h2 className="title-anim">
|
||||
{serviceData?.title || "Enterprise Technology Leaders"}
|
||||
{serviceData?.title || "GNX Soft Ltd. - Software Excellence"}
|
||||
</h2>
|
||||
<p>
|
||||
{serviceData?.description || "Founded 2008, EnterpriseSoft Solutions has emerged as a premier enterprise software company, serving Fortune 500 companies and innovative startups worldwide. Our team of 200+ engineers, architects, and consultants specializes in delivering mission-critical software solutions that drive digital transformation and business growth."}
|
||||
{serviceData?.description || "Founded in 2020, GNX Soft Ltd. has emerged as a premier enterprise software company, delivering mission-critical software solutions across various industries. Our team of expert engineers, architects, and consultants specializes in custom software development, data replication, incident management, and comprehensive system integrations that drive digital transformation and business growth."}
|
||||
</p>
|
||||
<div className="enterprise-features">
|
||||
<div className="row">
|
||||
@@ -144,7 +144,7 @@ const AboutServiceComponent = () => {
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<h6>Global Reach</h6>
|
||||
<p>Offices in 5 Countries</p>
|
||||
<p>Based in Bulgaria, Serving Worldwide</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,26 +208,6 @@ const BlogSingle = () => {
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
<span>LinkedIn</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://twitter.com/intent/tweet?url=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}&text=${encodeURIComponent(post.title)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-twitter"
|
||||
aria-label="Share on Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
<span>Twitter</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${typeof window !== 'undefined' ? encodeURIComponent(window.location.href) : ''}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="share-btn share-facebook"
|
||||
aria-label="Share on Facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
<span>Facebook</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined' && navigator.clipboard) {
|
||||
|
||||
@@ -114,38 +114,20 @@ const CareerBanner = () => {
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.facebook.com/"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.pinterest.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on pinterest"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.instagram.com/"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on instagram"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-instagram"></i>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -59,7 +59,7 @@ const ServiceDetails = ({ service }: ServiceDetailsProps) => {
|
||||
|
||||
<div className="enterprise-stats mb-4">
|
||||
<div className="row g-3">
|
||||
<div className="col-6">
|
||||
<div className="col-12">
|
||||
<div className="enterprise-stat-card">
|
||||
<div className="stat-icon">
|
||||
<i className="fa-solid fa-star"></i>
|
||||
@@ -72,19 +72,6 @@ const ServiceDetails = ({ service }: ServiceDetailsProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="enterprise-stat-card">
|
||||
<div className="stat-icon">
|
||||
<i className="fa-solid fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<div className="stat-number">
|
||||
{service.formatted_price}
|
||||
</div>
|
||||
<div className="stat-label">Starting Price</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -137,9 +137,6 @@ const ServiceMain = () => {
|
||||
</p>
|
||||
|
||||
<div className="service-footer">
|
||||
<div className="service-price">
|
||||
{service.formatted_price}
|
||||
</div>
|
||||
<Link href={`/services/${service.slug}`} className="service-link">
|
||||
<span>View Details</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
|
||||
@@ -61,11 +61,8 @@ const ServicePricing = ({ service }: ServicePricingProps) => {
|
||||
{service.title}
|
||||
</h3>
|
||||
<div className="price-display">
|
||||
<span className="price-amount">
|
||||
{service.formatted_price}
|
||||
</span>
|
||||
<span className="price-period">
|
||||
Starting Price
|
||||
Contact Us for Pricing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,38 +85,20 @@ const ServicesBanner = () => {
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.facebook.com/"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.pinterest.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on pinterest"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.instagram.com/"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on instagram"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-instagram"></i>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -48,14 +48,6 @@ const Transform = ({ service }: TransformProps) => {
|
||||
<p className="enterprise-section-description">
|
||||
{service.description}
|
||||
</p>
|
||||
{service.formatted_price && (
|
||||
<div className="mt-4">
|
||||
<div className="price-highlight">
|
||||
<span className="price-label">Starting from</span>
|
||||
<span className="price-value">{service.formatted_price}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
207
gnx-react/components/shared/OptimizedImage.tsx
Normal file
207
gnx-react/components/shared/OptimizedImage.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface OptimizedImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
priority?: boolean;
|
||||
fill?: boolean;
|
||||
sizes?: string;
|
||||
quality?: number;
|
||||
style?: React.CSSProperties;
|
||||
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
loading?: 'lazy' | 'eager';
|
||||
}
|
||||
|
||||
/**
|
||||
* OptimizedImage Component
|
||||
*
|
||||
* An enterprise-grade optimized image component that provides:
|
||||
* - Automatic lazy loading
|
||||
* - Responsive images with srcset
|
||||
* - WebP/AVIF format support
|
||||
* - Blur placeholder while loading
|
||||
* - Error handling with fallback
|
||||
* - Performance optimization
|
||||
*
|
||||
* @example
|
||||
* <OptimizedImage
|
||||
* src="/images/hero.jpg"
|
||||
* alt="Hero banner showcasing our services"
|
||||
* width={1200}
|
||||
* height={600}
|
||||
* priority={true}
|
||||
* />
|
||||
*/
|
||||
export default function OptimizedImage({
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
className = '',
|
||||
priority = false,
|
||||
fill = false,
|
||||
sizes,
|
||||
quality = 85,
|
||||
style,
|
||||
objectFit = 'cover',
|
||||
loading,
|
||||
}: OptimizedImageProps) {
|
||||
const [imgSrc, setImgSrc] = useState(src);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
// Fallback image for errors
|
||||
const fallbackImage = '/images/placeholder.png';
|
||||
|
||||
// Handle image load
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Handle image error
|
||||
const handleError = () => {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
if (imgSrc !== fallbackImage) {
|
||||
setImgSrc(fallbackImage);
|
||||
}
|
||||
};
|
||||
|
||||
// SEO-friendly alt text validation
|
||||
const seoAlt = alt || 'GNX Soft - Enterprise Software Solutions';
|
||||
|
||||
// Validate alt text for SEO
|
||||
if (process.env.NODE_ENV === 'development' && !alt) {
|
||||
console.warn(
|
||||
`OptimizedImage: Missing alt text for image "${src}". Alt text is crucial for SEO and accessibility.`
|
||||
);
|
||||
}
|
||||
|
||||
// Common image props
|
||||
const imageProps = {
|
||||
src: imgSrc,
|
||||
alt: seoAlt,
|
||||
className: `${className} ${isLoading ? 'image-loading' : 'image-loaded'}`,
|
||||
onLoad: handleLoad,
|
||||
onError: handleError,
|
||||
quality,
|
||||
loading: loading || (priority ? 'eager' : 'lazy'),
|
||||
style: {
|
||||
...style,
|
||||
objectFit: objectFit as any,
|
||||
},
|
||||
};
|
||||
|
||||
// Use fill layout for responsive images
|
||||
if (fill) {
|
||||
return (
|
||||
<div className={`optimized-image-wrapper ${hasError ? 'has-error' : ''}`}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
fill
|
||||
sizes={sizes || '100vw'}
|
||||
priority={priority}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.optimized-image-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.optimized-image-wrapper.has-error {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
:global(.image-loading) {
|
||||
filter: blur(10px);
|
||||
transform: scale(1.1);
|
||||
transition: filter 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
:global(.image-loaded) {
|
||||
filter: blur(0);
|
||||
transform: scale(1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Standard layout with explicit dimensions
|
||||
return (
|
||||
<div className={`optimized-image-container ${hasError ? 'has-error' : ''}`}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
width={width}
|
||||
height={height}
|
||||
sizes={sizes}
|
||||
priority={priority}
|
||||
/>
|
||||
<style jsx>{`
|
||||
.optimized-image-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.optimized-image-container.has-error {
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
:global(.image-loading) {
|
||||
filter: blur(10px);
|
||||
transform: scale(1.05);
|
||||
transition: filter 0.4s ease, transform 0.4s ease;
|
||||
}
|
||||
:global(.image-loaded) {
|
||||
filter: blur(0);
|
||||
transform: scale(1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage Examples:
|
||||
*
|
||||
* 1. Hero Image (Priority Loading):
|
||||
* <OptimizedImage
|
||||
* src="/images/hero.jpg"
|
||||
* alt="Enterprise software development solutions"
|
||||
* width={1920}
|
||||
* height={1080}
|
||||
* priority={true}
|
||||
* sizes="100vw"
|
||||
* />
|
||||
*
|
||||
* 2. Service Card Image (Lazy Loading):
|
||||
* <OptimizedImage
|
||||
* src="/images/service/custom-software.jpg"
|
||||
* alt="Custom software development service icon"
|
||||
* width={400}
|
||||
* height={300}
|
||||
* sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
* />
|
||||
*
|
||||
* 3. Background Image (Fill):
|
||||
* <OptimizedImage
|
||||
* src="/images/background.jpg"
|
||||
* alt="Technology background pattern"
|
||||
* fill={true}
|
||||
* sizes="100vw"
|
||||
* objectFit="cover"
|
||||
* />
|
||||
*
|
||||
* 4. Logo (High Priority):
|
||||
* <OptimizedImage
|
||||
* src="/images/logo.png"
|
||||
* alt="GNX Soft company logo"
|
||||
* width={200}
|
||||
* height={50}
|
||||
* priority={true}
|
||||
* quality={100}
|
||||
* />
|
||||
*/
|
||||
|
||||
57
gnx-react/components/shared/ProtectedImage.tsx
Normal file
57
gnx-react/components/shared/ProtectedImage.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, CSSProperties } from 'react';
|
||||
|
||||
interface ProtectedImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showWatermark?: boolean;
|
||||
priority?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected Image Component
|
||||
* Wraps images with protection against downloading and copying
|
||||
*/
|
||||
export default function ProtectedImage({
|
||||
src,
|
||||
alt,
|
||||
className = '',
|
||||
style = {},
|
||||
width,
|
||||
height,
|
||||
showWatermark = false,
|
||||
children
|
||||
}: ProtectedImageProps) {
|
||||
const wrapperClass = `protected-image-wrapper ${showWatermark ? 'watermarked-image' : ''} ${className}`;
|
||||
|
||||
return (
|
||||
<div className={wrapperClass} style={style}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
draggable="false"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
style={{
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -309,19 +309,6 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.formatted_price && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card">
|
||||
<div className="highlight-icon">
|
||||
<i className="fa-solid fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div className="highlight-content">
|
||||
<span className="highlight-label">Starting From</span>
|
||||
<span className="highlight-value">{service.formatted_price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{service.featured && (
|
||||
<div className="col-auto">
|
||||
<div className="highlight-card featured">
|
||||
@@ -355,38 +342,20 @@ const ServiceDetailsBanner = ({ service }: ServiceDetailsBannerProps) => {
|
||||
<ul className="social">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.facebook.com/"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on facebook"
|
||||
>
|
||||
<i className="fa-brands fa-facebook-f"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.pinterest.com/"
|
||||
target="_blank"
|
||||
aria-label="share us on pinterest"
|
||||
aria-label="connect with us on linkedin"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin-in"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.instagram.com/"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="share us on instagram"
|
||||
aria-label="view our code on github"
|
||||
>
|
||||
<i className="fa-brands fa-instagram"></i>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -243,40 +243,19 @@ const Footer = () => {
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="social justify-content-center justify-content-lg-end">
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/itify"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<i className="fa-brands fa-linkedin"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/itify"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
<i className="fa-brands fa-github"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://www.twitter.com/itify"
|
||||
target="_blank"
|
||||
title="Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://www.youtube.com/c/itify"
|
||||
target="_blank"
|
||||
title="YouTube"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://stackoverflow.com/teams/itify"
|
||||
target="_blank"
|
||||
title="Stack Overflow"
|
||||
>
|
||||
<i className="fa-brands fa-stack-overflow"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const Header = () => {
|
||||
created_at: service.created_at,
|
||||
updated_at: service.updated_at
|
||||
}))
|
||||
};
|
||||
} as any;
|
||||
} else {
|
||||
console.log('Using static services data. API services:', apiServices.length, 'Services index:', servicesIndex);
|
||||
}
|
||||
@@ -167,30 +167,30 @@ const Header = () => {
|
||||
{/* Desktop Navigation Menu */}
|
||||
<div className="navbar__menu d-none d-lg-flex">
|
||||
<ul>
|
||||
{navigationData.map((item, index) =>
|
||||
item.submenu ? (
|
||||
{navigationData.map((item) =>
|
||||
item.title === "Support Center" ? null : item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children"
|
||||
key={index}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(index)}
|
||||
key={item.id}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
|
||||
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
|
||||
>
|
||||
<button
|
||||
aria-label="dropdown menu"
|
||||
className={
|
||||
"navbar__dropdown-label" +
|
||||
(openDropdown === index
|
||||
(openDropdown === item.id
|
||||
? " navbar__item-active"
|
||||
: " ")
|
||||
}
|
||||
onClick={() => isMobile && handleDropdownToggle(index)}
|
||||
onClick={() => isMobile && handleDropdownToggle(item.id)}
|
||||
>
|
||||
{item.title}
|
||||
{item.title === "Services" && servicesLoading && (
|
||||
<span className="loading-indicator">⏳</span>
|
||||
)}
|
||||
</button>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === index ? 'show' : ''}`}>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}>
|
||||
{item.title === "Services" && servicesLoading ? (
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
@@ -218,7 +218,10 @@ const Header = () => {
|
||||
</ul>
|
||||
</li>
|
||||
) : (
|
||||
<li className="navbar__item" key={index}>
|
||||
<li
|
||||
className="navbar__item"
|
||||
key={item.id}
|
||||
>
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
|
||||
@@ -25,12 +25,18 @@ const OffcanvasMenu = ({
|
||||
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"
|
||||
@@ -51,6 +57,7 @@ const OffcanvasMenu = ({
|
||||
className={
|
||||
"offcanvas-menu" + (isOffcanvasOpen ? " show-offcanvas-menu" : " ")
|
||||
}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<nav
|
||||
className={
|
||||
@@ -115,7 +122,7 @@ const OffcanvasMenu = ({
|
||||
<Link
|
||||
href={subItem.path || "#"}
|
||||
className={
|
||||
pathname === subItem.path
|
||||
mounted && pathname === subItem.path
|
||||
? " active-current-sub"
|
||||
: " "
|
||||
}
|
||||
@@ -133,7 +140,7 @@ const OffcanvasMenu = ({
|
||||
<Link
|
||||
href={item.path || "#"}
|
||||
className={
|
||||
pathname === item.path ? " active-current-link" : " "
|
||||
mounted && pathname === item.path ? " active-current-link" : " "
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
@@ -150,13 +157,13 @@ const OffcanvasMenu = ({
|
||||
<h4>Get in Touch</h4>
|
||||
<p>Ready to transform your business?</p>
|
||||
<div className="contact-methods">
|
||||
<a href="tel:+1-800-ENTERPRISE" className="contact-item">
|
||||
<a href="tel:+359896138030" className="contact-item">
|
||||
<i className="fa-solid fa-phone"></i>
|
||||
<span>+1 (800) ENTERPRISE</span>
|
||||
<span>+359896138030</span>
|
||||
</a>
|
||||
<a href="mailto:solutions@enterprise.com" className="contact-item">
|
||||
<a href="mailto:info@gnxsoft.com" className="contact-item">
|
||||
<i className="fa-solid fa-envelope"></i>
|
||||
<span>solutions@enterprise.com</span>
|
||||
<span>info@gnxsoft.com</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +171,7 @@ const OffcanvasMenu = ({
|
||||
<ul className="enterprise-social nav-fade">
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.linkedin.com/company/enterprise"
|
||||
href="https://www.linkedin.com/company/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="Connect with us on LinkedIn"
|
||||
>
|
||||
@@ -173,25 +180,7 @@ const OffcanvasMenu = ({
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.twitter.com/enterprise"
|
||||
target="_blank"
|
||||
aria-label="Follow us on Twitter"
|
||||
>
|
||||
<i className="fa-brands fa-twitter"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://www.youtube.com/enterprise"
|
||||
target="_blank"
|
||||
aria-label="Watch our videos on YouTube"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="https://github.com/enterprise"
|
||||
href="https://github.com/gnxtech"
|
||||
target="_blank"
|
||||
aria-label="View our code on GitHub"
|
||||
>
|
||||
|
||||
378
gnx-react/components/shared/seo/StructuredData.tsx
Normal file
378
gnx-react/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