update
@@ -131,10 +131,6 @@ export default function RootLayout({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Console warning
|
|
||||||
console.log('%cSTOP!', 'color: red; font-size: 40px; font-weight: bold;');
|
|
||||||
console.log('%c© GNX Soft - All Rights Reserved', 'font-size: 14px;');
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
`,
|
`,
|
||||||
@@ -150,8 +146,8 @@ export default function RootLayout({
|
|||||||
<CookieConsentProvider
|
<CookieConsentProvider
|
||||||
config={{
|
config={{
|
||||||
companyName: "GNX Soft",
|
companyName: "GNX Soft",
|
||||||
privacyPolicyUrl: "/policy",
|
privacyPolicyUrl: "/policy?type=privacy",
|
||||||
cookiePolicyUrl: "/policy",
|
cookiePolicyUrl: "/policy?type=privacy",
|
||||||
dataControllerEmail: "privacy@gnxsoft.com",
|
dataControllerEmail: "privacy@gnxsoft.com",
|
||||||
retentionPeriod: 365,
|
retentionPeriod: 365,
|
||||||
enableAuditLog: true,
|
enableAuditLog: true,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export async function generateStaticParams() {
|
|||||||
slug: service.slug,
|
slug: service.slug,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating static params:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +53,6 @@ const ServicePage = async ({ params }: ServicePageProps) => {
|
|||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
service = await serviceService.getServiceBySlug(slug);
|
service = await serviceService.getServiceBySlug(slug);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching service:', error);
|
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch dynamic career postings
|
// Fetch dynamic career postings
|
||||||
const careerResponse = await fetch(`${apiUrl}/career/positions/`, {
|
const careerResponse = await fetch(`${apiUrl}/career/jobs`, {
|
||||||
next: { revalidate: 3600 },
|
next: { revalidate: 3600 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,7 +129,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
|
|
||||||
return [...staticPages, ...servicePages, ...blogPages, ...caseStudyPages, ...careerPages];
|
return [...staticPages, ...servicePages, ...blogPages, ...caseStudyPages, ...careerPages];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating sitemap:', error);
|
|
||||||
// Return at least static pages if API fails
|
// Return at least static pages if API fails
|
||||||
return staticPages;
|
return staticPages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.urls import path, include
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import JobPositionViewSet, JobApplicationViewSet
|
from .views import JobPositionViewSet, JobApplicationViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter(trailing_slash=False)
|
||||||
router.register(r'jobs', JobPositionViewSet, basename='job')
|
router.register(r'jobs', JobPositionViewSet, basename='job')
|
||||||
router.register(r'applications', JobApplicationViewSet, basename='application')
|
router.register(r'applications', JobApplicationViewSet, basename='application')
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from rest_framework import viewsets, status, filters
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
import logging
|
import logging
|
||||||
@@ -63,6 +64,7 @@ class JobApplicationViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
queryset = JobApplication.objects.all()
|
queryset = JobApplication.objects.all()
|
||||||
serializer_class = JobApplicationSerializer
|
serializer_class = JobApplicationSerializer
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
filterset_fields = ['job', 'status']
|
filterset_fields = ['job', 'status']
|
||||||
search_fields = ['first_name', 'last_name', 'email', 'job__title']
|
search_fields = ['first_name', 'last_name', 'email', 'job__title']
|
||||||
@@ -85,20 +87,51 @@ class JobApplicationViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""Submit a job application"""
|
"""Submit a job application"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
# Build data dict - Django QueryDict returns lists, so we need to get first item
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Get POST data (text fields)
|
||||||
|
for key in request.POST.keys():
|
||||||
|
value = request.POST.get(key)
|
||||||
|
data[key] = value
|
||||||
|
|
||||||
|
# Get FILES data
|
||||||
|
for key in request.FILES.keys():
|
||||||
|
file_obj = request.FILES.get(key)
|
||||||
|
data[key] = file_obj
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing request: {str(e)}")
|
||||||
|
return Response(
|
||||||
|
{'error': 'Error parsing request data'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=data)
|
||||||
|
|
||||||
|
if not serializer.is_valid():
|
||||||
|
logger.error(f"Validation errors: {serializer.errors}")
|
||||||
|
return Response(
|
||||||
|
{'error': 'Validation failed', 'details': serializer.errors},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Save the application
|
# Save the application
|
||||||
application = serializer.save()
|
application = serializer.save()
|
||||||
|
|
||||||
# Send email notifications
|
|
||||||
email_service = CareerEmailService()
|
|
||||||
email_service.send_application_confirmation(application)
|
|
||||||
email_service.send_application_notification_to_admin(application)
|
|
||||||
|
|
||||||
logger.info(f"New job application received: {application.full_name} for {application.job.title}")
|
logger.info(f"New job application received: {application.full_name} for {application.job.title}")
|
||||||
|
|
||||||
|
# Try to send email notifications (non-blocking - don't fail if emails fail)
|
||||||
|
try:
|
||||||
|
email_service = CareerEmailService()
|
||||||
|
email_service.send_application_confirmation(application)
|
||||||
|
email_service.send_application_notification_to_admin(application)
|
||||||
|
logger.info(f"Email notifications sent successfully for application {application.id}")
|
||||||
|
except Exception as email_error:
|
||||||
|
# Log email error but don't fail the application submission
|
||||||
|
logger.warning(f"Failed to send email notifications for application {application.id}: {str(email_error)}")
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
'message': 'Application submitted successfully',
|
'message': 'Application submitted successfully',
|
||||||
@@ -106,6 +139,7 @@ class JobApplicationViewSet(viewsets.ModelViewSet):
|
|||||||
},
|
},
|
||||||
status=status.HTTP_201_CREATED
|
status=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error submitting job application: {str(e)}", exc_info=True)
|
logger.error(f"Error submitting job application: {str(e)}", exc_info=True)
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@@ -245,26 +245,35 @@ CORS_ALLOW_CREDENTIALS = True
|
|||||||
|
|
||||||
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only allow all origins in development
|
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only allow all origins in development
|
||||||
|
|
||||||
|
# Django URL configuration
|
||||||
|
APPEND_SLASH = True
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
# Production email settings - use SMTP backend
|
# Always use .env configuration for consistency across dev and production
|
||||||
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
# Set USE_SMTP_IN_DEV=True in your .env file to test real email sending in development
|
||||||
EMAIL_HOST = config('EMAIL_HOST', default='mail.gnxsoft.com')
|
USE_SMTP_IN_DEV = config('USE_SMTP_IN_DEV', default=False, cast=bool)
|
||||||
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
|
|
||||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
|
# Email addresses - same for dev and production (from .env)
|
||||||
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=False, cast=bool)
|
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='support@gnxsoft.com')
|
||||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
|
COMPANY_EMAIL = config('COMPANY_EMAIL', default='support@gnxsoft.com')
|
||||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
|
SUPPORT_EMAIL = config('SUPPORT_EMAIL', default=COMPANY_EMAIL)
|
||||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')
|
|
||||||
|
if DEBUG and not USE_SMTP_IN_DEV:
|
||||||
|
# Development: Use console backend to print emails to console (no SMTP required)
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
else:
|
||||||
|
# Production or Dev with SMTP enabled - use SMTP backend
|
||||||
|
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
||||||
|
EMAIL_HOST = config('EMAIL_HOST', default='mail.gnxsoft.com')
|
||||||
|
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
|
||||||
|
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
|
||||||
|
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=False, cast=bool)
|
||||||
|
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
|
||||||
|
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
|
||||||
|
|
||||||
# Email timeout settings for production
|
# Email timeout settings for production
|
||||||
EMAIL_TIMEOUT = config('EMAIL_TIMEOUT', default=30, cast=int)
|
EMAIL_TIMEOUT = config('EMAIL_TIMEOUT', default=30, cast=int)
|
||||||
|
|
||||||
# Company email for contact form notifications
|
|
||||||
COMPANY_EMAIL = config('COMPANY_EMAIL')
|
|
||||||
|
|
||||||
# Support email for ticket notifications
|
|
||||||
SUPPORT_EMAIL = config('SUPPORT_EMAIL', default=config('COMPANY_EMAIL'))
|
|
||||||
|
|
||||||
# Site URL for email links
|
# Site URL for email links
|
||||||
SITE_URL = config('SITE_URL', default='http://localhost:3000')
|
SITE_URL = config('SITE_URL', default='http://localhost:3000')
|
||||||
|
|
||||||
|
|||||||
BIN
gnx-react/backend/media/about/banner/649891.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
gnx-react/backend/media/about/journey/2149512727.jpg
Normal file
|
After Width: | Height: | Size: 785 KiB |
BIN
gnx-react/backend/media/about/process/124061.jpg
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
gnx-react/backend/media/about/services/649891.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Test resume content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Test resume content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
test resume content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Test resume content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Test resume content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fake PDF content
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
test resume
|
||||||
|
After Width: | Height: | Size: 409 KiB |
BIN
gnx-react/backend/media/services/images/backend-engineering.jpg
Normal file
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 490 KiB |
BIN
gnx-react/backend/media/services/images/data-replication.jpg
Normal file
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 274 KiB |
BIN
gnx-react/backend/media/services/images/frontend-engineering.jpg
Normal file
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 349 KiB |
@@ -14,6 +14,7 @@ class PolicyAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ['type', 'is_active', 'last_updated']
|
list_filter = ['type', 'is_active', 'last_updated']
|
||||||
search_fields = ['title', 'type', 'description']
|
search_fields = ['title', 'type', 'description']
|
||||||
prepopulated_fields = {'slug': ('type',)}
|
prepopulated_fields = {'slug': ('type',)}
|
||||||
|
readonly_fields = ('last_updated',)
|
||||||
inlines = [PolicySectionInline]
|
inlines = [PolicySectionInline]
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-10-11 12:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('services', '0006_service_deliverables_description_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='service',
|
||||||
|
name='image2',
|
||||||
|
field=models.ImageField(blank=True, help_text='Secondary service image', null=True, upload_to='services/images/'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='service',
|
||||||
|
name='image2_url',
|
||||||
|
field=models.CharField(blank=True, help_text='External secondary image URL (alternative to uploaded image)', max_length=500),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-10-11 12:49
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('services', '0007_service_image2_service_image2_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='service',
|
||||||
|
name='image2',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='service',
|
||||||
|
name='image2_url',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -89,23 +89,28 @@ class SupportTicketCreateSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def validate_user_email(self, value):
|
def validate_user_email(self, value):
|
||||||
"""
|
"""
|
||||||
Validate that the email is registered and active in the RegisteredEmail model
|
Validate email format and optionally check if registered
|
||||||
"""
|
"""
|
||||||
from .models import RegisteredEmail
|
from .models import RegisteredEmail
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
|
||||||
# Check if email exists and is active in RegisteredEmail model
|
# Basic email format validation
|
||||||
|
try:
|
||||||
|
validate_email(value)
|
||||||
|
except DjangoValidationError:
|
||||||
|
raise serializers.ValidationError("Please enter a valid email address.")
|
||||||
|
|
||||||
|
# Optional: Check if email is registered (for analytics/tracking)
|
||||||
|
# But don't block ticket creation if not registered
|
||||||
try:
|
try:
|
||||||
registered_email = RegisteredEmail.objects.get(email=value)
|
registered_email = RegisteredEmail.objects.get(email=value)
|
||||||
if not registered_email.is_active:
|
if not registered_email.is_active:
|
||||||
raise serializers.ValidationError(
|
# Log this but don't block the ticket
|
||||||
"This email has been deactivated. "
|
logger.warning(f'Ticket submitted by deactivated registered email: {value}')
|
||||||
"Please contact us at support@gnxsoft.com for assistance."
|
|
||||||
)
|
|
||||||
except RegisteredEmail.DoesNotExist:
|
except RegisteredEmail.DoesNotExist:
|
||||||
raise serializers.ValidationError(
|
# This is fine - unregistered users can submit tickets
|
||||||
"This email is not registered in our system. "
|
logger.info(f'Ticket submitted by unregistered email: {value}')
|
||||||
"Please contact us at support@gnxsoft.com to register your email first."
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -133,12 +138,14 @@ class SupportTicketCreateSerializer(serializers.ModelSerializer):
|
|||||||
user_name=ticket.user_name
|
user_name=ticket.user_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update registered email statistics
|
# Update registered email statistics (only if email is registered)
|
||||||
try:
|
try:
|
||||||
registered_email = RegisteredEmail.objects.get(email=validated_data['user_email'])
|
registered_email = RegisteredEmail.objects.get(email=validated_data['user_email'])
|
||||||
registered_email.increment_ticket_count()
|
registered_email.increment_ticket_count()
|
||||||
|
logger.info(f'Updated ticket count for registered email: {validated_data["user_email"]}')
|
||||||
except RegisteredEmail.DoesNotExist:
|
except RegisteredEmail.DoesNotExist:
|
||||||
logger.warning(f'RegisteredEmail not found for {validated_data["user_email"]} after validation')
|
# This is normal now - unregistered users can submit tickets
|
||||||
|
logger.info(f'Ticket created by unregistered email: {validated_data["user_email"]}')
|
||||||
|
|
||||||
# Send email notifications
|
# Send email notifications
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import django
|
|
||||||
|
|
||||||
# Add the backend directory to Python path
|
|
||||||
sys.path.append('/home/gnx/Desktop/gnx-react/backend')
|
|
||||||
|
|
||||||
# Set up Django
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gnx.settings')
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
from about.models import AboutService
|
|
||||||
|
|
||||||
# Update the service image
|
|
||||||
service = AboutService.objects.get(id=1)
|
|
||||||
service.image = 'about/services/service-image.jpg'
|
|
||||||
service.save()
|
|
||||||
|
|
||||||
print(f"Updated service image: {service.image}")
|
|
||||||
print(f"Service image URL: {service.image.url if service.image else 'None'}")
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useState, useRef } from "react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { useServiceManagement } from "@/lib/hooks/useServices";
|
|
||||||
|
|
||||||
interface ServiceImageUploadProps {
|
|
||||||
serviceSlug: string;
|
|
||||||
currentImageUrl?: string;
|
|
||||||
onImageUploaded?: (service: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServiceImageUpload: React.FC<ServiceImageUploadProps> = ({
|
|
||||||
serviceSlug,
|
|
||||||
currentImageUrl,
|
|
||||||
onImageUploaded,
|
|
||||||
}) => {
|
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { uploadServiceImage, loading, error } = useServiceManagement();
|
|
||||||
|
|
||||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
// Validate file type
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
alert('Please select an image file');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size (5MB limit)
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
alert('File size must be less than 5MB');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedFile(file);
|
|
||||||
|
|
||||||
// Create preview URL
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
setPreviewUrl(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpload = async () => {
|
|
||||||
if (!selectedFile) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setUploading(true);
|
|
||||||
const updatedService = await uploadServiceImage(serviceSlug, selectedFile);
|
|
||||||
|
|
||||||
// Clean up preview URL
|
|
||||||
if (previewUrl) {
|
|
||||||
URL.revokeObjectURL(previewUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedFile(null);
|
|
||||||
setPreviewUrl(null);
|
|
||||||
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onImageUploaded) {
|
|
||||||
onImageUploaded(updatedService);
|
|
||||||
}
|
|
||||||
|
|
||||||
alert('Image uploaded successfully!');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Upload failed:', err);
|
|
||||||
alert('Failed to upload image. Please try again.');
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setSelectedFile(null);
|
|
||||||
if (previewUrl) {
|
|
||||||
URL.revokeObjectURL(previewUrl);
|
|
||||||
setPreviewUrl(null);
|
|
||||||
}
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="service-image-upload">
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="image-upload" className="form-label">
|
|
||||||
Service Image
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
id="image-upload"
|
|
||||||
className="form-control"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
disabled={uploading || loading}
|
|
||||||
/>
|
|
||||||
<div className="form-text">
|
|
||||||
Select an image file (JPG, PNG, GIF). Maximum size: 5MB.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Image */}
|
|
||||||
{currentImageUrl && !previewUrl && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Current Image:</label>
|
|
||||||
<div className="current-image">
|
|
||||||
<Image
|
|
||||||
src={currentImageUrl}
|
|
||||||
alt="Current service image"
|
|
||||||
className="img-thumbnail"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
style={{ maxWidth: '200px', maxHeight: '200px', objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Preview */}
|
|
||||||
{previewUrl && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<label className="form-label">Preview:</label>
|
|
||||||
<div className="image-preview">
|
|
||||||
<Image
|
|
||||||
src={previewUrl}
|
|
||||||
alt="Preview"
|
|
||||||
className="img-thumbnail"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
style={{ maxWidth: '200px', maxHeight: '200px', objectFit: 'cover' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Display */}
|
|
||||||
{error && (
|
|
||||||
<div className="alert alert-danger" role="alert">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
{selectedFile && (
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={uploading || loading}
|
|
||||||
>
|
|
||||||
{uploading || loading ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
||||||
Uploading...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Upload Image'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={uploading || loading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ServiceImageUpload;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export default function ContactForm() {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
subject: '',
|
|
||||||
message: ''
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[e.target.name]: e.target.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Simulate form submission
|
|
||||||
setTimeout(() => {
|
|
||||||
setSuccess(true);
|
|
||||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
|
||||||
setLoading(false);
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
return (
|
|
||||||
<div className="alert alert-success" role="alert">
|
|
||||||
<h4 className="alert-heading">Message Sent!</h4>
|
|
||||||
<p>Thank you for your message. We'll get back to you soon.</p>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-success"
|
|
||||||
onClick={() => setSuccess(false)}
|
|
||||||
>
|
|
||||||
Send Another Message
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container py-5">
|
|
||||||
<div className="row justify-content-center">
|
|
||||||
<div className="col-md-8">
|
|
||||||
<h2 className="text-center mb-4">Contact Us</h2>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="alert alert-danger" role="alert">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-6 mb-3">
|
|
||||||
<label htmlFor="name" className="form-label">Name *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6 mb-3">
|
|
||||||
<label htmlFor="email" className="form-label">Email *</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-control"
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="subject" className="form-label">Subject</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="subject"
|
|
||||||
name="subject"
|
|
||||||
value={formData.subject}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="message" className="form-label">Message *</label>
|
|
||||||
<textarea
|
|
||||||
className="form-control"
|
|
||||||
id="message"
|
|
||||||
name="message"
|
|
||||||
rows={5}
|
|
||||||
value={formData.message}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary btn-lg"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
|
||||||
Sending...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Send Message'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
export default function ServicesList() {
|
|
||||||
// Static services data
|
|
||||||
const services = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "Web Development",
|
|
||||||
description: "Custom web applications built with modern technologies",
|
|
||||||
slug: "web-development",
|
|
||||||
icon: "code",
|
|
||||||
price: 2500,
|
|
||||||
featured: true,
|
|
||||||
display_order: 1,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Mobile Apps",
|
|
||||||
description: "Native and cross-platform mobile applications",
|
|
||||||
slug: "mobile-apps",
|
|
||||||
icon: "phone",
|
|
||||||
price: 3500,
|
|
||||||
featured: false,
|
|
||||||
display_order: 2,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleDeleteService = (id: number) => {
|
|
||||||
if (!confirm('Are you sure you want to delete this service?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Note: This is a demo - in a real app, you'd handle deletion differently
|
|
||||||
console.log('Service deletion requested for ID:', id);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container py-5">
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12">
|
|
||||||
<h2 className="mb-4">Our Services</h2>
|
|
||||||
<div className="row">
|
|
||||||
{services.map((service) => (
|
|
||||||
<div key={service.id} className="col-md-6 col-lg-4 mb-4">
|
|
||||||
<div className="card h-100">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="d-flex justify-content-between align-items-start mb-3">
|
|
||||||
<h5 className="card-title">{service.title}</h5>
|
|
||||||
{service.featured && (
|
|
||||||
<span className="badge bg-primary">Featured</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="card-text">{service.description}</p>
|
|
||||||
<div className="mt-auto">
|
|
||||||
<a
|
|
||||||
href={`/services/${service.slug}`}
|
|
||||||
className="btn btn-primary me-2"
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-danger btn-sm"
|
|
||||||
onClick={() => handleDeleteService(service.id)}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
|
||||||
import { Autoplay, Navigation } from "swiper/modules";
|
|
||||||
import "swiper/swiper-bundle.css";
|
|
||||||
import Counter from "../../common/Counter";
|
|
||||||
import one from "@/public/images/study/one.png";
|
|
||||||
import two from "@/public/images/study/two.png";
|
|
||||||
import three from "@/public/images/study/three.png";
|
|
||||||
import four from "@/public/images/study/four.png";
|
|
||||||
import five from "@/public/images/study/five.png";
|
|
||||||
|
|
||||||
const AboutCase = () => {
|
|
||||||
return (
|
|
||||||
<section className="tp-study pt-120 pb-120 bg-quinary">
|
|
||||||
<div className="container">
|
|
||||||
<div className="row vertical-column-gap align-items-center">
|
|
||||||
<div className="col-12 col-lg-7">
|
|
||||||
<div className="tp-lp-title text-center text-lg-start">
|
|
||||||
<div className="enterprise-badge mb-20">
|
|
||||||
<span className="badge enterprise-badge-content">
|
|
||||||
<i className="fa-solid fa-chart-line"></i>
|
|
||||||
Success Stories
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h2 className="mt-8 fw-7 text-secondary title-anim">
|
|
||||||
Enterprise Success Stories
|
|
||||||
</h2>
|
|
||||||
<p className="cur-lg mt-20">
|
|
||||||
Discover how we've helped Fortune 500 companies and innovative startups
|
|
||||||
achieve their digital transformation goals through cutting-edge technology solutions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-lg-5">
|
|
||||||
<div className="tp-study-arrows d-flex justify-content-center justify-content-lg-end">
|
|
||||||
<button className="prev-study" aria-label="previous study">
|
|
||||||
<span className="material-symbols-outlined">west</span>
|
|
||||||
</button>
|
|
||||||
<button className="next-study" aria-label="next study">
|
|
||||||
<span className="material-symbols-outlined">east</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-12">
|
|
||||||
<div className="tp-study-slider-wrapper">
|
|
||||||
<div className="tp-study-slider-wrap">
|
|
||||||
<Swiper
|
|
||||||
slidesPerView={"auto"}
|
|
||||||
spaceBetween={24}
|
|
||||||
slidesPerGroup={1}
|
|
||||||
freeMode={true}
|
|
||||||
speed={1200}
|
|
||||||
loop={true}
|
|
||||||
roundLengths={true}
|
|
||||||
modules={[Autoplay, Navigation]}
|
|
||||||
autoplay={{
|
|
||||||
delay: 5000,
|
|
||||||
disableOnInteraction: false,
|
|
||||||
pauseOnMouseEnter: true,
|
|
||||||
}}
|
|
||||||
className="tp-study-slider"
|
|
||||||
navigation={{
|
|
||||||
nextEl: ".prev-study",
|
|
||||||
prevEl: ".next-study",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={one}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={two}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={three}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={four}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={five}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={one}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={two}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={three}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={four}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="tp-study-slider__single">
|
|
||||||
<div className="thumb">
|
|
||||||
<Link href="case-study-single">
|
|
||||||
<Image
|
|
||||||
src={five}
|
|
||||||
className="w-100 mh-400"
|
|
||||||
alt="Image"
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="content text-center">
|
|
||||||
<h5 className="mt-8 mb-12 text-white fw-5 text-uppercase">
|
|
||||||
<Link href="case-study-single">ENTERPRISE CRM</Link>
|
|
||||||
</h5>
|
|
||||||
<Link href="case-study-single" className="btn-angle">
|
|
||||||
View
|
|
||||||
<span></span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row pt-120 vertical-column-gap-lg">
|
|
||||||
<div className="col-12 col-sm-6 col-xl-3">
|
|
||||||
<div className="counter__single text-center">
|
|
||||||
<h2 className="fw-5 text-secondary mt-8 mb-16">
|
|
||||||
<span className="odometer">
|
|
||||||
<Counter value={500} />
|
|
||||||
</span>
|
|
||||||
<span>+</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-tertiary">Enterprise Projects</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 col-xl-3">
|
|
||||||
<div className="counter__single text-center">
|
|
||||||
<h2 className="fw-5 text-secondary mt-8 mb-16">
|
|
||||||
<span className="odometer">
|
|
||||||
<Counter value={99} />
|
|
||||||
</span>
|
|
||||||
<span>.9%</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-tertiary">Uptime SLA</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 col-xl-3">
|
|
||||||
<div className="counter__single text-center">
|
|
||||||
<h2 className="fw-5 text-secondary mt-8 mb-16">
|
|
||||||
<span className="odometer">
|
|
||||||
<Counter value={15} />
|
|
||||||
</span>
|
|
||||||
<span>+</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-tertiary">Years Experience</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-sm-6 col-xl-3">
|
|
||||||
<div className="counter__single text-center border-0">
|
|
||||||
<h2 className="fw-5 text-secondary mt-8 mb-16">
|
|
||||||
<span className="odometer">
|
|
||||||
<Counter value={200} />
|
|
||||||
</span>
|
|
||||||
<span>+</span>
|
|
||||||
</h2>
|
|
||||||
<p className="text-tertiary">Expert Engineers</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AboutCase;
|
|
||||||
@@ -3,6 +3,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useAbout } from "@/lib/hooks/useAbout";
|
import { useAbout } from "@/lib/hooks/useAbout";
|
||||||
import { AboutService as AboutServiceType, AboutProcess } from "@/lib/api/aboutService";
|
import { AboutService as AboutServiceType, AboutProcess } from "@/lib/api/aboutService";
|
||||||
|
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||||
import thumb from "@/public/images/service/two.png";
|
import thumb from "@/public/images/service/two.png";
|
||||||
import thumbTwo from "@/public/images/service/three.png";
|
import thumbTwo from "@/public/images/service/three.png";
|
||||||
|
|
||||||
@@ -62,13 +63,23 @@ const AboutServiceComponent = () => {
|
|||||||
<Link href="service-single" className="w-100">
|
<Link href="service-single" className="w-100">
|
||||||
<div className="parallax-image-wrap">
|
<div className="parallax-image-wrap">
|
||||||
<div className="parallax-image-inner">
|
<div className="parallax-image-inner">
|
||||||
<Image
|
{serviceData?.image_url ? (
|
||||||
src={serviceData?.image_url || thumb}
|
<img
|
||||||
className="w-100 parallax-image"
|
src={getValidImageUrl(serviceData.image_url, FALLBACK_IMAGES.SERVICE)}
|
||||||
alt="Enterprise Technology Solutions"
|
className="w-100 parallax-image"
|
||||||
width={400}
|
alt="Enterprise Technology Solutions"
|
||||||
height={300}
|
width={400}
|
||||||
/>
|
height={300}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={thumb}
|
||||||
|
className="w-100 parallax-image"
|
||||||
|
alt="Enterprise Technology Solutions"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -241,13 +252,23 @@ const AboutServiceComponent = () => {
|
|||||||
<Link href="service-single" className="w-100">
|
<Link href="service-single" className="w-100">
|
||||||
<div className="parallax-image-wrap">
|
<div className="parallax-image-wrap">
|
||||||
<div className="parallax-image-inner">
|
<div className="parallax-image-inner">
|
||||||
<Image
|
{processData?.image_url ? (
|
||||||
src={processData?.image_url || thumbTwo}
|
<img
|
||||||
className="w-100 parallax-image"
|
src={getValidImageUrl(processData.image_url, FALLBACK_IMAGES.SERVICE)}
|
||||||
alt="Enterprise Development Process"
|
className="w-100 parallax-image"
|
||||||
width={400}
|
alt="Enterprise Development Process"
|
||||||
height={300}
|
width={400}
|
||||||
/>
|
height={300}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={thumbTwo}
|
||||||
|
className="w-100 parallax-image"
|
||||||
|
alt="Enterprise Development Process"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useAbout } from "@/lib/hooks/useAbout";
|
import { useAbout } from "@/lib/hooks/useAbout";
|
||||||
import { AboutJourney } from "@/lib/api/aboutService";
|
import { AboutJourney } from "@/lib/api/aboutService";
|
||||||
|
import { getValidImageUrl, FALLBACK_IMAGES } from "@/lib/imageUtils";
|
||||||
import thumb from "@/public/images/start-thumb.png";
|
import thumb from "@/public/images/start-thumb.png";
|
||||||
|
|
||||||
const AboutStarter = () => {
|
const AboutStarter = () => {
|
||||||
@@ -52,7 +53,7 @@ const AboutStarter = () => {
|
|||||||
const journeyData = data?.journey as AboutJourney | undefined;
|
const journeyData = data?.journey as AboutJourney | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="tp-gallery">
|
<section className="tp-gallery about-compact">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col-12 col-lg-6">
|
<div className="col-12 col-lg-6">
|
||||||
@@ -65,7 +66,7 @@ const AboutStarter = () => {
|
|||||||
{journeyData?.title || "From Startup to Enterprise Leader"}
|
{journeyData?.title || "From Startup to Enterprise Leader"}
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
{journeyData?.description || "Founded in 2008 by three visionary engineers, Itify Technologies began as a small startup with a big dream: to revolutionize how enterprises approach software development. What started as a passion project has grown into a global enterprise software company serving Fortune 500 clients worldwide."}
|
{journeyData?.description || "Founded in 2020 by three visionary engineers, GNX Technologies began as a small startup with a big dream: to revolutionize how enterprises approach software development. What started as a passion project has grown into a global enterprise software company serving Fortune 500 clients worldwide."}
|
||||||
</p>
|
</p>
|
||||||
<div className="journey-milestones">
|
<div className="journey-milestones">
|
||||||
{journeyData?.milestones && journeyData.milestones.length > 0 ? (
|
{journeyData?.milestones && journeyData.milestones.length > 0 ? (
|
||||||
@@ -112,7 +113,7 @@ const AboutStarter = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4">
|
<div className="cta-wrap">
|
||||||
<Link
|
<Link
|
||||||
href={journeyData?.cta_link || "services"}
|
href={journeyData?.cta_link || "services"}
|
||||||
className="btn-line"
|
className="btn-line"
|
||||||
@@ -124,26 +125,123 @@ const AboutStarter = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-6">
|
<div className="col-12 col-lg-6">
|
||||||
<div className="tp-gallery__thumb">
|
<div className="tp-gallery__thumb">
|
||||||
<Image
|
{journeyData?.image_url ? (
|
||||||
src={journeyData?.image_url || thumb}
|
<img
|
||||||
className="w-100"
|
src={getValidImageUrl(journeyData.image_url, FALLBACK_IMAGES.DEFAULT)}
|
||||||
alt="Enterprise Journey"
|
className="w-100 compact-img"
|
||||||
width={500}
|
alt="Enterprise Journey"
|
||||||
height={400}
|
width={500}
|
||||||
style={{
|
height={400}
|
||||||
objectFit: 'contain',
|
/>
|
||||||
borderRadius: '12px',
|
) : (
|
||||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)',
|
<Image
|
||||||
maxWidth: '100%',
|
src={thumb}
|
||||||
maxHeight: '500px',
|
className="w-100 compact-img"
|
||||||
width: '100%',
|
alt="Enterprise Journey"
|
||||||
height: 'auto'
|
width={500}
|
||||||
}}
|
height={400}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<style jsx>{`
|
||||||
|
.about-compact {
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
.about-compact .enterprise-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.about-compact .enterprise-badge i { font-size: 12px; }
|
||||||
|
|
||||||
|
.about-compact .title-anim {
|
||||||
|
font-size: clamp(20px, 2.2vw, 28px);
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
.about-compact p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Milestones */
|
||||||
|
.about-compact .journey-milestones {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.about-compact .journey-milestones { grid-template-columns: 1fr 1fr; }
|
||||||
|
}
|
||||||
|
.about-compact .milestone-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.about-compact .year-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.about-compact .milestone-content h6 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.about-compact .milestone-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA */
|
||||||
|
.about-compact .cta-wrap { margin-top: 12px; }
|
||||||
|
.about-compact :global(.btn-line) {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image */
|
||||||
|
.about-compact .tp-gallery__thumb { margin-top: 8px; }
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.about-compact .tp-gallery__thumb { margin-top: 0; }
|
||||||
|
}
|
||||||
|
.about-compact :global(.compact-img),
|
||||||
|
.about-compact .compact-img {
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
||||||
|
max-height: 260px;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const PostFilterButtons = ({ handleClick, active }: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error loading categories:', error);
|
|
||||||
// Fallback to showing "All" button only
|
// Fallback to showing "All" button only
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ const PostFilterItems = ({ currentPage, onPageChange, onTotalPagesChange, postsP
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error loading posts:', error);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
<PostFilterButtons active={active} handleClick={handleCategoryClick} />
|
||||||
|
|||||||
@@ -91,11 +91,11 @@ const JobSingle = ({ job }: JobSingleProps) => {
|
|||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="job-header-content" style={{ color: 'white' }}>
|
<div className="job-header-content" style={{ color: '#ffffff' }}>
|
||||||
<div className="mb-12 mb-md-16">
|
<div className="mb-12 mb-md-16">
|
||||||
<span className="badge" style={{
|
<span className="badge" style={{
|
||||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||||
color: 'white',
|
color: '#ffffff',
|
||||||
padding: '6px 12px',
|
padding: '6px 12px',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
@@ -110,7 +110,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
|
|||||||
<h1 className="fw-7 mb-16 mb-md-20 mb-lg-24" style={{
|
<h1 className="fw-7 mb-16 mb-md-20 mb-lg-24" style={{
|
||||||
fontSize: 'clamp(1.75rem, 5vw, 3.5rem)',
|
fontSize: 'clamp(1.75rem, 5vw, 3.5rem)',
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
color: 'white'
|
color: '#ffffff'
|
||||||
}}>
|
}}>
|
||||||
{job.title}
|
{job.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ const CaseItems = () => {
|
|||||||
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
|
<h2 className="mt-8 title-anim fw-7 text-secondary mb-24">
|
||||||
Case Studies
|
Case Studies
|
||||||
</h2>
|
</h2>
|
||||||
<p className="cur-lg">
|
<p className="cur-lg">
|
||||||
Lorem ipsum dolor sit amet consectetur. Morbi in non nibh
|
Discover how we help enterprises solve complex challenges with
|
||||||
netus tortor. Non vitae ut consectetur sit quam egestas
|
secure, scalable solutions. Our case studies highlight real
|
||||||
praesent. Enim augue cras donec sed pellentesque tortor
|
business outcomes accelerated delivery, reduced costs,
|
||||||
maecenas. Tellus duis sit justo neque. Est elit diam quam
|
improved reliability, and data-driven growth powered by modern
|
||||||
venenatis sit morbi sed dignissim eros.
|
cloud, AI, and platform engineering.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ const ContactSection = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Contact form submission error:', error);
|
|
||||||
setSubmitStatus({
|
setSubmitStatus({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: error instanceof Error ? error.message : 'Failed to submit form. Please try again.'
|
message: error instanceof Error ? error.message : 'Failed to submit form. Please try again.'
|
||||||
@@ -413,7 +412,7 @@ const ContactSection = () => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label htmlFor="privacy">
|
<label htmlFor="privacy">
|
||||||
I agree to the <a href="/privacy-policy">Privacy Policy</a> and consent to being contacted by our team *
|
I agree to the <a href="/policy?type=privacy">Privacy Policy</a> and consent to being contacted by our team *
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,18 +6,35 @@ const HomeBanner = () => {
|
|||||||
const [currentTextIndex, setCurrentTextIndex] = useState(0);
|
const [currentTextIndex, setCurrentTextIndex] = useState(0);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
// Static banner slides data
|
// Fix viewport height for mobile browsers (especially iOS Safari)
|
||||||
|
useEffect(() => {
|
||||||
|
const setVH = () => {
|
||||||
|
const vh = window.innerHeight * 0.01;
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
setVH();
|
||||||
|
window.addEventListener('resize', setVH);
|
||||||
|
window.addEventListener('orientationchange', setVH);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', setVH);
|
||||||
|
window.removeEventListener('orientationchange', setVH);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Static banner slides data based on actual services
|
||||||
const carouselTexts = [
|
const carouselTexts = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
badge: "Enterprise Solutions",
|
badge: "Custom Development",
|
||||||
icon: "fa-solid fa-shield-halved",
|
icon: "fa-solid fa-code",
|
||||||
heading: "Secure Enterprise Software",
|
heading: "Tailored Enterprise Software ",
|
||||||
highlight: "Development",
|
highlight: "Development",
|
||||||
subheading: "for Modern Businesses",
|
subheading: "Aligned with Your Business Goals",
|
||||||
description: "We build enterprise-grade software solutions with advanced security, scalability, and 24/7 support. Transform your business with our custom development services.",
|
description: "We design and build custom digital solutions that deliver reliable, scalable, and future-ready applications, driving measurable value and competitive advantage for your enterprise.",
|
||||||
button_text: "Explore Solutions",
|
button_text: "Explore Solutions",
|
||||||
button_url: "/services",
|
button_url: "/services/custom-software-development",
|
||||||
is_active: true,
|
is_active: true,
|
||||||
display_order: 1,
|
display_order: 1,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
@@ -25,14 +42,14 @@ const HomeBanner = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
badge: "API Integration",
|
badge: "Business Intelligence",
|
||||||
icon: "fa-solid fa-plug",
|
icon: "fa-solid fa-brain",
|
||||||
heading: "Seamless API",
|
heading: "AI-Powered ",
|
||||||
highlight: "Integration",
|
highlight: "Analytics",
|
||||||
subheading: "& System Connectivity",
|
subheading: "Transform Data into Insights",
|
||||||
description: "Connect all your systems with our robust API development and integration services. Enable smooth data flow and unified workflows across your organization.",
|
description: "Turn enterprise data into actionable intelligence with advanced AI and machine learning, enabling smarter decisions, performance optimization, and data-driven innovation.",
|
||||||
button_text: "Learn More",
|
button_text: "Discover AI Solutions",
|
||||||
button_url: "/services/api-development",
|
button_url: "/services/ai-powered-business-intelligence",
|
||||||
is_active: true,
|
is_active: true,
|
||||||
display_order: 2,
|
display_order: 2,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
@@ -40,18 +57,33 @@ const HomeBanner = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
badge: "Cloud Migration",
|
badge: "System Integration",
|
||||||
icon: "fa-solid fa-cloud",
|
icon: "fa-solid fa-plug",
|
||||||
heading: "Cloud-First",
|
heading: "Enterprise Systems ",
|
||||||
highlight: "Solutions",
|
highlight: "Integration",
|
||||||
subheading: "for Enterprise Scale",
|
subheading: "Seamless Connectivity",
|
||||||
description: "Migrate to the cloud with confidence. Our cloud solutions provide improved scalability, security, and cost-effectiveness for your enterprise operations.",
|
description: "Connect everything — from payment systems and ERP platforms to cloud services, enabling your enterprise to operate seamlessly across physical and digital environments.",
|
||||||
button_text: "Start Migration",
|
button_text: "View Integrations",
|
||||||
button_url: "/services/cloud-solutions",
|
button_url: "/services/external-systems-integrations",
|
||||||
is_active: true,
|
is_active: true,
|
||||||
display_order: 3,
|
display_order: 3,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
badge: "Incident Management",
|
||||||
|
icon: "fa-solid fa-bell",
|
||||||
|
heading: "Intelligent Incident ",
|
||||||
|
highlight: "Management",
|
||||||
|
subheading: "Minimize Downtime & Protect Trust",
|
||||||
|
description: "Cloud-based incident management that empowers teams to detect, respond, and resolve issues faster, reducing downtime and maintaining customer confidence.",
|
||||||
|
button_text: "Learn More",
|
||||||
|
button_url: "/services/incident-management-saas",
|
||||||
|
is_active: true,
|
||||||
|
display_order: 4,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,77 @@
|
|||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import thumb from "@/public/images/service/one.png";
|
import thumb from "@/public/images/leading.jpg";
|
||||||
|
|
||||||
const ServiceIntro = () => {
|
const ServiceIntro = () => {
|
||||||
return (
|
return (
|
||||||
<section className="tp-service pt-120 pb-120">
|
<section className="tp-service pt-120 pb-120">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="row vertical-column-gap-md">
|
<div className="row vertical-column-gap-md">
|
||||||
<div className="col-12 col-lg-5">
|
<div className="col-12 col-lg-4">
|
||||||
<div className="tp-service__thumb fade-img">
|
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
|
||||||
<Link href="service-single" className="w-100">
|
<Link href="services">
|
||||||
<div className="parallax-image-wrap">
|
<Image
|
||||||
<div className="parallax-image-inner">
|
src={thumb}
|
||||||
<Image
|
alt="Enterprise Software Solutions"
|
||||||
src={thumb}
|
width={400}
|
||||||
className="w-100 mh-300 parallax-image"
|
height={500}
|
||||||
alt="Image"
|
objectFit="cover"
|
||||||
/>
|
style={{ display: 'block', border: 'none', margin: 0, padding: 0 }}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</Link>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-7 col-xl-6 offset-xl-1">
|
</div>
|
||||||
<div className="tp-service__content">
|
<div className="col-12 col-lg-8">
|
||||||
<h2 className="mt-8 title-anim text-secondary fw-7 mb-30 text-capitalize">
|
<div className="tp-service__content">
|
||||||
Leading Enterprise Software Solutions Provider.
|
<div className="tp-section-wrapper">
|
||||||
|
|
||||||
|
<h2 className="title-anim text-secondary fw-7 mb-30">
|
||||||
|
Accelerating Digital Transformation Through<br />
|
||||||
|
Mission-Critical Enterprise Software
|
||||||
</h2>
|
</h2>
|
||||||
<div className="pl-100">
|
</div>
|
||||||
<p className="cur-lg">
|
<div className="pl-50">
|
||||||
Transform your enterprise with cutting-edge software solutions,
|
<p className="cur-lg mb-25">
|
||||||
system integrations, and digital transformation services. We help
|
GNX partners with Fortune 40 companies and global enterprises to architect,
|
||||||
Fortune 500 companies modernize their technology infrastructure,
|
develop, and deploy business-critical software solutions that drive measurable
|
||||||
enhance security, and achieve operational excellence through
|
results. Our engineering teams deliver secure, scalable, and compliant systems
|
||||||
innovative software development.
|
that power digital innovation across industries.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-60">
|
<p className="cur-lg mb-30">
|
||||||
<Link href="service-single" className="btn-anim btn-anim-light">
|
From cloud-native architectures and enterprise integration platforms to
|
||||||
Our Solutions
|
AI-powered analytics and legacy modernization, we provide end-to-end
|
||||||
<i className="fa-solid fa-arrow-trend-up"></i>
|
technology solutions that reduce operational costs, enhance efficiency,
|
||||||
<span></span>
|
and deliver competitive advantage.
|
||||||
</Link>
|
</p>
|
||||||
</div>
|
<div className="tp-service__features mb-40">
|
||||||
|
<ul className="list-unstyled">
|
||||||
|
<li className="mb-15">
|
||||||
|
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||||
|
<span className="fw-6">Enterprise-Grade Security & Compliance</span>
|
||||||
|
</li>
|
||||||
|
<li className="mb-15">
|
||||||
|
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||||
|
<span className="fw-6">Scalable Cloud-Native Architectures</span>
|
||||||
|
</li>
|
||||||
|
<li className="mb-15">
|
||||||
|
<i className="fa-solid fa-circle-check text-primary me-10"></i>
|
||||||
|
<span className="fw-6">24/7 Support & Dedicated Engineering Teams</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="mt-60">
|
||||||
|
<Link href="services" className="btn-anim btn-anim-light">
|
||||||
|
Explore Our Solutions
|
||||||
|
<i className="fa-solid fa-arrow-right"></i>
|
||||||
|
<span></span>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ const Story = () => {
|
|||||||
// Log when API data is loaded
|
// Log when API data is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (caseStudies.length > 0) {
|
if (caseStudies.length > 0) {
|
||||||
console.log('Case studies loaded from API:', caseStudies.length);
|
|
||||||
}
|
}
|
||||||
}, [caseStudies]);
|
}, [caseStudies]);
|
||||||
|
|
||||||
@@ -143,11 +142,9 @@ const Story = () => {
|
|||||||
// Just filename or relative path
|
// Just filename or relative path
|
||||||
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
|
imageUrl = `${API_CONFIG.MEDIA_URL}/${item.thumbnail}`;
|
||||||
}
|
}
|
||||||
console.log('Case study image URL:', item.title, '→', imageUrl);
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to static image
|
// Fallback to static image
|
||||||
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
|
imageUrl = getValidImageUrl('/images/case/one.png', FALLBACK_IMAGES.CASE_STUDY);
|
||||||
console.log('Using fallback image for:', item.title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import gsap from "gsap";
|
|||||||
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
|
||||||
import Image from "next/legacy/image";
|
import Image from "next/legacy/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import thumbOne from "@/public/images/service/thumb-one.png";
|
|
||||||
import thumbTwo from "@/public/images/service/thumb-two.png";
|
|
||||||
|
|
||||||
const ServicesBanner = () => {
|
const ServicesBanner = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,12 +54,12 @@ const ServicesBanner = () => {
|
|||||||
<div className="enterprise-banner__content">
|
<div className="enterprise-banner__content">
|
||||||
<div className="banner-badge mb-4">
|
<div className="banner-badge mb-4">
|
||||||
<span className="enterprise-badge">
|
<span className="enterprise-badge">
|
||||||
Professional Services
|
Enterprise Services
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="enterprise-title mb-4">
|
<h1 className="enterprise-title mb-4">
|
||||||
The end-to-end bespoke software development agency you need
|
The end-to-end bespoke software development company you need
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="enterprise-description mb-5">
|
<p className="enterprise-description mb-5">
|
||||||
@@ -106,12 +105,7 @@ const ServicesBanner = () => {
|
|||||||
Scroll
|
Scroll
|
||||||
<span className="arrow"></span>
|
<span className="arrow"></span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="thumb-one">
|
|
||||||
<Image src={thumbOne} alt="Image" width={600} height={400} />
|
|
||||||
</div>
|
|
||||||
<div className="thumb-two">
|
|
||||||
<Image src={thumbTwo} alt="Image" width={600} height={400} />
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,27 @@ const CreateTicketForm = () => {
|
|||||||
category: undefined
|
category: undefined
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setSubmitError(error.message || 'Failed to submit ticket. Please try again.');
|
console.error('Ticket creation error:', error);
|
||||||
|
|
||||||
|
// Provide user-friendly error messages based on error type
|
||||||
|
let errorMessage = 'Failed to submit ticket. Please try again.';
|
||||||
|
|
||||||
|
if (error.message) {
|
||||||
|
if (error.message.includes('email')) {
|
||||||
|
errorMessage = 'There was an issue with your email address. Please check and try again.';
|
||||||
|
} else if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||||
|
errorMessage = 'Network error. Please check your connection and try again.';
|
||||||
|
} else if (error.message.includes('validation')) {
|
||||||
|
errorMessage = 'Please check all required fields and try again.';
|
||||||
|
} else if (error.message.includes('server') || error.message.includes('500')) {
|
||||||
|
errorMessage = 'Server error. Our team has been notified. Please try again later.';
|
||||||
|
} else {
|
||||||
|
// Use the actual error message if it's user-friendly
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitError(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ const KnowledgeBaseArticleModal = ({ slug, onClose }: KnowledgeBaseArticleModalP
|
|||||||
await markArticleHelpful(slug, helpful);
|
await markArticleHelpful(slug, helpful);
|
||||||
setFeedbackGiven(true);
|
setFeedbackGiven(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting feedback:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
'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}
|
|
||||||
* />
|
|
||||||
*/
|
|
||||||
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,55 +49,116 @@ export const CookieConsentBanner: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
{/* Fullscreen overlay to center the banner */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 100, opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ y: 100, opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
transition={{ duration: 0.2 }}
|
||||||
className="cookie-consent-banner"
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 10000,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'rgba(17, 24, 39, 0.45)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="cookie-consent-banner__container">
|
{/* Centered enterprise-style card */}
|
||||||
<div className="cookie-consent-banner__content">
|
<motion.div
|
||||||
<div className="cookie-consent-banner__icon">
|
initial={{ opacity: 0, scale: 0.96, y: 8 }}
|
||||||
<i className="fas fa-cookie-bite"></i>
|
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>
|
||||||
<div className="cookie-consent-banner__text">
|
<div
|
||||||
<h3>Cookie Preferences</h3>
|
className="cookie-consent-banner__actions"
|
||||||
<p>
|
style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, marginTop: 8 }}
|
||||||
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.
|
<button
|
||||||
</p>
|
type="button"
|
||||||
{config.showPrivacyNotice && (
|
className="cookie-consent-banner__btn cookie-consent-banner__btn--secondary"
|
||||||
<div className="cookie-consent-banner__links">
|
onClick={showSettings}
|
||||||
<a href={config.privacyPolicyUrl} target="_blank" rel="noopener noreferrer">
|
style={{
|
||||||
Privacy Policy
|
padding: '10px 14px',
|
||||||
</a>
|
borderRadius: 10,
|
||||||
<a href={config.cookiePolicyUrl} target="_blank" rel="noopener noreferrer">
|
border: '1px solid rgba(255,255,255,0.12)',
|
||||||
Cookie Policy
|
background: 'transparent',
|
||||||
</a>
|
color: '#e5e7eb',
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<div className="cookie-consent-banner__actions">
|
</motion.div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="cookie-consent-banner__btn cookie-consent-banner__btn--secondary"
|
|
||||||
onClick={showSettings}
|
|
||||||
>
|
|
||||||
<i className="fas fa-cog"></i>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="cookie-consent-banner__btn cookie-consent-banner__btn--primary"
|
|
||||||
onClick={acceptNecessary}
|
|
||||||
>
|
|
||||||
<i className="fas fa-check"></i>
|
|
||||||
Accept Functional Only
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ const defaultPreferences: CookiePreferences = {
|
|||||||
const defaultConfig: CookieConsentConfig = {
|
const defaultConfig: CookieConsentConfig = {
|
||||||
version: '2.0',
|
version: '2.0',
|
||||||
companyName: 'Your Company Name',
|
companyName: 'Your Company Name',
|
||||||
privacyPolicyUrl: '/privacy-policy',
|
privacyPolicyUrl: '/policy?type=privacy',
|
||||||
cookiePolicyUrl: '/cookie-policy',
|
cookiePolicyUrl: '/policy?type=privacy',
|
||||||
dataControllerEmail: 'privacy@yourcompany.com',
|
dataControllerEmail: 'privacy@yourcompany.com',
|
||||||
retentionPeriod: 365, // 1 year
|
retentionPeriod: 365, // 1 year
|
||||||
enableAuditLog: true,
|
enableAuditLog: true,
|
||||||
@@ -137,7 +137,6 @@ export const CookieConsentProvider: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load cookie preferences:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show banner if no valid consent found
|
// Show banner if no valid consent found
|
||||||
@@ -171,7 +170,6 @@ export const CookieConsentProvider: React.FC<{
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save cookie preferences:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,7 +269,6 @@ export const CookieConsentProvider: React.FC<{
|
|||||||
try {
|
try {
|
||||||
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to reset cookie preferences:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
@@ -288,7 +285,6 @@ export const CookieConsentProvider: React.FC<{
|
|||||||
try {
|
try {
|
||||||
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to withdraw consent:', error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auditEntry = config.enableAuditLog ? createAuditLogEntry('consent_withdrawn', defaultPreferences) : null;
|
const auditEntry = config.enableAuditLog ? createAuditLogEntry('consent_withdrawn', defaultPreferences) : null;
|
||||||
@@ -386,7 +382,6 @@ export const useFunctional = () => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save user preference:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -397,7 +392,6 @@ export const useFunctional = () => {
|
|||||||
const saved = localStorage.getItem(`user-preference-${key}`);
|
const saved = localStorage.getItem(`user-preference-${key}`);
|
||||||
return saved ? JSON.parse(saved) : defaultValue;
|
return saved ? JSON.parse(saved) : defaultValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load user preference:', error);
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +400,6 @@ export const useFunctional = () => {
|
|||||||
|
|
||||||
const rememberUserAction = (action: string, data?: any) => {
|
const rememberUserAction = (action: string, data?: any) => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
console.log('User Action Remembered:', action, data);
|
|
||||||
// Implement your user action tracking logic here
|
// Implement your user action tracking logic here
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const useFunctional = () => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
localStorage.setItem(`user-preference-${key}`, JSON.stringify(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save user preference:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -37,7 +36,6 @@ export const useFunctional = () => {
|
|||||||
const saved = localStorage.getItem(`user-preference-${key}`);
|
const saved = localStorage.getItem(`user-preference-${key}`);
|
||||||
return saved ? JSON.parse(saved) : defaultValue;
|
return saved ? JSON.parse(saved) : defaultValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load user preference:', error);
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +44,6 @@ export const useFunctional = () => {
|
|||||||
|
|
||||||
const rememberUserAction = (action: string, data?: any) => {
|
const rememberUserAction = (action: string, data?: any) => {
|
||||||
if (canShow) {
|
if (canShow) {
|
||||||
console.log('User Action Remembered:', action, data);
|
|
||||||
// Implement your user action tracking logic here
|
// Implement your user action tracking logic here
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const Header = () => {
|
|||||||
// Find the Services menu item and update its submenu with API data
|
// Find the Services menu item and update its submenu with API data
|
||||||
const servicesIndex = baseNavigation.findIndex(item => item.title === "Services");
|
const servicesIndex = baseNavigation.findIndex(item => item.title === "Services");
|
||||||
if (servicesIndex !== -1 && apiServices.length > 0) {
|
if (servicesIndex !== -1 && apiServices.length > 0) {
|
||||||
console.log('Replacing services with API data:', apiServices);
|
|
||||||
baseNavigation[servicesIndex] = {
|
baseNavigation[servicesIndex] = {
|
||||||
...baseNavigation[servicesIndex],
|
...baseNavigation[servicesIndex],
|
||||||
submenu: apiServices.map(service => ({
|
submenu: apiServices.map(service => ({
|
||||||
@@ -37,8 +36,6 @@ const Header = () => {
|
|||||||
updated_at: service.updated_at
|
updated_at: service.updated_at
|
||||||
}))
|
}))
|
||||||
} as any;
|
} as any;
|
||||||
} else {
|
|
||||||
console.log('Using static services data. API services:', apiServices.length, 'Services index:', servicesIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseNavigation;
|
return baseNavigation;
|
||||||
|
|||||||
11
gnx-react/dev.log
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
> itify@0.1.0 dev
|
||||||
|
> next dev
|
||||||
|
|
||||||
|
▲ Next.js 15.5.3
|
||||||
|
- Local: http://localhost:3000
|
||||||
|
- Network: http://192.168.1.101:3000
|
||||||
|
- Environments: .env.local
|
||||||
|
|
||||||
|
✓ Starting...
|
||||||
|
✓ Ready in 2.4s
|
||||||
@@ -131,7 +131,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching about page data:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +154,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching about banners:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +177,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching about banner ${id}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +200,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching about services:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,7 +223,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching about service ${id}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +246,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching about processes:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +269,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching about process ${id}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,7 +292,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching about journeys:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +315,6 @@ class AboutServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching about journey ${id}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -142,7 +141,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog post:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -164,7 +162,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching featured posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -186,7 +183,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching latest posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -208,7 +204,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching popular posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -230,7 +225,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching related posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -252,7 +246,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return Array.isArray(data) ? data : data.results || [];
|
return Array.isArray(data) ? data : data.results || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog categories:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -274,7 +267,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching categories with posts:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -296,7 +288,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog category:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -318,7 +309,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return Array.isArray(data) ? data : data.results || [];
|
return Array.isArray(data) ? data : data.results || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog tags:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -340,7 +330,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching posts by tag:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -362,7 +351,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return Array.isArray(data) ? data : data.results || [];
|
return Array.isArray(data) ? data : data.results || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching blog authors:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -384,7 +372,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching posts by author:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -406,7 +393,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return Array.isArray(data) ? data : data.results || [];
|
return Array.isArray(data) ? data : data.results || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching comments:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -429,7 +415,6 @@ export const blogService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating comment:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ export interface JobApplication {
|
|||||||
class CareerService {
|
class CareerService {
|
||||||
private baseUrl = `${API_BASE_URL}/api/career`;
|
private baseUrl = `${API_BASE_URL}/api/career`;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all active job positions
|
* Get all active job positions
|
||||||
*/
|
*/
|
||||||
async getAllJobs(): Promise<JobPosition[]> {
|
async getAllJobs(): Promise<JobPosition[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/jobs/`, {
|
const response = await fetch(`${this.baseUrl}/jobs`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -75,7 +76,6 @@ class CareerService {
|
|||||||
// Handle paginated response - extract results array
|
// Handle paginated response - extract results array
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching jobs:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ class CareerService {
|
|||||||
*/
|
*/
|
||||||
async getJobBySlug(slug: string): Promise<JobPosition> {
|
async getJobBySlug(slug: string): Promise<JobPosition> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/jobs/${slug}/`, {
|
const response = await fetch(`${this.baseUrl}/jobs/${slug}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -99,7 +99,6 @@ class CareerService {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching job:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +108,7 @@ class CareerService {
|
|||||||
*/
|
*/
|
||||||
async getFeaturedJobs(): Promise<JobPosition[]> {
|
async getFeaturedJobs(): Promise<JobPosition[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/jobs/featured/`, {
|
const response = await fetch(`${this.baseUrl}/jobs/featured`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -124,7 +123,6 @@ class CareerService {
|
|||||||
// Handle paginated response - extract results array
|
// Handle paginated response - extract results array
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching featured jobs:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,19 +134,15 @@ class CareerService {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// Append all fields to FormData
|
// Required fields
|
||||||
formData.append('job', applicationData.job.toString());
|
formData.append('job', applicationData.job.toString());
|
||||||
formData.append('first_name', applicationData.first_name);
|
formData.append('first_name', applicationData.first_name);
|
||||||
formData.append('last_name', applicationData.last_name);
|
formData.append('last_name', applicationData.last_name);
|
||||||
formData.append('email', applicationData.email);
|
formData.append('email', applicationData.email);
|
||||||
formData.append('consent', applicationData.consent.toString());
|
formData.append('consent', applicationData.consent.toString());
|
||||||
|
formData.append('resume', applicationData.resume);
|
||||||
|
|
||||||
// Append resume file
|
// Optional fields (only append if they exist)
|
||||||
if (applicationData.resume) {
|
|
||||||
formData.append('resume', applicationData.resume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append optional fields
|
|
||||||
if (applicationData.phone) formData.append('phone', applicationData.phone);
|
if (applicationData.phone) formData.append('phone', applicationData.phone);
|
||||||
if (applicationData.current_position) formData.append('current_position', applicationData.current_position);
|
if (applicationData.current_position) formData.append('current_position', applicationData.current_position);
|
||||||
if (applicationData.current_company) formData.append('current_company', applicationData.current_company);
|
if (applicationData.current_company) formData.append('current_company', applicationData.current_company);
|
||||||
@@ -160,24 +154,24 @@ class CareerService {
|
|||||||
if (applicationData.website_url) formData.append('website_url', applicationData.website_url);
|
if (applicationData.website_url) formData.append('website_url', applicationData.website_url);
|
||||||
if (applicationData.available_from) formData.append('available_from', applicationData.available_from);
|
if (applicationData.available_from) formData.append('available_from', applicationData.available_from);
|
||||||
if (applicationData.notice_period) formData.append('notice_period', applicationData.notice_period);
|
if (applicationData.notice_period) formData.append('notice_period', applicationData.notice_period);
|
||||||
if (applicationData.expected_salary) formData.append('expected_salary', applicationData.expected_salary.toString());
|
if (applicationData.expected_salary !== undefined) formData.append('expected_salary', applicationData.expected_salary.toString());
|
||||||
if (applicationData.salary_currency) formData.append('salary_currency', applicationData.salary_currency);
|
if (applicationData.salary_currency) formData.append('salary_currency', applicationData.salary_currency);
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/applications/`, {
|
const response = await fetch(`${this.baseUrl}/applications`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
// Don't set Content-Type header - browser will set it with boundary for multipart/form-data
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.error || `Failed to submit application: ${response.statusText}`);
|
const errorMessage = errorData.error || errorData.message || errorData.detail || `HTTP ${response.status}: ${response.statusText}`;
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting application:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +181,7 @@ class CareerService {
|
|||||||
*/
|
*/
|
||||||
async getJobsByDepartment(department: string): Promise<JobPosition[]> {
|
async getJobsByDepartment(department: string): Promise<JobPosition[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/jobs/?department=${department}`, {
|
const response = await fetch(`${this.baseUrl}/jobs?department=${department}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -202,7 +196,6 @@ class CareerService {
|
|||||||
// Handle paginated response - extract results array
|
// Handle paginated response - extract results array
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching jobs by department:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +205,7 @@ class CareerService {
|
|||||||
*/
|
*/
|
||||||
async getJobsByEmploymentType(employmentType: string): Promise<JobPosition[]> {
|
async getJobsByEmploymentType(employmentType: string): Promise<JobPosition[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/jobs/?employment_type=${employmentType}`, {
|
const response = await fetch(`${this.baseUrl}/jobs?employment_type=${employmentType}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -227,7 +220,6 @@ class CareerService {
|
|||||||
// Handle paginated response - extract results array
|
// Handle paginated response - extract results array
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching jobs by employment type:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching case studies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -140,7 +139,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching case study ${slug}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,7 +163,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching featured case studies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -190,7 +187,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching latest case studies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -215,7 +211,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching popular case studies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -240,7 +235,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching related case studies for ${slug}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -265,7 +259,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching case study categories:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -290,7 +283,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching categories with case studies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -315,7 +307,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching clients:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -340,7 +331,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching client ${slug}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -365,7 +355,6 @@ export const caseStudyService = {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching case studies for client ${slug}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ class PolicyServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching policies:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +74,6 @@ class PolicyServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching policy ${type}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +97,6 @@ class PolicyServiceAPI {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching policy ${id}:`, error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ export const getTicketCategories = async (): Promise<TicketCategory[]> => {
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching ticket categories:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -167,7 +166,6 @@ export const getTicketStatuses = async (): Promise<TicketStatus[]> => {
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching ticket statuses:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -183,7 +181,6 @@ export const getTicketPriorities = async (): Promise<TicketPriority[]> => {
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching ticket priorities:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -204,17 +201,38 @@ export const createTicket = async (data: CreateTicketData): Promise<SupportTicke
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
// Handle validation errors
|
// Handle specific HTTP status codes
|
||||||
if (response.status === 400 && errorData.user_email) {
|
if (response.status === 400) {
|
||||||
throw new Error(errorData.user_email[0] || 'Email validation failed');
|
// Handle validation errors
|
||||||
|
if (errorData.user_email) {
|
||||||
|
throw new Error(errorData.user_email[0] || 'Email validation failed');
|
||||||
|
}
|
||||||
|
if (errorData.title) {
|
||||||
|
throw new Error(errorData.title[0] || 'Title is required');
|
||||||
|
}
|
||||||
|
if (errorData.description) {
|
||||||
|
throw new Error(errorData.description[0] || 'Description is required');
|
||||||
|
}
|
||||||
|
if (errorData.user_name) {
|
||||||
|
throw new Error(errorData.user_name[0] || 'Name is required');
|
||||||
|
}
|
||||||
|
// Generic validation error
|
||||||
|
throw new Error(errorData.detail || errorData.message || 'Please check all required fields and try again');
|
||||||
|
} else if (response.status === 429) {
|
||||||
|
throw new Error('Too many requests. Please wait a moment and try again');
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
throw new Error('Server error. Please try again later');
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
throw new Error('Access denied. Please contact support if this persists');
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
throw new Error('Service not found. Please refresh the page and try again');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(errorData.detail || errorData.message || 'Failed to create ticket');
|
throw new Error(errorData.detail || errorData.message || 'An unexpected error occurred');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating ticket:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -239,7 +257,6 @@ export const checkTicketStatus = async (ticketNumber: string): Promise<SupportTi
|
|||||||
}
|
}
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking ticket status:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -268,7 +285,6 @@ export const addTicketMessage = async (
|
|||||||
if (!response.ok) throw new Error('Failed to add ticket message');
|
if (!response.ok) throw new Error('Failed to add ticket message');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding ticket message:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -284,7 +300,6 @@ export const getKnowledgeBaseCategories = async (): Promise<KnowledgeBaseCategor
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching knowledge base categories:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -303,7 +318,6 @@ export const getKnowledgeBaseArticles = async (search?: string): Promise<Knowled
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching knowledge base articles:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -319,7 +333,6 @@ export const getFeaturedArticles = async (): Promise<KnowledgeBaseArticle[]> =>
|
|||||||
// Handle both array and paginated responses
|
// Handle both array and paginated responses
|
||||||
return Array.isArray(data) ? data : (data.results || data);
|
return Array.isArray(data) ? data : (data.results || data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching featured articles:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -333,7 +346,6 @@ export const getKnowledgeBaseArticle = async (slug: string): Promise<KnowledgeBa
|
|||||||
if (!response.ok) throw new Error('Failed to fetch knowledge base article');
|
if (!response.ok) throw new Error('Failed to fetch knowledge base article');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching knowledge base article:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -349,7 +361,6 @@ export const getArticlesByCategory = async (categorySlug: string): Promise<Knowl
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching articles by category:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -369,7 +380,6 @@ export const markArticleHelpful = async (slug: string, helpful: boolean): Promis
|
|||||||
if (!response.ok) throw new Error('Failed to mark article helpful');
|
if (!response.ok) throw new Error('Failed to mark article helpful');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error marking article helpful:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -385,7 +395,6 @@ export const getSupportSettings = async (): Promise<SupportSettings[]> => {
|
|||||||
// Handle paginated response
|
// Handle paginated response
|
||||||
return data.results || data;
|
return data.results || data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching support settings:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -399,7 +408,6 @@ export const getSupportSetting = async (settingName: string): Promise<SupportSet
|
|||||||
if (!response.ok) throw new Error('Failed to fetch support setting');
|
if (!response.ok) throw new Error('Failed to fetch support setting');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching support setting:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export const useAbout = (): UseAboutReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching about page data:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -87,7 +86,6 @@ export const useAboutBanners = (): UseAboutBannerReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching about banners:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -121,7 +119,6 @@ export const useAboutServices = (): UseAboutServiceReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching about services:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -155,7 +152,6 @@ export const useAboutProcesses = (): UseAboutProcessReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching about processes:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -189,7 +185,6 @@ export const useAboutJourneys = (): UseAboutJourneyReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching about journeys:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const useJobs = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching jobs:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,21 +38,27 @@ export const useJob = (slug: string) => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('🔍 useJob hook called with slug:', slug);
|
||||||
|
|
||||||
if (!slug) {
|
if (!slug) {
|
||||||
|
console.log('❌ No slug provided, setting loading to false');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchJob = async () => {
|
const fetchJob = async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('📡 Fetching job data for slug:', slug);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await careerService.getJobBySlug(slug);
|
const data = await careerService.getJobBySlug(slug);
|
||||||
|
console.log('✅ Job data fetched successfully:', data);
|
||||||
setJob(data);
|
setJob(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('❌ Error fetching job data:', err);
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching job:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
|
console.log('🔄 Setting loading to false');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -81,7 +86,6 @@ export const useFeaturedJobs = () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching featured jobs:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export const useCaseStudies = (params?: {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching case studies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -79,7 +78,6 @@ export const useCaseStudy = (slug: string | null) => {
|
|||||||
setCaseStudy(data);
|
setCaseStudy(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error(`Error fetching case study ${slug}:`, err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -106,7 +104,6 @@ export const useFeaturedCaseStudies = () => {
|
|||||||
setFeaturedCaseStudies(data);
|
setFeaturedCaseStudies(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching featured case studies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -133,7 +130,6 @@ export const useLatestCaseStudies = (limit: number = 6) => {
|
|||||||
setLatestCaseStudies(data);
|
setLatestCaseStudies(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching latest case studies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -160,7 +156,6 @@ export const usePopularCaseStudies = (limit: number = 6) => {
|
|||||||
setPopularCaseStudies(data);
|
setPopularCaseStudies(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching popular case studies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -192,7 +187,6 @@ export const useRelatedCaseStudies = (slug: string | null) => {
|
|||||||
setRelatedCaseStudies(data);
|
setRelatedCaseStudies(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error(`Error fetching related case studies for ${slug}:`, err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -219,7 +213,6 @@ export const useCaseStudyCategories = () => {
|
|||||||
setCategories(data);
|
setCategories(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching case study categories:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -246,7 +239,6 @@ export const useCategoriesWithCaseStudies = () => {
|
|||||||
setCategories(data);
|
setCategories(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching categories with case studies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -273,7 +265,6 @@ export const useClients = () => {
|
|||||||
setClients(data);
|
setClients(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error('Error fetching clients:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -305,7 +296,6 @@ export const useClient = (slug: string | null) => {
|
|||||||
setClient(data);
|
setClient(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error(`Error fetching client ${slug}:`, err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -337,7 +327,6 @@ export const useClientCaseStudies = (slug: string | null) => {
|
|||||||
setCaseStudies(data.results);
|
setCaseStudies(data.results);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
console.error(`Error fetching case studies for client ${slug}:`, err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const usePolicies = (): UsePoliciesReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
console.error('Error fetching policies:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,6 @@ export const usePolicy = (type: 'privacy' | 'terms' | 'support' | null): UsePoli
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error('An error occurred'));
|
setError(err instanceof Error ? err : new Error('An error occurred'));
|
||||||
console.error('Error fetching policy:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -110,7 +108,6 @@ export const usePolicyById = (id: number | null): UsePolicyReturn => {
|
|||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error('An error occurred'));
|
setError(err instanceof Error ? err : new Error('An error occurred'));
|
||||||
console.error('Error fetching policy:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 MiB |
@@ -133,12 +133,12 @@ const nextConfig = {
|
|||||||
// Redirects for SEO
|
// Redirects for SEO
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
// Redirect trailing slashes
|
// Temporarily disabled - causing API issues
|
||||||
{
|
// {
|
||||||
source: '/:path+/',
|
// source: '/((?!api/).*)+/',
|
||||||
destination: '/:path+',
|
// destination: '/$1',
|
||||||
permanent: true,
|
// permanent: true,
|
||||||
},
|
// },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// Rewrites for API proxy (Production: routes /api to backend through nginx)
|
// Rewrites for API proxy (Production: routes /api to backend through nginx)
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Blog category data - Replace with API call to get blog categories
|
|
||||||
export const BlogCategoryButtons = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "All",
|
|
||||||
slug: "all",
|
|
||||||
display_order: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Enterprise Software",
|
|
||||||
slug: "enterprise-software",
|
|
||||||
display_order: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "Digital Transformation",
|
|
||||||
slug: "digital-transformation",
|
|
||||||
display_order: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "System Integration",
|
|
||||||
slug: "system-integration",
|
|
||||||
display_order: 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Cloud Solutions",
|
|
||||||
slug: "cloud-solutions",
|
|
||||||
display_order: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Security",
|
|
||||||
slug: "security",
|
|
||||||
display_order: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: "API Development",
|
|
||||||
slug: "api-development",
|
|
||||||
display_order: 7
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Blog post data - Replace with API call to postsAPI.getAll()
|
|
||||||
import one from "@/public/images/blog/one.png";
|
|
||||||
import two from "@/public/images/blog/two.png";
|
|
||||||
import three from "@/public/images/blog/three.png";
|
|
||||||
import four from "@/public/images/blog/four.png";
|
|
||||||
import five from "@/public/images/blog/five.png";
|
|
||||||
import six from "@/public/images/blog/six.png";
|
|
||||||
import seven from "@/public/images/blog/seven.png";
|
|
||||||
import eight from "@/public/images/blog/eight.png";
|
|
||||||
|
|
||||||
export const BlogPostData = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "The Future of Enterprise Software Architecture",
|
|
||||||
thumb: one,
|
|
||||||
category: "enterprise-software",
|
|
||||||
author: "Sarah Johnson",
|
|
||||||
date: "15 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "Digital Transformation Strategies for Large Enterprises",
|
|
||||||
thumb: two,
|
|
||||||
category: "digital-transformation",
|
|
||||||
author: "Michael Chen",
|
|
||||||
date: "12 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "API-First Approach to System Integration",
|
|
||||||
thumb: three,
|
|
||||||
category: "system-integration",
|
|
||||||
author: "Emily Rodriguez",
|
|
||||||
date: "10 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Cloud Migration Best Practices for Enterprise",
|
|
||||||
thumb: four,
|
|
||||||
category: "cloud-solutions",
|
|
||||||
author: "David Thompson",
|
|
||||||
date: "08 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "Enterprise Security in the Digital Age",
|
|
||||||
thumb: five,
|
|
||||||
category: "security",
|
|
||||||
author: "Sarah Johnson",
|
|
||||||
date: "05 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: "Building Scalable API Architectures",
|
|
||||||
thumb: six,
|
|
||||||
category: "api-development",
|
|
||||||
author: "Michael Chen",
|
|
||||||
date: "03 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: "Microservices Architecture for Enterprise Applications",
|
|
||||||
thumb: seven,
|
|
||||||
category: "enterprise-software",
|
|
||||||
author: "Emily Rodriguez",
|
|
||||||
date: "01 Jan 2024",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: "Data Analytics and Business Intelligence Solutions",
|
|
||||||
thumb: eight,
|
|
||||||
category: "digital-transformation",
|
|
||||||
author: "David Thompson",
|
|
||||||
date: "29 Dec 2023",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
gnx-react/public/images/leading.jpg
Normal file
|
After Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -406,6 +406,10 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 6px 14px;
|
padding: 6px 14px;
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
|
background: rgba(14, 165, 233, 0.15) !important;
|
||||||
|
border-color: rgba(14, 165, 233, 0.5) !important;
|
||||||
|
color: #0ea5e9 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1096,12 +1100,17 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
padding: 4px 10px;
|
padding: 5px 12px;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
|
background: rgba(14, 165, 233, 0.2) !important;
|
||||||
|
border-color: rgba(14, 165, 233, 0.6) !important;
|
||||||
|
color: #3dd5f3 !important;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 10px rgba(14, 165, 233, 0.3);
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1183,6 +1192,354 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra extra small screens (< 375px)
|
||||||
|
@media only screen and (max-width: 374.98px) {
|
||||||
|
.modern-banner {
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-center {
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
.badge-container {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(14, 165, 233, 0.25) !important;
|
||||||
|
border-color: rgba(14, 165, 233, 0.7) !important;
|
||||||
|
color: #5de4ff !important;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 12px rgba(14, 165, 233, 0.4);
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-heading {
|
||||||
|
font-size: clamp(1rem, 7vw, 1.3rem);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
gap: 3px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.cta-primary,
|
||||||
|
.cta-secondary {
|
||||||
|
max-width: 220px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-indicators {
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.trust-item {
|
||||||
|
min-width: 55px;
|
||||||
|
|
||||||
|
.trust-number {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-label {
|
||||||
|
font-size: 6.5px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-background {
|
||||||
|
.gradient-orb {
|
||||||
|
&.orb-1 {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.orb-2 {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.orb-3 {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landscape orientation for phones
|
||||||
|
@media only screen and (max-height: 500px) and (orientation: landscape) {
|
||||||
|
.modern-banner {
|
||||||
|
min-height: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-center {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
.badge-container {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: rgba(14, 165, 233, 0.18) !important;
|
||||||
|
border-color: rgba(14, 165, 233, 0.55) !important;
|
||||||
|
color: #3dd5f3 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 0 8px rgba(14, 165, 233, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-heading {
|
||||||
|
font-size: clamp(1.2rem, 3vh, 1.6rem);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-indicators {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.cta-primary,
|
||||||
|
.cta-secondary {
|
||||||
|
width: auto;
|
||||||
|
max-width: 180px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-indicators {
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.trust-item {
|
||||||
|
min-width: 65px;
|
||||||
|
|
||||||
|
.trust-number {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-label {
|
||||||
|
font-size: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-background {
|
||||||
|
.gradient-orb {
|
||||||
|
opacity: 0.2;
|
||||||
|
|
||||||
|
&.orb-1 {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.orb-2 {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.orb-3 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterprise-bg-elements {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tablet landscape (iPad, etc.)
|
||||||
|
@media only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) {
|
||||||
|
.modern-banner {
|
||||||
|
min-height: 90vh;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-center {
|
||||||
|
max-width: 700px;
|
||||||
|
|
||||||
|
.main-heading {
|
||||||
|
font-size: clamp(2rem, 4vw, 3rem);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-indicators {
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-background {
|
||||||
|
.enterprise-bg-elements {
|
||||||
|
opacity: 0.1;
|
||||||
|
|
||||||
|
.flying-code,
|
||||||
|
.request-response-data,
|
||||||
|
.space-data-generation {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large tablets and small laptops (portrait)
|
||||||
|
@media only screen and (min-width: 768px) and (max-width: 991.98px) and (orientation: portrait) {
|
||||||
|
.modern-banner {
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.content-center {
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0 30px;
|
||||||
|
|
||||||
|
.main-heading {
|
||||||
|
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.cta-primary,
|
||||||
|
.cta-secondary {
|
||||||
|
width: auto;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch device optimizations
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.modern-banner {
|
||||||
|
.content-center {
|
||||||
|
.carousel-indicators {
|
||||||
|
.indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin: 0 2px;
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
.cta-primary,
|
||||||
|
.cta-secondary {
|
||||||
|
min-height: 44px; // Apple's minimum touch target size
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce animations for better performance on mobile devices
|
||||||
|
.banner-background {
|
||||||
|
.enterprise-bg-elements {
|
||||||
|
* {
|
||||||
|
animation-duration: 1.5x !important; // Slow down animations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 1400px) {
|
@media only screen and (min-width: 1400px) {
|
||||||
.tp-banner {
|
.tp-banner {
|
||||||
.tp-banner__content {
|
.tp-banner__content {
|
||||||
|
|||||||
@@ -70,20 +70,29 @@
|
|||||||
h3 {
|
h3 {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
color: var(--secondary-800);
|
color: #ffffff !important;
|
||||||
margin: 0 0 var(--space-2) 0;
|
margin: 0 0 var(--space-2) 0;
|
||||||
line-height: var(--leading-tight);
|
line-height: var(--leading-tight);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
color: var(--secondary-600);
|
color: #ffffff !important;
|
||||||
margin: 0 0 var(--space-3) 0;
|
margin: 0 0 var(--space-3) 0;
|
||||||
line-height: var(--leading-relaxed);
|
line-height: var(--leading-relaxed);
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force white color with higher specificity
|
||||||
|
&.cookie-consent-banner .cookie-consent-banner__text h3 {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cookie-consent-banner .cookie-consent-banner__text p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
&__links {
|
&__links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
@@ -618,11 +627,11 @@
|
|||||||
border-top-color: var(--secondary-700);
|
border-top-color: var(--secondary-700);
|
||||||
|
|
||||||
&__text h3 {
|
&__text h3 {
|
||||||
color: var(--secondary-100);
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text p {
|
&__text p {
|
||||||
color: var(--secondary-300);
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,21 @@
|
|||||||
--------- (4.02) banner styles start ---------
|
--------- (4.02) banner styles start ---------
|
||||||
==== */
|
==== */
|
||||||
|
|
||||||
|
// CSS custom property for dynamic viewport height (better mobile support)
|
||||||
|
:root {
|
||||||
|
--vh: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
// 4.02.01 modern banner styles start
|
// 4.02.01 modern banner styles start
|
||||||
.modern-banner {
|
.modern-banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: -webkit-fill-available; // iOS viewport fix
|
||||||
background: #0a0a0a;
|
background: #0a0a0a;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
overflow-x: hidden; // Prevent horizontal scroll
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -610,6 +619,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 0;
|
padding: 80px 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (max-width: 1199.98px) {
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-content {
|
.banner-content {
|
||||||
@@ -645,6 +671,31 @@
|
|||||||
i {
|
i {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobile visibility enhancement
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
background: linear-gradient(135deg, rgba(30, 64, 175, 0.25), rgba(14, 165, 233, 0.25));
|
||||||
|
border: 2px solid rgba(14, 165, 233, 0.7);
|
||||||
|
color: #3dd5f3;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 15px rgba(14, 165, 233, 0.35),
|
||||||
|
0 4px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
text-shadow: 0 0 8px rgba(14, 165, 233, 0.5);
|
||||||
|
|
||||||
|
i {
|
||||||
|
filter: drop-shadow(0 0 4px rgba(14, 165, 233, 0.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
background: rgba(14, 165, 233, 0.3);
|
||||||
|
border: 2px solid rgba(14, 165, 233, 0.8);
|
||||||
|
color: #5de4ff;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 20px rgba(14, 165, 233, 0.5),
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
text-shadow: 0 0 10px rgba(14, 165, 233, 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,6 +754,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
padding: 8px 0;
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -712,6 +764,9 @@
|
|||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: #0ea5e9;
|
background: #0ea5e9;
|
||||||
@@ -719,10 +774,19 @@
|
|||||||
box-shadow: 0 0 10px rgba(14, 165, 233, 0.5);
|
box-shadow: 0 0 10px rgba(14, 165, 233, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(.active) {
|
||||||
background: rgba(255, 255, 255, 0.6);
|
background: rgba(255, 255, 255, 0.6);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,12 +810,19 @@
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 6px 24px rgba(30, 64, 175, 0.3);
|
box-shadow: 0 6px 24px rgba(30, 64, 175, 0.3);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.4);
|
box-shadow: 0 8px 32px rgba(30, 64, 175, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
@@ -776,6 +847,9 @@
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
@@ -783,6 +857,10 @@
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
@@ -833,6 +911,8 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #0ea5e9;
|
color: #0ea5e9;
|
||||||
@@ -853,6 +933,10 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
|
|||||||