import React from 'react'; import type { AppProps } from 'next/app'; import type { ReactElement, ReactNode } from 'react'; import type { NextPage } from 'next'; import Head from 'next/head'; import { store } from '../stores/store'; import { Provider } from 'react-redux'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '../lib/queryClient'; // Import Instrument Sans font (self-hosted via Fontsource) import '@fontsource-variable/instrument-sans'; import '@fontsource-variable/instrument-sans/wdth.css'; // Condensed width axis import '../css/main.css'; import axios from 'axios'; import { baseURLApi } from '../config'; import { useRouter } from 'next/router'; import ErrorBoundary from '../components/ErrorBoundary'; import 'intro.js/introjs.css'; import { appWithTranslation } from 'next-i18next'; import '../i18n'; import IntroGuide from '../components/IntroGuide'; import { appSteps, loginSteps, usersSteps, rolesSteps, } from '../stores/introSteps'; import { DownloadProvider } from '../context/DownloadContext'; import { logger } from '../lib/logger'; import { disablePresignedUrls } from '../lib/assetUrl'; /** * Check if a URL is a presigned S3 URL */ const isPresignedS3Url = (url: string): boolean => { return url.includes('X-Amz-Signature=') || url.includes('x-amz-signature='); }; // Initialize axios axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API ? process.env.NEXT_PUBLIC_BACK_API : baseURLApi; axios.defaults.headers.common['Content-Type'] = 'application/json'; // Set up axios request interceptor synchronously to ensure token is always attached axios.interceptors.request.use( (config) => { if (typeof window !== 'undefined') { const token = sessionStorage.getItem('token') || localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } else { delete config.headers.Authorization; } } return config; }, (error) => Promise.reject(error), ); // Set up axios response interceptor to handle 401 errors and presigned URL failures axios.interceptors.response.use( (response) => response, (error) => { if (typeof window !== 'undefined') { const status = error?.response?.status; const requestUrl = `${error?.config?.url || ''}`; const isLoginRequest = requestUrl.includes('/auth/signin/local') || requestUrl.includes('auth/signin/local'); // Detect presigned S3 URL failures (CORS not configured) // Network errors (status 0) or CORS errors typically indicate S3 CORS issues if ( isPresignedS3Url(requestUrl) && (!status || status === 0 || error.message?.includes('Network Error')) ) { logger.info('[axios] Presigned URL failed, disabling presigned URLs', { url: requestUrl.slice(0, 80), }); disablePresignedUrls(); } if (status === 401 && !isLoginRequest) { // Clear stored tokens sessionStorage.removeItem('token'); sessionStorage.removeItem('user'); localStorage.removeItem('token'); localStorage.removeItem('user'); delete axios.defaults.headers.common['Authorization']; // Redirect to login if not already there if (!window.location.pathname.includes('/login')) { window.location.href = '/login'; } } } return Promise.reject(error); }, ); export type NextPageWithLayout
, IP = P> = NextPage<
P,
IP
> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page);
const router = useRouter();
const [stepsEnabled, setStepsEnabled] = React.useState(false);
const [stepName, setStepName] = React.useState('');
const [steps, setSteps] = React.useState([]);
// Register service worker for PWA offline support
React.useEffect(() => {
if (
typeof window !== 'undefined' &&
'serviceWorker' in navigator &&
process.env.NODE_ENV === 'production'
) {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
logger.info('[PWA] Service worker registered:', {
scope: registration.scope,
});
// Check for updates periodically
setInterval(
() => {
registration.update();
},
60 * 60 * 1000,
); // Check every hour
})
.catch((error) => {
logger.error(
'[PWA] Service worker registration failed:',
error instanceof Error ? error : { error },
);
});
}
}, []);
React.useEffect(() => {
// Tour is disabled by default in generated projects.
return;
const isCompleted = (stepKey: string) => {
return localStorage.getItem(`completed_${stepKey}`) === 'true';
};
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
setSteps(loginSteps);
setStepName('loginSteps');
setStepsEnabled(true);
} else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
setTimeout(() => {
setSteps(appSteps);
setStepName('appSteps');
setStepsEnabled(true);
}, 1000);
} else if (
router.pathname === '/users/users-list' &&
!isCompleted('usersSteps')
) {
setTimeout(() => {
setSteps(usersSteps);
setStepName('usersSteps');
setStepsEnabled(true);
}, 1000);
} else if (
router.pathname === '/roles/roles-list' &&
!isCompleted('rolesSteps')
) {
setTimeout(() => {
setSteps(rolesSteps);
setStepName('rolesSteps');
setStepsEnabled(true);
}, 1000);
} else {
setSteps([]);
setStepsEnabled(false);
}
}, [router.pathname]);
const handleExit = () => {
setStepsEnabled(false);
};
const title = 'Shimahara Visual';
const description =
'Platform to build, preview (stage), and publish offline-ready interactive tours on project subdomains.';
const url = 'https://flatlogic.com/';
const image =
'https://project-screens.s3.amazonaws.com/screenshots/39215/app-hero-20260316-121031.png';
const imageWidth = '1920';
const imageHeight = '960';
return (