updates
This commit is contained in:
@@ -15,17 +15,51 @@ interface JobPageProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
// Generate static params for all job positions at build time (optional - for better performance)
|
||||
// This pre-generates known pages, but new pages can still be generated on-demand
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/career/jobs`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
next: { revalidate: 60 }, // Revalidate every minute
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Error fetching jobs for static params:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const jobs = data.results || data;
|
||||
|
||||
return jobs.map((job: JobPosition) => ({
|
||||
slug: job.slug,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error generating static params for jobs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate metadata for each job page
|
||||
export async function generateMetadata({ params }: JobPageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
|
||||
`${apiUrl}/api/career/jobs/${slug}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
next: { revalidate: 3600 }, // Revalidate every hour
|
||||
next: { revalidate: 60 }, // Revalidate every minute
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,12 +89,14 @@ const JobPage = async ({ params }: JobPageProps) => {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${API_CONFIG.BASE_URL}/api/career/jobs/${slug}/`,
|
||||
`${apiUrl}/api/career/jobs/${slug}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
next: { revalidate: 3600 }, // Revalidate every hour
|
||||
next: { revalidate: 60 }, // Revalidate every minute
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import ServicesInitAnimations from "@/components/pages/services/ServicesInitAnim
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
import { generateServiceMetadata } from "@/lib/seo/metadata";
|
||||
import { ServiceSchema, BreadcrumbSchema } from "@/components/shared/seo/StructuredData";
|
||||
import { API_CONFIG, getApiHeaders } from "@/lib/config/api";
|
||||
import { API_CONFIG } from "@/lib/config/api";
|
||||
|
||||
interface ServicePageProps {
|
||||
params: Promise<{
|
||||
@@ -24,13 +24,13 @@ interface ServicePageProps {
|
||||
// This pre-generates known pages, but new pages can still be generated on-demand
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/services/`,
|
||||
`${API_CONFIG.BASE_URL}/api/services/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||
}
|
||||
);
|
||||
@@ -57,13 +57,13 @@ export async function generateMetadata({ params }: ServicePageProps) {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/services/${slug}/`,
|
||||
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||
}
|
||||
);
|
||||
@@ -87,13 +87,13 @@ const ServicePage = async ({ params }: ServicePageProps) => {
|
||||
const { slug } = await params;
|
||||
|
||||
try {
|
||||
// Use internal API URL for server-side requests
|
||||
const apiUrl = process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1086';
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/services/${slug}/`,
|
||||
`${API_CONFIG.BASE_URL}/api/services/${slug}/`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: getApiHeaders(),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 60 }, // Revalidate every minute for faster image updates
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import Image from "next/legacy/image";
|
||||
import time from "@/public/images/time.png";
|
||||
import trans from "@/public/images/trans.png";
|
||||
import support from "@/public/images/support.png";
|
||||
import skill from "@/public/images/skill.png";
|
||||
|
||||
const Thrive = () => {
|
||||
return (
|
||||
@@ -20,7 +16,7 @@ const Thrive = () => {
|
||||
<div className="row vertical-column-gap-lg mt-60">
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={time} alt="Image" width={80} height={80} />
|
||||
<Image src="/images/time.png" alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
@@ -35,7 +31,7 @@ const Thrive = () => {
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={trans} alt="Image" width={80} height={80} />
|
||||
<Image src="/images/trans.png" alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
@@ -50,7 +46,7 @@ const Thrive = () => {
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={support} alt="Image" width={80} height={80} />
|
||||
<Image src="/images/support.png" alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">Support</h4>
|
||||
@@ -63,7 +59,7 @@ const Thrive = () => {
|
||||
</div>
|
||||
<div className="col-12 col-md-6 fade-top">
|
||||
<div className="thumb">
|
||||
<Image src={skill} alt="Image" width={80} height={80} />
|
||||
<Image src="/images/skill.png" alt="Image" width={80} height={80} />
|
||||
</div>
|
||||
<div className="content mt-40">
|
||||
<h4 className="mt-8 title-anim fw-7 text-white mb-16">
|
||||
|
||||
@@ -3,7 +3,6 @@ import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudies } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
|
||||
const CaseItems = () => {
|
||||
const { caseStudies, loading: casesLoading } = useCaseStudies();
|
||||
@@ -56,7 +55,7 @@ const CaseItems = () => {
|
||||
<div className="thumb mb-24">
|
||||
<Link href={`/case-study/${caseStudy.slug}`} className="w-100">
|
||||
<Image
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : one}
|
||||
src={caseStudy.thumbnail ? getImageUrl(caseStudy.thumbnail) : "/images/case/one.png"}
|
||||
className="w-100 mh-300"
|
||||
alt={caseStudy.title}
|
||||
width={600}
|
||||
|
||||
@@ -5,8 +5,6 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import poster from "@/public/images/case/poster.png";
|
||||
import project from "@/public/images/case/project.png";
|
||||
|
||||
interface CaseSingleProps {
|
||||
slug: string;
|
||||
|
||||
@@ -3,7 +3,6 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCaseStudy } from "@/lib/hooks/useCaseStudy";
|
||||
import { getImageUrl } from "@/lib/imageUtils";
|
||||
import one from "@/public/images/case/one.png";
|
||||
|
||||
interface RelatedCaseProps {
|
||||
slug: string;
|
||||
@@ -34,7 +33,7 @@ const RelatedCase = ({ slug }: RelatedCaseProps) => {
|
||||
<Link href={`/case-study/${relatedCase.slug}`} className="case-link">
|
||||
<div className="case-image-wrapper">
|
||||
<Image
|
||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : one}
|
||||
src={relatedCase.thumbnail ? getImageUrl(relatedCase.thumbnail) : "/images/case/one.png"}
|
||||
className="case-image"
|
||||
alt={relatedCase.title}
|
||||
width={400}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import thumb from "@/public/images/contact-thumb.png";
|
||||
import { contactApiService, ContactFormData } from "@/lib/api/contactService";
|
||||
|
||||
const ContactSection = () => {
|
||||
|
||||
@@ -5,14 +5,15 @@ import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { useServices } from "@/lib/hooks/useServices";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
import one from "@/public/images/overview/one.png";
|
||||
import two from "@/public/images/overview/two.png";
|
||||
import three from "@/public/images/overview/three.png";
|
||||
import four from "@/public/images/overview/four.png";
|
||||
import five from "@/public/images/overview/five.png";
|
||||
|
||||
// Default images array for fallback
|
||||
const defaultImages = [one, two, three, four, five];
|
||||
// Default images array for fallback - use string paths
|
||||
const defaultImages = [
|
||||
"/images/overview/one.png",
|
||||
"/images/overview/two.png",
|
||||
"/images/overview/three.png",
|
||||
"/images/overview/four.png",
|
||||
"/images/overview/five.png"
|
||||
];
|
||||
|
||||
const Overview = () => {
|
||||
// Memoize the parameters to prevent infinite re-renders
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import thumb from "@/public/images/leading.jpg";
|
||||
|
||||
const ServiceIntro = () => {
|
||||
return (
|
||||
@@ -11,7 +10,7 @@ const ServiceIntro = () => {
|
||||
<div className="tp-service__thumb" style={{ maxWidth: '400px', border: 'none', padding: 0, margin: 0, overflow: 'hidden', borderRadius: '8px' }}>
|
||||
<Link href="services">
|
||||
<Image
|
||||
src={thumb}
|
||||
src="/images/leading.jpg"
|
||||
alt="Enterprise Software Solutions"
|
||||
width={400}
|
||||
height={500}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { useEffect } from "react";
|
||||
import Image from "next/legacy/image";
|
||||
import gsap from "gsap";
|
||||
import ScrollTrigger from "gsap/dist/ScrollTrigger";
|
||||
import thumb from "@/public/images/transform-thumb.png";
|
||||
import teamThumb from "@/public/images/team-thumb.png";
|
||||
import { Service } from "@/lib/api/serviceService";
|
||||
import { serviceUtils } from "@/lib/api/serviceService";
|
||||
|
||||
@@ -55,7 +53,7 @@ const Transform = ({ service }: TransformProps) => {
|
||||
<div className="transform__thumb">
|
||||
<div className="enterprise-image-wrapper">
|
||||
<Image
|
||||
src={serviceUtils.getServiceImageUrl(service) || thumb}
|
||||
src={serviceUtils.getServiceImageUrl(service) || "/images/transform-thumb.png"}
|
||||
className="enterprise-service-image"
|
||||
alt={service.title}
|
||||
width={600}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { usePathname } from "next/navigation";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import Image from "next/legacy/image";
|
||||
import Link from "next/link";
|
||||
import logoLight from "@/public/images/logo-light.png";
|
||||
|
||||
interface OffcanvasMenuProps {
|
||||
isOffcanvasOpen: boolean;
|
||||
@@ -67,7 +66,7 @@ const OffcanvasMenu = ({
|
||||
<div className="offcanvas-menu__header nav-fade">
|
||||
<div className="logo">
|
||||
<Link href="/" className="logo-img">
|
||||
<Image src={logoLight} priority alt="Image" title="Logo" width={160} height={60} />
|
||||
<Image src="/images/logo-light.png" priority alt="Image" title="Logo" width={160} height={60} />
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -45,7 +45,52 @@ export const API_BASE_URL = isServer
|
||||
|
||||
// Internal API key for server-side requests (must match backend INTERNAL_API_KEY)
|
||||
// This is only used when calling backend directly (build time or internal requests)
|
||||
export const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY || '9hZtPwyScigoBAl59Uvcz_9VztSRC6Zt_6L1B2xTM2M';
|
||||
// SECURITY: Never hardcode API keys in production - always use environment variables
|
||||
const getInternalApiKey = (): string => {
|
||||
const apiKey = process.env.INTERNAL_API_KEY;
|
||||
|
||||
// Check if we're in build phase (Next.js build context)
|
||||
// During build, NEXT_RUNTIME is typically not set
|
||||
// Also check for specific build phases
|
||||
const isBuildTime =
|
||||
!process.env.NEXT_RUNTIME || // Most reliable indicator - not set during build
|
||||
process.env.NEXT_PHASE === 'phase-production-build' ||
|
||||
process.env.NEXT_PHASE === 'phase-production-compile' ||
|
||||
process.env.NEXT_PHASE === 'phase-development-build';
|
||||
|
||||
if (!apiKey) {
|
||||
// During build time, be lenient - allow build to proceed
|
||||
// The key will be validated when actually used (in getApiHeaders)
|
||||
if (isBuildTime) {
|
||||
// Build time: allow fallback (will be validated when actually used)
|
||||
return 'build-time-fallback-key';
|
||||
}
|
||||
|
||||
// Runtime production: require the key (but only validate when actually used)
|
||||
if (isProduction) {
|
||||
// Don't throw here - validate lazily in getApiHeaders when actually needed
|
||||
// This allows the build to complete even if key is missing
|
||||
return 'runtime-requires-key';
|
||||
}
|
||||
|
||||
// Development fallback (only for local development)
|
||||
return 'dev-key-not-for-production';
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
};
|
||||
|
||||
// Lazy getter - only evaluates when accessed
|
||||
let _internalApiKey: string | null = null;
|
||||
export const getInternalApiKeyLazy = (): string => {
|
||||
if (_internalApiKey === null) {
|
||||
_internalApiKey = getInternalApiKey();
|
||||
}
|
||||
return _internalApiKey;
|
||||
};
|
||||
|
||||
// For backward compatibility - evaluates at module load but uses lenient validation
|
||||
export const INTERNAL_API_KEY = getInternalApiKeyLazy();
|
||||
|
||||
// Helper to get headers for API requests
|
||||
// Adds API key header when calling internal backend directly
|
||||
@@ -55,9 +100,25 @@ export const getApiHeaders = (): Record<string, string> => {
|
||||
};
|
||||
|
||||
// If we're calling the internal backend directly (not through nginx),
|
||||
// add the API key header
|
||||
// add the API key header (lazy evaluation - only when actually needed)
|
||||
if (isServer && API_BASE_URL.includes('127.0.0.1:1086')) {
|
||||
headers['X-Internal-API-Key'] = INTERNAL_API_KEY;
|
||||
const apiKey = getInternalApiKeyLazy();
|
||||
|
||||
// Validate API key when actually used (not at module load time)
|
||||
if (apiKey === 'runtime-requires-key' && isProduction) {
|
||||
const actualKey = process.env.INTERNAL_API_KEY;
|
||||
if (!actualKey) {
|
||||
throw new Error(
|
||||
'INTERNAL_API_KEY environment variable is required in production runtime. ' +
|
||||
'Set it in your .env.production file or environment variables.'
|
||||
);
|
||||
}
|
||||
// Update cached value
|
||||
_internalApiKey = actualKey;
|
||||
headers['X-Internal-API-Key'] = actualKey;
|
||||
} else {
|
||||
headers['X-Internal-API-Key'] = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
|
||||
@@ -10,6 +10,40 @@ export const FALLBACK_IMAGES = {
|
||||
DEFAULT: '/images/logo.png'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the correct image URL for the current environment
|
||||
*
|
||||
* During build: Use internal backend URL so Next.js can fetch images
|
||||
* During runtime (client): Use relative URLs (nginx serves media files)
|
||||
* During runtime (server/SSR): Use relative URLs (nginx serves media files)
|
||||
* In development: Use API_BASE_URL (which points to backend)
|
||||
*/
|
||||
function getImageBaseUrl(): string {
|
||||
const isServer = typeof window === 'undefined';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Check if we're in build phase (Next.js build context)
|
||||
const isBuildTime =
|
||||
!process.env.NEXT_RUNTIME || // Not set during build
|
||||
process.env.NEXT_PHASE === 'phase-production-build' ||
|
||||
process.env.NEXT_PHASE === 'phase-production-compile';
|
||||
|
||||
// During build time in production: use internal backend URL
|
||||
// This allows Next.js to fetch images during static generation
|
||||
if (isProduction && isBuildTime && isServer) {
|
||||
return process.env.INTERNAL_API_URL || 'http://127.0.0.1:1086';
|
||||
}
|
||||
|
||||
// Runtime (both client and server): use relative URLs
|
||||
// Nginx will serve /media/ files directly
|
||||
if (isProduction) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Development: use API_BASE_URL (which points to backend)
|
||||
return API_BASE_URL;
|
||||
}
|
||||
|
||||
export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
if (!imageUrl || imageUrl.trim() === '') {
|
||||
return fallback || FALLBACK_IMAGES.DEFAULT;
|
||||
@@ -20,22 +54,28 @@ export function getValidImageUrl(imageUrl?: string, fallback?: string): string {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /media/, it's a Django media file - prepend API base URL
|
||||
// Get the base URL for images (handles client/server differences)
|
||||
const baseUrl = getImageBaseUrl();
|
||||
|
||||
// If it starts with /media/, it's a Django media file
|
||||
if (imageUrl.startsWith('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
// In production client-side, baseUrl is empty, so this becomes /media/... (correct)
|
||||
// In production server-side, baseUrl is the domain, so this becomes https://domain.com/media/... (correct)
|
||||
return `${baseUrl}${imageUrl}`;
|
||||
}
|
||||
|
||||
// If it starts with /images/, it's a local public file
|
||||
// If it starts with /images/, it's a local public file (always relative)
|
||||
if (imageUrl.startsWith('/images/')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// If it starts with /, check if it's a media file
|
||||
if (imageUrl.startsWith('/')) {
|
||||
// If it contains /media/, prepend API base URL
|
||||
// If it contains /media/, prepend base URL
|
||||
if (imageUrl.includes('/media/')) {
|
||||
return `${API_BASE_URL}${imageUrl}`;
|
||||
return `${baseUrl}${imageUrl}`;
|
||||
}
|
||||
// Other absolute paths (like /static/) are served directly
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ const nextConfig = {
|
||||
// Enable standalone output for optimized production deployment
|
||||
output: 'standalone',
|
||||
images: {
|
||||
// Enable image optimization in standalone mode
|
||||
unoptimized: false,
|
||||
// Disable image optimization - nginx serves images directly
|
||||
// This prevents 400 errors from Next.js trying to optimize relative URLs
|
||||
// Images are already optimized and served efficiently by nginx
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
min-height: calc(var(--vh, 1vh) * 100); // Dynamic viewport height for mobile browsers
|
||||
min-height: -webkit-fill-available; // iOS viewport fix
|
||||
background: #0a0a0a;
|
||||
overflow-x: hidden; // Prevent horizontal scroll
|
||||
overflow-y: auto; // Allow vertical scroll if content is too long
|
||||
overflow: hidden; // Prevent all scrolling in banner
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start; // Align content to top
|
||||
|
||||
@@ -200,9 +200,9 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
# Frontend public images - must come before root location
|
||||
location ~ ^/images/(.*)$ {
|
||||
alias /var/www/GNX-WEB/frontEnd/public/images/$1;
|
||||
# Frontend public images - use prefix location (must come before root location)
|
||||
location /images/ {
|
||||
alias /var/www/GNX-WEB/frontEnd/public/images/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
@@ -216,6 +216,9 @@ server {
|
||||
image/webp webp;
|
||||
}
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Try files, then fallback
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Root location - Frontend (Next.js) - MUST be last
|
||||
|
||||
Reference in New Issue
Block a user