updates
This commit is contained in:
26
frontEnd/.dockerignore
Normal file
26
frontEnd/.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
coverage
|
||||
.nyc_output
|
||||
dist
|
||||
build
|
||||
README.md
|
||||
*.md
|
||||
|
||||
50
frontEnd/Dockerfile
Normal file
50
frontEnd/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Next.js Frontend Dockerfile
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Set environment variables for build
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Build Next.js
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files from builder
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 1087
|
||||
|
||||
ENV PORT=1087
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
# Use the standalone server
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/shared/layout/header/Header";
|
||||
import JobSingle from "@/components/pages/career/JobSingle";
|
||||
import Footer from "@/components/shared/layout/footer/Footer";
|
||||
@@ -78,9 +79,9 @@ const JobPage = () => {
|
||||
<p className="mt-24">
|
||||
The job position you are looking for does not exist or is no longer available.
|
||||
</p>
|
||||
<a href="/career" className="btn mt-40">
|
||||
<Link href="/career" className="btn mt-40">
|
||||
View All Positions
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,8 @@ const PolicyContent = () => {
|
||||
url: `/policy?type=${type}`,
|
||||
});
|
||||
|
||||
document.title = metadata.title || `${policyTitles[type]} | GNX Soft`;
|
||||
const titleString = typeof metadata.title === 'string' ? metadata.title : `${policyTitles[type]} | GNX Soft`;
|
||||
document.title = titleString;
|
||||
|
||||
let metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (!metaDescription) {
|
||||
@@ -49,7 +50,8 @@ const PolicyContent = () => {
|
||||
metaDescription.setAttribute('name', 'description');
|
||||
document.head.appendChild(metaDescription);
|
||||
}
|
||||
metaDescription.setAttribute('content', metadata.description || policyDescriptions[type]);
|
||||
const descriptionString = typeof metadata.description === 'string' ? metadata.description : policyDescriptions[type];
|
||||
metaDescription.setAttribute('content', descriptionString);
|
||||
}, [type]);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -235,7 +237,7 @@ const PolicyContent = () => {
|
||||
<div className="policy-footer">
|
||||
<div className="contact-box">
|
||||
<h3>Questions?</h3>
|
||||
<p>If you have any questions about this policy, please don't hesitate to contact us.</p>
|
||||
<p>If you have any questions about this policy, please don't hesitate to contact us.</p>
|
||||
<a href="/contact-us" className="btn btn-primary">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ const BlogSingle = () => {
|
||||
</div>
|
||||
<h2 className="text-secondary mb-3">Insight Not Found</h2>
|
||||
<p className="text-tertiary mb-4">
|
||||
The insight you're looking for doesn't exist or has been removed.
|
||||
The insight you're looking for doesn't exist or has been removed.
|
||||
</p>
|
||||
<Link href="/insights" className="btn btn-primary">
|
||||
<i className="fa-solid fa-arrow-left me-2"></i>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { JobPosition } from "@/lib/api/careerService";
|
||||
import JobApplicationForm from "./JobApplicationForm";
|
||||
|
||||
@@ -529,7 +530,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
|
||||
<span className="d-sm-none">~5 min</span>
|
||||
</p>
|
||||
|
||||
<a
|
||||
<Link
|
||||
href="/career"
|
||||
className="btn w-100 mt-12 mt-md-16"
|
||||
style={{
|
||||
@@ -560,7 +561,7 @@ const JobSingle = ({ job }: JobSingleProps) => {
|
||||
<span className="material-symbols-outlined" style={{ fontSize: 'clamp(16px, 3vw, 18px)' }}>arrow_back</span>
|
||||
<span className="d-none d-sm-inline">Back to Career Page</span>
|
||||
<span className="d-sm-none">Back</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ const CaseSingle = ({ slug }: CaseSingleProps) => {
|
||||
<div className="col-12">
|
||||
<div className="error-state">
|
||||
<h2>Case Study Not Found</h2>
|
||||
<p>The case study you're looking for doesn't exist or has been removed.</p>
|
||||
<p>The case study you're looking for doesn't exist or has been removed.</p>
|
||||
<Link href="/case-study" className="btn btn-primary">
|
||||
View All Case Studies
|
||||
</Link>
|
||||
|
||||
@@ -203,7 +203,7 @@ const CreateTicketForm = ({ onOpenStatusCheck }: CreateTicketFormProps) => {
|
||||
</button>
|
||||
</div>
|
||||
<p className="ticket-info">
|
||||
We've received your support request and will respond as soon as possible.
|
||||
We've received your support request and will respond as soon as possible.
|
||||
Please save your ticket number for future reference.
|
||||
</p>
|
||||
<div className="success-actions">
|
||||
|
||||
@@ -236,7 +236,7 @@ const KnowledgeBase = () => {
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<p className="search-info">
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
Found {displayArticles.length} {displayArticles.length === 1 ? 'article' : 'articles'} for "{searchTerm}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -153,11 +153,56 @@ const Footer = () => {
|
||||
</div>
|
||||
<h6 className="cta-title">Ready to Transform Your Business?</h6>
|
||||
<p className="cta-description">Start your software journey with our enterprise solutions, incident management, and custom development services.</p>
|
||||
<div className="cta-button-wrapper">
|
||||
<Link href="/contact-us" className="btn-luxury-cta">
|
||||
<span>Start Your Journey</span>
|
||||
<i className="fa-solid fa-arrow-right"></i>
|
||||
<div className="btn-shine"></div>
|
||||
</Link>
|
||||
<div className="goodfirms-wrapper text-center mt-3" style={{ lineHeight: 0 }}>
|
||||
<Link
|
||||
href="https://www.goodfirms.co/company/gnx-soft-ltd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="goodfirms-badge d-inline-block"
|
||||
title="View our company profile on GoodFirms"
|
||||
style={{
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
textDecoration: 'none',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: 'inline-block',
|
||||
lineHeight: 0,
|
||||
background: 'transparent'
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/images/gnx-goodfirms.webp"
|
||||
alt="GoodFirms Company Profile"
|
||||
width={150}
|
||||
height={80}
|
||||
className="goodfirms-image"
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
objectFit: 'contain',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: 'block',
|
||||
verticalAlign: 'top',
|
||||
borderWidth: 0,
|
||||
borderStyle: 'none',
|
||||
borderColor: 'transparent'
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
@@ -12,6 +12,7 @@ const Header = () => {
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const dropdownTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Fetch services from API
|
||||
const { services: apiServices, loading: servicesLoading, error: servicesError } = useNavigationServices();
|
||||
@@ -112,6 +113,36 @@ const Header = () => {
|
||||
setOpenDropdown(openDropdown === index ? null : index);
|
||||
};
|
||||
|
||||
const handleDropdownEnter = (index: number) => {
|
||||
if (dropdownTimeoutRef.current) {
|
||||
clearTimeout(dropdownTimeoutRef.current);
|
||||
dropdownTimeoutRef.current = null;
|
||||
}
|
||||
if (!isMobile) {
|
||||
setOpenDropdown(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdownLeave = (e: React.MouseEvent) => {
|
||||
if (!isMobile) {
|
||||
// Check if we're moving to the dropdown menu itself
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
const currentTarget = e.currentTarget as HTMLElement;
|
||||
|
||||
// If moving to a child element (dropdown menu), don't close
|
||||
if (relatedTarget && currentTarget.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dropdownTimeoutRef.current) {
|
||||
clearTimeout(dropdownTimeoutRef.current);
|
||||
}
|
||||
dropdownTimeoutRef.current = setTimeout(() => {
|
||||
setOpenDropdown(null);
|
||||
}, 300); // Increased delay to allow for scrolling
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollPosition = window.scrollY;
|
||||
@@ -145,6 +176,9 @@ const Header = () => {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (dropdownTimeoutRef.current) {
|
||||
clearTimeout(dropdownTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -200,12 +234,12 @@ const Header = () => {
|
||||
<div className="navbar__menu d-none d-lg-flex">
|
||||
<ul>
|
||||
{navigationData.map((item) =>
|
||||
item.title === "Support Center" ? null : item.submenu ? (
|
||||
item.title === "Support Center" ? null : item.submenu ? (
|
||||
<li
|
||||
className="navbar__item navbar__item--has-children"
|
||||
key={item.id}
|
||||
onMouseEnter={() => !isMobile && setOpenDropdown(item.id)}
|
||||
onMouseLeave={() => !isMobile && setOpenDropdown(null)}
|
||||
onMouseEnter={() => handleDropdownEnter(item.id)}
|
||||
onMouseLeave={(e) => handleDropdownLeave(e)}
|
||||
>
|
||||
<button
|
||||
aria-label="dropdown menu"
|
||||
@@ -222,7 +256,27 @@ const Header = () => {
|
||||
<span className="loading-indicator">⏳</span>
|
||||
)}
|
||||
</button>
|
||||
<ul className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}>
|
||||
<ul
|
||||
className={`navbar__sub-menu ${openDropdown === item.id ? 'show' : ''}`}
|
||||
onMouseEnter={() => {
|
||||
if (dropdownTimeoutRef.current) {
|
||||
clearTimeout(dropdownTimeoutRef.current);
|
||||
dropdownTimeoutRef.current = null;
|
||||
}
|
||||
if (!isMobile) {
|
||||
setOpenDropdown(item.id);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => handleDropdownLeave(e)}
|
||||
onWheel={(e) => {
|
||||
// Prevent dropdown from closing when scrolling
|
||||
e.stopPropagation();
|
||||
if (dropdownTimeoutRef.current) {
|
||||
clearTimeout(dropdownTimeoutRef.current);
|
||||
dropdownTimeoutRef.current = null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.title === "Services" && servicesLoading ? (
|
||||
<li>
|
||||
<span className="text-muted">Loading services...</span>
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
|
||||
// Production: Use relative URLs (nginx proxy)
|
||||
// Development: Use full backend URL
|
||||
// Docker: Use backend service name or port 1086
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
export const API_BASE_URL = isProduction
|
||||
? '' // Use relative URLs in production (proxied by nginx)
|
||||
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000');
|
||||
const isDocker = process.env.DOCKER_ENV === 'true';
|
||||
|
||||
export const API_BASE_URL = isDocker
|
||||
? (process.env.NEXT_PUBLIC_API_URL || 'http://backend:1086')
|
||||
: isProduction
|
||||
? '' // Use relative URLs in production (proxied by nginx)
|
||||
: (process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000');
|
||||
|
||||
export const API_CONFIG = {
|
||||
// Django API Base URL
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Image utility functions
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE_URL } from './config/api';
|
||||
|
||||
export const FALLBACK_IMAGES = {
|
||||
BLOG: '/images/blog/blog-poster.png',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Enable standalone output for Docker
|
||||
output: 'standalone',
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
BIN
frontEnd/public/images/gnx-goodfirms.webp
Normal file
BIN
frontEnd/public/images/gnx-goodfirms.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 36 KiB |
@@ -747,6 +747,60 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// GoodFirms Badge
|
||||
.goodfirms-badge {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
text-decoration: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: inline-block !important;
|
||||
background: transparent !important;
|
||||
line-height: 0 !important;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:active,
|
||||
&:hover {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.goodfirms-image,
|
||||
img {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
display: block !important;
|
||||
vertical-align: top !important;
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
// Target the actual img element that Next.js Image renders
|
||||
img {
|
||||
border: 0 !important;
|
||||
border-style: none !important;
|
||||
border-width: 0 !important;
|
||||
border-color: transparent !important;
|
||||
border-image: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goodfirms-wrapper {
|
||||
line-height: 0 !important;
|
||||
|
||||
* {
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ====
|
||||
|
||||
@@ -141,13 +141,16 @@
|
||||
min-width: 280px !important;
|
||||
max-width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 9999 !important;
|
||||
backdrop-filter: blur(25px) saturate(180%);
|
||||
pointer-events: none;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// Custom scrollbar styles
|
||||
&::-webkit-scrollbar {
|
||||
@@ -174,6 +177,19 @@
|
||||
visibility: visible !important;
|
||||
transform: translateX(-50%) translateY(0) scale(1) !important;
|
||||
display: block !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
// Bridge element to cover the gap
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
pointer-events: auto;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@@ -188,6 +204,7 @@
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid #1a1a1a;
|
||||
filter: drop-shadow(0 -3px 8px rgba(0, 0, 0, 0.5));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
Reference in New Issue
Block a user